node-telegram-bot-api
Version:
1,283 lines (1,200 loc) • 133 kB
JavaScript
// shims
require('array.prototype.findindex').shim(); // for Node.js v0.x
const errors = require('./errors');
const TelegramBotWebHook = require('./telegramWebHook');
const TelegramBotPolling = require('./telegramPolling');
const debug = require('debug')('node-telegram-bot-api');
const EventEmitter = require('eventemitter3');
const fileType = require('file-type');
const request = require('@cypress/request-promise');
const streamedRequest = require('@cypress/request');
const qs = require('querystring');
const stream = require('stream');
const mime = require('mime');
const path = require('path');
const URL = require('url');
const fs = require('fs');
const pump = require('pump');
const deprecate = require('./utils').deprecate;
const _messageTypes = [
'text',
'animation',
'audio',
'channel_chat_created',
'contact',
'delete_chat_photo',
'dice',
'document',
'game',
'group_chat_created',
'invoice',
'left_chat_member',
'location',
'migrate_from_chat_id',
'migrate_to_chat_id',
'new_chat_members',
'new_chat_photo',
'new_chat_title',
'passport_data',
'photo',
'pinned_message',
'poll',
'sticker',
'successful_payment',
'supergroup_chat_created',
'video',
'video_note',
'voice',
'video_chat_started',
'video_chat_ended',
'video_chat_participants_invited',
'video_chat_scheduled',
'message_auto_delete_timer_changed',
'chat_invite_link',
'chat_member_updated',
'web_app_data',
'message_reaction'
];
const _deprecatedMessageTypes = [
'new_chat_participant', 'left_chat_participant'
];
/**
* JSON-serialize data. If the provided data is already a String,
* return it as is.
* @private
* @param {*} data
* @return {String}
*/
function stringify(data) {
if (typeof data === 'string') {
return data;
}
return JSON.stringify(data);
}
class TelegramBot extends EventEmitter {
/**
* The different errors the library uses.
* @type {Object}
*/
static get errors() {
return errors;
}
/**
* The types of message updates the library handles.
* @type {String[]}
*/
static get messageTypes() {
return _messageTypes;
}
/**
* Add listener for the specified [event](https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#events).
* This is the usual `emitter.on()` method.
* @param {String} event
* @param {Function} listener
* @see {@link https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#events|Available events}
* @see https://nodejs.org/api/events.html#events_emitter_on_eventname_listener
*/
on(event, listener) {
if (_deprecatedMessageTypes.indexOf(event) !== -1) {
const url = 'https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#events';
deprecate(`Events ${_deprecatedMessageTypes.join(',')} are deprecated. See the updated list of events: ${url}`);
}
super.on(event, listener);
}
/**
* Both request method to obtain messages are implemented. To use standard polling, set `polling: true`
* on `options`. Notice that [webHook](https://core.telegram.org/bots/api#setwebhook) will need a SSL certificate.
* Emits `message` when a message arrives.
*
* @class TelegramBot
* @constructor
* @param {String} token Bot Token
* @param {Object} [options]
* @param {Boolean|Object} [options.polling=false] Set true to enable polling or set options.
* If a WebHook has been set, it will be deleted automatically.
* @param {String|Number} [options.polling.timeout=10] *Deprecated. Use `options.polling.params` instead*.
* Timeout in seconds for long polling.
* @param {Boolean} [options.testEnvironment=false] Set true to work with test enviroment.
* When working with the test environment, you may use HTTP links without TLS to test your Web App.
* @param {String|Number} [options.polling.interval=300] Interval between requests in miliseconds
* @param {Boolean} [options.polling.autoStart=true] Start polling immediately
* @param {Object} [options.polling.params] Parameters to be used in polling API requests.
* See https://core.telegram.org/bots/api#getupdates for more information.
* @param {Number} [options.polling.params.timeout=10] Timeout in seconds for long polling.
* @param {Boolean|Object} [options.webHook=false] Set true to enable WebHook or set options
* @param {String} [options.webHook.host="0.0.0.0"] Host to bind to
* @param {Number} [options.webHook.port=8443] Port to bind to
* @param {String} [options.webHook.key] Path to file with PEM private key for webHook server.
* The file is read **synchronously**!
* @param {String} [options.webHook.cert] Path to file with PEM certificate (public) for webHook server.
* The file is read **synchronously**!
* @param {String} [options.webHook.pfx] Path to file with PFX private key and certificate chain for webHook server.
* The file is read **synchronously**!
* @param {Boolean} [options.webHook.autoOpen=true] Open webHook immediately
* @param {Object} [options.webHook.https] Options to be passed to `https.createServer()`.
* Note that `options.webHook.key`, `options.webHook.cert` and `options.webHook.pfx`, if provided, will be
* used to override `key`, `cert` and `pfx` in this object, respectively.
* See https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener for more information.
* @param {String} [options.webHook.healthEndpoint="/healthz"] An endpoint for health checks that always responds with 200 OK
* @param {Boolean} [options.onlyFirstMatch=false] Set to true to stop after first match. Otherwise, all regexps are executed
* @param {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
* @param {Boolean} [options.filepath=true] Allow passing file-paths as arguments when sending files,
* such as photos using `TelegramBot#sendPhoto()`. See [usage information][usage-sending-files-performance]
* for more information on this option and its consequences.
* @param {Boolean} [options.badRejection=false] Set to `true`
* **if and only if** the Node.js version you're using terminates the
* process on unhandled rejections. This option is only for
* *forward-compatibility purposes*.
* @see https://core.telegram.org/bots/api
*/
constructor(token, options = {}) {
super();
this.token = token;
this.options = options;
this.options.polling = (typeof options.polling === 'undefined') ? false : options.polling;
this.options.webHook = (typeof options.webHook === 'undefined') ? false : options.webHook;
this.options.baseApiUrl = options.baseApiUrl || 'https://api.telegram.org';
this.options.filepath = (typeof options.filepath === 'undefined') ? true : options.filepath;
this.options.badRejection = (typeof options.badRejection === 'undefined') ? false : options.badRejection;
this._textRegexpCallbacks = [];
this._replyListenerId = 0;
this._replyListeners = [];
this._polling = null;
this._webHook = null;
if (options.polling) {
const autoStart = options.polling.autoStart;
if (typeof autoStart === 'undefined' || autoStart === true) {
this.startPolling();
}
}
if (options.webHook) {
const autoOpen = options.webHook.autoOpen;
if (typeof autoOpen === 'undefined' || autoOpen === true) {
this.openWebHook();
}
}
}
/**
* Generates url with bot token and provided path/method you want to be got/executed by bot
* @param {String} path
* @return {String} url
* @private
* @see https://core.telegram.org/bots/api#making-requests
*/
_buildURL(_path) {
return `${this.options.baseApiUrl}/bot${this.token}${this.options.testEnvironment ? '/test' : ''}/${_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') {
obj.reply_markup = stringify(replyMarkup);
}
}
/**
* Fix 'entities' or 'caption_entities' or 'explanation_entities' parameter by making it JSON-serialized, as
* required by the Telegram Bot API
* @param {Object} obj Object;
* @private
* @see https://core.telegram.org/bots/api#sendmessage
* @see https://core.telegram.org/bots/api#copymessage
* @see https://core.telegram.org/bots/api#sendpoll
*/
_fixEntitiesField(obj) {
const entities = obj.entities;
const captionEntities = obj.caption_entities;
const explanationEntities = obj.explanation_entities;
if (entities && typeof entities !== 'string') {
obj.entities = stringify(entities);
}
if (captionEntities && typeof captionEntities !== 'string') {
obj.caption_entities = stringify(captionEntities);
}
if (explanationEntities && typeof explanationEntities !== 'string') {
obj.explanation_entities = stringify(explanationEntities);
}
}
_fixAddFileThumbnail(options, opts) {
if (options.thumb) {
if (opts.formData === null) {
opts.formData = {};
}
const attachName = 'photo';
const [formData] = this._formatSendData(attachName, options.thumb.replace('attach://', ''));
if (formData) {
opts.formData[attachName] = formData[attachName];
opts.qs.thumbnail = `attach://${attachName}`;
}
}
}
/**
* Fix 'reply_parameters' 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
*/
_fixReplyParameters(obj) {
if (obj.hasOwnProperty('reply_parameters') && typeof obj.reply_parameters !== 'string') {
obj.reply_parameters = stringify(obj.reply_parameters);
}
}
/**
* Make request against the API
* @param {String} _path API endpoint
* @param {Object} [options]
* @private
* @return {Promise}
*/
_request(_path, options = {}) {
if (!this.token) {
return Promise.reject(new errors.FatalError('Telegram Bot Token not provided!'));
}
if (this.options.request) {
Object.assign(options, this.options.request);
}
if (options.form) {
this._fixReplyMarkup(options.form);
this._fixEntitiesField(options.form);
this._fixReplyParameters(options.form);
}
if (options.qs) {
this._fixReplyMarkup(options.qs);
this._fixReplyParameters(options.qs);
}
options.method = 'POST';
options.url = this._buildURL(_path);
options.simple = false;
options.resolveWithFullResponse = true;
options.forever = true;
debug('HTTP request: %j', options);
return request(options)
.then(resp => {
let data;
try {
data = resp.body = JSON.parse(resp.body);
} catch (err) {
throw new errors.ParseError(`Error parsing response: ${resp.body}`, resp);
}
if (data.ok) {
return data.result;
}
throw new errors.TelegramError(`${data.error_code} ${data.description}`, resp);
}).catch(error => {
// TODO: why can't we do `error instanceof errors.BaseError`?
if (error.response) throw error;
throw new errors.FatalError(error);
});
}
/**
* Format data to be uploaded; handles file paths, streams and buffers
* @param {String} type
* @param {String|stream.Stream|Buffer} data
* @param {Object} fileOptions File options
* @param {String} [fileOptions.filename] File name
* @param {String} [fileOptions.contentType] Content type (i.e. MIME)
* @return {Array} formatted
* @return {Object} formatted[0] formData
* @return {String} formatted[1] fileId
* @throws Error if Buffer file type is not supported.
* @see https://npmjs.com/package/file-type
* @private
*/
_formatSendData(type, data, fileOptions = {}) {
const deprecationMessage =
'See https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files' +
' for more information on how sending files has been improved and' +
' on how to disable this deprecation message altogether.';
let filedata = data;
let filename = fileOptions.filename;
let contentType = fileOptions.contentType;
if (data instanceof stream.Stream) {
if (!filename && data.path) {
// Will be 'null' if could not be parsed.
// For example, 'data.path' === '/?id=123' from 'request("https://example.com/?id=123")'
const url = URL.parse(path.basename(data.path.toString()));
if (url.pathname) {
filename = qs.unescape(url.pathname);
}
}
} else if (Buffer.isBuffer(data)) {
if (!filename && !process.env.NTBA_FIX_350) {
deprecate(`Buffers will have their filenames default to "filename" instead of "data". ${deprecationMessage}`);
filename = 'data';
}
if (!contentType) {
const filetype = fileType(data);
if (filetype) {
contentType = filetype.mime;
const ext = filetype.ext;
if (ext && !process.env.NTBA_FIX_350) {
filename = `${filename}.${ext}`;
}
} else if (!process.env.NTBA_FIX_350) {
deprecate(`An error will no longer be thrown if file-type of buffer could not be detected. ${deprecationMessage}`);
throw new errors.FatalError('Unsupported Buffer file-type');
}
}
} else if (data) {
if (this.options.filepath && fs.existsSync(data)) {
filedata = fs.createReadStream(data);
if (!filename) {
filename = path.basename(data);
}
} else {
return [null, data];
}
} else {
return [null, data];
}
filename = filename || 'filename';
contentType = contentType || mime.lookup(filename);
if (process.env.NTBA_FIX_350) {
contentType = contentType || 'application/octet-stream';
} else {
deprecate(`In the future, content-type of files you send will default to "application/octet-stream". ${deprecationMessage}`);
}
// TODO: Add missing file extension.
return [{
[type]: {
value: filedata,
options: {
filename,
contentType,
},
},
}, null];
}
/**
* Start polling.
* Rejects returned promise if a WebHook is being used by this instance.
* @param {Object} [options]
* @param {Boolean} [options.restart=true] Consecutive calls to this method causes polling to be restarted
* @return {Promise}
*/
startPolling(options = {}) {
if (this.hasOpenWebHook()) {
return Promise.reject(new errors.FatalError('Polling and WebHook are mutually exclusive'));
}
options.restart = typeof options.restart === 'undefined' ? true : options.restart;
if (!this._polling) {
this._polling = new TelegramBotPolling(this);
}
return this._polling.start(options);
}
/**
* Alias of `TelegramBot#startPolling()`. This is **deprecated**.
* @param {Object} [options]
* @return {Promise}
* @deprecated
*/
initPolling() {
deprecate('TelegramBot#initPolling() is deprecated. Use TelegramBot#startPolling() instead.');
return this.startPolling();
}
/**
* Stops polling after the last polling request resolves.
* Multiple invocations do nothing if polling is already stopped.
* Returning the promise of the last polling request is **deprecated**.
* @param {Object} [options] Options
* @param {Boolean} [options.cancel] Cancel current request
* @param {String} [options.reason] Reason for stopping polling
* @return {Promise}
*/
stopPolling(options) {
if (!this._polling) {
return Promise.resolve();
}
return this._polling.stop(options);
}
/**
* Get link for file.
* Use this method to get link for file for subsequent use.
* Attention: link will be valid for 1 hour.
*
* This method is a sugar extension of the (getFile)[#getfilefileid] method,
* which returns just path to file on remote server (you will have to manually build full uri after that).
*
* @param {String} fileId File identifier to get info about
* @param {Object} [options] Additional Telegram query options
* @return {Promise} Promise which will have *fileURI* in resolve callback
* @see https://core.telegram.org/bots/api#getfile
*/
getFileLink(fileId, form = {}) {
return this.getFile(fileId, form)
.then(resp => `${this.options.baseApiUrl}/file/bot${this.token}/${resp.file_path}`);
}
/**
* Return a readable stream for file.
*
* `fileStream.path` is the specified file ID i.e. `fileId`.
* `fileStream` emits event `info` passing a single argument i.e.
* `info` with the interface `{ uri }` where `uri` is the URI of the
* file on Telegram servers.
*
* This method is a sugar extension of the [getFileLink](#TelegramBot+getFileLink) method,
* which returns the full URI to the file on remote server.
*
* @param {String} fileId File identifier to get info about
* @param {Object} [options] Additional Telegram query options
* @return {stream.Readable} fileStream
*/
getFileStream(fileId, form = {}) {
const fileStream = new stream.PassThrough();
fileStream.path = fileId;
this.getFileLink(fileId, form)
.then((fileURI) => {
fileStream.emit('info', {
uri: fileURI,
});
pump(streamedRequest(Object.assign({ uri: fileURI }, this.options.request)), fileStream);
})
.catch((error) => {
fileStream.emit('error', error);
});
return fileStream;
}
/**
* Downloads file in the specified folder.
*
* This method is a sugar extension of the [getFileStream](#TelegramBot+getFileStream) method,
* which returns a readable file stream.
*
* @param {String} fileId File identifier to get info about
* @param {String} downloadDir Absolute path to the folder in which file will be saved
* @param {Object} [options] Additional Telegram query options
* @return {Promise} Promise, which will have *filePath* of downloaded file in resolve callback
*/
downloadFile(fileId, downloadDir, form = {}) {
let resolve;
let reject;
const promise = new Promise((a, b) => {
resolve = a;
reject = b;
});
const fileStream = this.getFileStream(fileId, form);
fileStream.on('info', (info) => {
const fileName = info.uri.slice(info.uri.lastIndexOf('/') + 1);
// TODO: Ensure fileName doesn't contains slashes
const filePath = path.join(downloadDir, fileName);
pump(fileStream, fs.createWriteStream(filePath), (error) => {
if (error) { return reject(error); }
return resolve(filePath);
});
});
fileStream.on('error', (err) => {
reject(err);
});
return promise;
}
/**
* Register a RegExp to test against an incomming text message.
* @param {RegExp} regexpRexecuted with `exec`.
* @param {Function} callback Callback will be called with 2 parameters,
* the `msg` and the result of executing `regexp.exec` on message text.
*/
onText(regexp, callback) {
this._textRegexpCallbacks.push({ regexp, callback });
}
/**
* Remove a listener registered with `onText()`.
* @param {RegExp} regexp RegExp used previously in `onText()`
* @return {Object} deletedListener The removed reply listener if
* found. This object has `regexp` and `callback`
* properties. If not found, returns `null`.
*/
removeTextListener(regexp) {
const index = this._textRegexpCallbacks.findIndex((textListener) => {
return String(textListener.regexp) === String(regexp);
});
if (index === -1) {
return null;
}
return this._textRegexpCallbacks.splice(index, 1)[0];
}
/**
* Remove all listeners registered with `onText()`.
*/
clearTextListeners() {
this._textRegexpCallbacks = [];
}
/**
* Register a reply to wait for a message response.
*
* @param {Number|String} chatId The chat id where the message cames from.
* @param {Number|String} messageId The message id to be replied.
* @param {Function} callback Callback will be called with the reply
* message.
* @return {Number} id The ID of the inserted reply listener.
*/
onReplyToMessage(chatId, messageId, callback) {
const id = ++this._replyListenerId;
this._replyListeners.push({
id,
chatId,
messageId,
callback
});
return id;
}
/**
* Removes a reply that has been prev. registered for a message response.
* @param {Number} replyListenerId The ID of the reply listener.
* @return {Object} deletedListener The removed reply listener if
* found. This object has `id`, `chatId`, `messageId` and `callback`
* properties. If not found, returns `null`.
*/
removeReplyListener(replyListenerId) {
const index = this._replyListeners.findIndex((replyListener) => {
return replyListener.id === replyListenerId;
});
if (index === -1) {
return null;
}
return this._replyListeners.splice(index, 1)[0];
}
/**
* Removes all replies that have been prev. registered for a message response.
*
* @return {Array} deletedListeners An array of removed listeners.
*/
clearReplyListeners() {
this._replyListeners = [];
}
/**
* Return true if polling. Otherwise, false.
*
* @return {Boolean}
*/
isPolling() {
return this._polling ? this._polling.isPolling() : false;
}
/**
* Open webhook.
* Multiple invocations do nothing if webhook is already open.
* Rejects returned promise if Polling is being used by this instance.
*
* @return {Promise}
*/
openWebHook() {
if (this.isPolling()) {
return Promise.reject(new errors.FatalError('WebHook and Polling are mutually exclusive'));
}
if (!this._webHook) {
this._webHook = new TelegramBotWebHook(this);
}
return this._webHook.open();
}
/**
* Close webhook after closing all current connections.
* Multiple invocations do nothing if webhook is already closed.
*
* @return {Promise} Promise
*/
closeWebHook() {
if (!this._webHook) {
return Promise.resolve();
}
return this._webHook.close();
}
/**
* Return true if using webhook and it is open i.e. accepts connections.
* Otherwise, false.
*
* @return {Boolean}
*/
hasOpenWebHook() {
return this._webHook ? this._webHook.isOpen() : false;
}
/**
* 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 businessConnection = update.business_connection;
const businesssMessage = update.business_message;
const editedBusinessMessage = update.edited_business_message;
const deletedBusinessMessage = update.deleted_business_messages;
const messageReaction = update.message_reaction;
const messageReactionCount = update.message_reaction_count;
const inlineQuery = update.inline_query;
const chosenInlineResult = update.chosen_inline_result;
const callbackQuery = update.callback_query;
const shippingQuery = update.shipping_query;
const preCheckoutQuery = update.pre_checkout_query;
const poll = update.poll;
const pollAnswer = update.poll_answer;
const myChatMember = update.my_chat_member;
const chatMember = update.chat_member;
const chatJoinRequest = update.chat_join_request;
const chatBoost = update.chat_boost;
const removedChatBoost = update.removed_chat_boost;
if (message) {
debug('Process Update message %j', message);
const metadata = {};
metadata.type = TelegramBot.messageTypes.find((messageType) => {
return message[messageType];
});
this.emit('message', message, metadata);
if (metadata.type) {
debug('Emitting %s: %j', metadata.type, message);
this.emit(metadata.type, message, metadata);
}
if (message.text) {
debug('Text message');
this._textRegexpCallbacks.some(reg => {
debug('Matching %s with %s', message.text, reg.regexp);
if (!(reg.regexp instanceof RegExp)) {
reg.regexp = new RegExp(reg.regexp);
}
const result = reg.regexp.exec(message.text);
if (!result) {
return false;
}
// reset index so we start at the beginning of the regex each time
reg.regexp.lastIndex = 0;
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._replyListeners.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 (businessConnection) {
debug('Process Update business_connection %j', businessConnection);
this.emit('business_connection', businessConnection);
} else if (businesssMessage) {
debug('Process Update business_message %j', businesssMessage);
this.emit('business_message', businesssMessage);
} else if (editedBusinessMessage) {
debug('Process Update edited_business_message %j', editedBusinessMessage);
this.emit('edited_business_message', editedBusinessMessage);
} else if (deletedBusinessMessage) {
debug('Process Update deleted_business_messages %j', deletedBusinessMessage);
this.emit('deleted_business_messages', deletedBusinessMessage);
} else if (messageReaction) {
debug('Process Update message_reaction %j', messageReaction);
this.emit('message_reaction', messageReaction);
} else if (messageReactionCount) {
debug('Process Update message_reaction_count %j', messageReactionCount);
this.emit('message_reaction_count', messageReactionCount);
} 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);
} else if (shippingQuery) {
debug('Process Update shipping_query %j', shippingQuery);
this.emit('shipping_query', shippingQuery);
} else if (preCheckoutQuery) {
debug('Process Update pre_checkout_query %j', preCheckoutQuery);
this.emit('pre_checkout_query', preCheckoutQuery);
} else if (poll) {
debug('Process Update poll %j', poll);
this.emit('poll', poll);
} else if (pollAnswer) {
debug('Process Update poll_answer %j', pollAnswer);
this.emit('poll_answer', pollAnswer);
} else if (chatMember) {
debug('Process Update chat_member %j', chatMember);
this.emit('chat_member', chatMember);
} else if (myChatMember) {
debug('Process Update my_chat_member %j', myChatMember);
this.emit('my_chat_member', myChatMember);
} else if (chatJoinRequest) {
debug('Process Update my_chat_member %j', chatJoinRequest);
this.emit('chat_join_request', chatJoinRequest);
} else if (chatBoost) {
debug('Process Update chat_boost %j', chatBoost);
this.emit('chat_boost', chatBoost);
} else if (removedChatBoost) {
debug('Process Update removed_chat_boost %j', removedChatBoost);
this.emit('removed_chat_boost', removedChatBoost);
}
}
/** Start Telegram Bot API methods */
/**
* Use this method to receive incoming updates using long polling.
* This method has an [older, compatible signature][getUpdates-v0.25.0]
* that is being deprecated.
*
* @param {Object} [options] Additional Telegram query options
* @return {Promise}
* @see https://core.telegram.org/bots/api#getupdates
*/
getUpdates(form = {}) {
/* The older method signature was getUpdates(timeout, limit, offset).
* We need to ensure backwards-compatibility while maintaining
* consistency of the method signatures throughout the library */
if (typeof form !== 'object') {
/* eslint-disable no-param-reassign, prefer-rest-params */
deprecate('The method signature getUpdates(timeout, limit, offset) has been deprecated since v0.25.0');
form = {
timeout: arguments[0],
limit: arguments[1],
offset: arguments[2],
};
/* eslint-enable no-param-reassign, prefer-rest-params */
}
return this._request('getUpdates', { form });
}
/**
* Specify an url to receive incoming updates via an outgoing webHook.
* This method has an [older, compatible signature][setWebHook-v0.25.0]
* that is being deprecated.
*
* @param {String} url URL where Telegram will make HTTP Post. Leave empty to
* delete webHook.
* @param {Object} [options] Additional Telegram query options
* @param {String|stream.Stream} [options.certificate] PEM certificate key (public).
* @param {String} [options.secret_token] Optional secret token to be sent in a header `X-Telegram-Bot-Api-Secret-Token` in every webhook request.
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise}
* @see https://core.telegram.org/bots/api#setwebhook
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
setWebHook(url, options = {}, fileOptions = {}) {
/* The older method signature was setWebHook(url, cert).
* We need to ensure backwards-compatibility while maintaining
* consistency of the method signatures throughout the library */
let cert;
// Note: 'options' could be an object, if a stream was provided (in place of 'cert')
if (typeof options !== 'object' || options instanceof stream.Stream) {
deprecate('The method signature setWebHook(url, cert) has been deprecated since v0.25.0');
cert = options;
options = {}; // eslint-disable-line no-param-reassign
} else {
cert = options.certificate;
}
const opts = {
qs: options,
};
opts.qs.url = url;
if (cert) {
try {
const sendData = this._formatSendData('certificate', cert, fileOptions);
opts.formData = sendData[0];
opts.qs.certificate = sendData[1];
} catch (ex) {
return Promise.reject(ex);
}
}
return this._request('setWebHook', opts);
}
/**
* Use this method to remove webhook integration if you decide to
* switch back to getUpdates. Returns True on success.
* @param {Object} [options] Additional Telegram query options
* @return {Promise}
* @see https://core.telegram.org/bots/api#deletewebhook
*/
deleteWebHook(form = {}) {
return this._request('deleteWebhook', { form });
}
/**
* Use this method to get current webhook status.
* On success, returns a [WebhookInfo](https://core.telegram.org/bots/api#webhookinfo) object.
* If the bot is using getUpdates, will return an object with the
* url field empty.
* @param {Object} [options] Additional Telegram query options
* @return {Promise}
* @see https://core.telegram.org/bots/api#getwebhookinfo
*/
getWebHookInfo(form = {}) {
return this._request('getWebhookInfo', { form });
}
/**
* A simple method for testing your bot's authentication token. Requires no parameters.
*
* @param {Object} [options] Additional Telegram query options
* @return {Promise} basic information about the bot in form of a [User](https://core.telegram.org/bots/api#user) object.
* @see https://core.telegram.org/bots/api#getme
*/
getMe(form = {}) {
return this._request('getMe', { form });
}
/**
* This method log out your bot from the cloud Bot API server before launching the bot locally.
* You must log out the bot before running it locally, otherwise there is no guarantee that the bot will receive updates.
* After a successful call, you will not be able to log in again using the same token for 10 minutes.
*
* @param {Object} [options] Additional Telegram query options
* @return {Promise} True on success
* @see https://core.telegram.org/bots/api#logout
*/
logOut(form = {}) {
return this._request('logOut', { form });
}
/**
* This method close the bot instance before moving it from one local server to another.
* This method will return error 429 in the first 10 minutes after the bot is launched.
*
* @param {Object} [options] Additional Telegram query options
* @return {Promise} True on success
* @see https://core.telegram.org/bots/api#close
*/
close(form = {}) {
return this._request('close', { form });
}
/**
* Send text message.
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String} text Text of the message to be sent
* @param {Object} [options] Additional Telegram query options
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendmessage
*/
sendMessage(chatId, text, form = {}) {
form.chat_id = chatId;
form.text = text;
return this._request('sendMessage', { form });
}
/**
* Forward messages of any kind.
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* or username of the target channel (in the format `@channelusername`)
* @param {Number|String} fromChatId Unique identifier for the chat where the
* original message was sent (or channel username in the format `@channelusername`)
* @param {Number|String} messageId Unique message identifier in the chat specified in fromChatId
* @param {Object} [options] Additional Telegram query options
* @return {Promise}
* @see https://core.telegram.org/bots/api#forwardmessage
*/
forwardMessage(chatId, fromChatId, messageId, form = {}) {
form.chat_id = chatId;
form.from_chat_id = fromChatId;
form.message_id = messageId;
return this._request('forwardMessage', { form });
}
/**
* Use this method to forward multiple messages of any kind.
* If some of the specified messages can't be found or forwarded, they are skipped.
*
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* or username of the target channel (in the format `@channelusername`)
* @param {Number|String} fromChatId Unique identifier for the chat where the
* original message was sent (or channel username in the format `@channelusername`)
* @param {Array<Number|String>} messageIds Identifiers of 1-100 messages in the chat from_chat_id to forward.
* The identifiers must be specified in a strictly increasing order.
* @param {Object} [options] Additional Telegram query options
* @return {Promise} An array of MessageId of the sent messages on success
* @see https://core.telegram.org/bots/api#forwardmessages
*/
forwardMessages(chatId, fromChatId, messageIds, form = {}) {
form.chat_id = chatId;
form.from_chat_id = fromChatId;
form.message_ids = messageIds;
return this._request('forwardMessages', { form });
}
/**
* Copy messages of any kind. **Service messages and invoice messages can't be copied.**
* The method is analogous to the method forwardMessages, but the copied message doesn't
* have a link to the original message.
* Returns the MessageId of the sent message on success.
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {Number|String} fromChatId Unique identifier for the chat where the
* original message was sent
* @param {Number|String} messageId Unique message identifier
* @param {Object} [options] Additional Telegram query options
* @return {Promise} The [MessageId](https://core.telegram.org/bots/api#messageid) of the sent message on success
* @see https://core.telegram.org/bots/api#copymessage
*/
copyMessage(chatId, fromChatId, messageId, form = {}) {
form.chat_id = chatId;
form.from_chat_id = fromChatId;
form.message_id = messageId;
return this._request('copyMessage', { form });
}
/**
* Use this method to copy messages of any kind. If some of the specified messages can't be found or copied, they are skipped.
* Service messages, giveaway messages, giveaway winners messages, and invoice messages can't be copied.
* Returns the MessageId of the sent message on success.
* @param {Number|String} chatId Unique identifier for the target chat
* @param {Number|String} fromChatId Unique identifier for the chat where the
* original message was sent
* @param {Array} messageIds Identifiers of 1-100 messages in the chat from_chat_id to copy.
* The identifiers must be specified in a strictly increasing order.
* @param {Object} [options] Additional Telegram query options
* @return {Promise} An array of MessageId of the sent messages
* @see https://core.telegram.org/bots/api#copymessages
*/
copyMessages(chatId, fromChatId, messageIds, form = {}) {
form.chat_id = chatId;
form.from_chat_id = fromChatId;
form.message_ids = stringify(messageIds);
return this._request('copyMessages', { form });
}
/**
* Send photo
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} photo A file path or a Stream. Can
* also be a `file_id` previously uploaded
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendphoto
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendPhoto(chatId, photo, options = {}, fileOptions = {}) {
const opts = {
qs: options,
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('photo', photo, fileOptions);
opts.formData = sendData[0];
opts.qs.photo = sendData[1];
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendPhoto', opts);
}
/**
* Send audio
*
* **Your audio must be in the .MP3 or .M4A format.**
*
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} audio A file path, Stream or Buffer.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendaudio
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendAudio(chatId, audio, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('audio', audio, fileOptions);
opts.formData = sendData[0];
opts.qs.audio = sendData[1];
this._fixAddFileThumbnail(options, opts);
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendAudio', opts);
}
/**
* Send Document
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} doc A file path, Stream or Buffer.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendDocument
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendDocument(chatId, doc, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('document', doc, fileOptions);
opts.formData = sendData[0];
opts.qs.document = sendData[1];
this._fixAddFileThumbnail(options, opts);
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendDocument', opts);
}
/**
* Use this method to send video files, **Telegram clients support mp4 videos** (other formats may be sent as Document).
*
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} video A file path or Stream.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendvideo
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendVideo(chatId, video, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('video', video, fileOptions);
opts.formData = sendData[0];
opts.qs.video = sendData[1];
this._fixAddFileThumbnail(options, opts);
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendVideo', opts);
}
/**
* Use this method to send animation files (GIF or H.264/MPEG-4 AVC video without sound).
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} animation A file path, Stream or Buffer.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendanimation
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendAnimation(chatId, animation, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('animation', animation, fileOptions);
opts.formData = sendData[0];
opts.qs.animation = sendData[1];
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendAnimation', opts);
}
/**
* Send voice
*
* **Your audio must be in an .OGG file encoded with OPUS**, or in .MP3 format, or in .M4A format (other formats may be sent as Audio or Document)
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} voice A file path, Stream or Buffer.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @see https://core.telegram.org/bots/api#sendvoice
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendVoice(chatId, voice, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('voice', voice, fileOptions);
opts.formData = sendData[0];
opts.qs.voice = sendData[1];
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendVoice', opts);
}
/**
* Use this method to send video messages
* Telegram clients support **rounded square MPEG4 videos** of up to 1 minute long.
* @param {Number|String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {String|stream.Stream|Buffer} videoNote A file path or Stream.
* Can also be a `file_id` previously uploaded.
* @param {Object} [options] Additional Telegram query options
* @param {Object} [fileOptions] Optional file related meta-data
* @return {Promise} On success, the sent [Message](https://core.telegram.org/bots/api#message) object is returned
* @info The length parameter is actually optional. However, the API (at time of writing) requires you to always provide it until it is fixed.
* @see https://core.telegram.org/bots/api#sendvideonote
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendVideoNote(chatId, videoNote, options = {}, fileOptions = {}) {
const opts = {
qs: options
};
opts.qs.chat_id = chatId;
try {
const sendData = this._formatSendData('video_note', videoNote, fileOptions);
opts.formData = sendData[0];
opts.qs.video_note = sendData[1];
this._fixAddFileThumbnail(options, opts);
} catch (ex) {
return Promise.reject(ex);
}
return this._request('sendVideoNote', opts);
}
/**
* Use this method to send a group of photos or videos as an album.
*
* **Documents and audio files can be only grouped in an album with messages of the same type**
*
* If you wish to [specify file options](https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files),
* add a `fileOptions` property to the target input in `media`.
*
* @param {String} chatId Unique identifier for the target chat or username of the target channel (in the format `@channelusername`)
* @param {Array} media A JSON-serialized array describing photos and videos to be sent, must include 2–10 items
* @param {Object} [options] Additional Telegram query options
* @return {Promise} On success, an array of the sent [Messages](https://core.telegram.org/bots/api#message)
* is returned.
* @see https://core.telegram.org/bots/api#sendmediagroup
* @see https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files
*/
sendMediaGroup(chatId, media, options = {}) {
const opts = {
qs: options,
};
opts.qs.chat_id = chatId;
opts.formData = {};
const inputMedia = [];
let index = 0;
for (const input of media) {
const payload = Object.assign({}, input);
delete payload.media;
delete payload.fileOptions;
try {
const attachName = String(index);
const [formData, fileId] = this._formatSendData(attachName, input.media, input.fileOptions);
if (formData) {
opts.formData[attachName] = formData[a