UNPKG

node-telegram-bot-api

Version:
1,283 lines (1,200 loc) 133 kB
// 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