From d6083e4327bcf43196a7305da7e7a93b4b006a0a Mon Sep 17 00:00:00 2001 From: GochoMugo Date: Thu, 16 Nov 2017 20:27:17 +0300 Subject: [PATCH] src: Support file options, defaults in sending files --- .eslintignore | 1 + doc/api.md | 61 ++++++++++++++----- doc/usage.md | 39 ++++++++++++ src/telegram.js | 138 +++++++++++++++++++++++++----------------- test/telegram.js | 7 --- test/test.sendfile.js | 109 +++++++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+), 80 deletions(-) create mode 100644 test/test.sendfile.js diff --git a/.eslintignore b/.eslintignore index 417342d..66758cc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,3 @@ bin +lib *.md diff --git a/doc/api.md b/doc/api.md index adb8c25..031db25 100644 --- a/doc/api.md +++ b/doc/api.md @@ -30,13 +30,13 @@ TelegramBot * [.sendMessage(chatId, text, [options])](#TelegramBot+sendMessage) ⇒ Promise * [.answerInlineQuery(inlineQueryId, results, [options])](#TelegramBot+answerInlineQuery) ⇒ Promise * [.forwardMessage(chatId, fromChatId, messageId, [options])](#TelegramBot+forwardMessage) ⇒ Promise - * [.sendPhoto(chatId, photo, [options])](#TelegramBot+sendPhoto) ⇒ Promise - * [.sendAudio(chatId, audio, [options])](#TelegramBot+sendAudio) ⇒ Promise + * [.sendPhoto(chatId, photo, [options], [fileOpts])](#TelegramBot+sendPhoto) ⇒ Promise + * [.sendAudio(chatId, audio, [options], [fileOpts])](#TelegramBot+sendAudio) ⇒ Promise * [.sendDocument(chatId, doc, [options], [fileOpts])](#TelegramBot+sendDocument) ⇒ Promise * [.sendSticker(chatId, sticker, [options])](#TelegramBot+sendSticker) ⇒ Promise - * [.sendVideo(chatId, video, [options])](#TelegramBot+sendVideo) ⇒ Promise - * [.sendVideoNote(chatId, videoNote, [options])](#TelegramBot+sendVideoNote) ⇒ Promise - * [.sendVoice(chatId, voice, [options])](#TelegramBot+sendVoice) ⇒ Promise + * [.sendVideo(chatId, video, [options], [fileOpts])](#TelegramBot+sendVideo) ⇒ Promise + * [.sendVideoNote(chatId, videoNote, [options], [fileOpts])](#TelegramBot+sendVideoNote) ⇒ Promise + * [.sendVoice(chatId, voice, [options], [fileOpts])](#TelegramBot+sendVoice) ⇒ Promise * [.sendChatAction(chatId, action, [options])](#TelegramBot+sendChatAction) ⇒ Promise * [.kickChatMember(chatId, userId, [options])](#TelegramBot+kickChatMember) ⇒ Promise * [.unbanChatMember(chatId, userId, [options])](#TelegramBot+unbanChatMember) ⇒ Promise @@ -339,31 +339,41 @@ Forward messages of any kind. -### telegramBot.sendPhoto(chatId, photo, [options]) ⇒ Promise +### telegramBot.sendPhoto(chatId, photo, [options], [fileOpts]) ⇒ Promise Send photo **Kind**: instance method of [TelegramBot](#TelegramBot) -**See**: https://core.telegram.org/bots/api#sendphoto +**See** + +- https://core.telegram.org/bots/api#sendphoto +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | Unique identifier for the message recipient | | photo | String | stream.Stream | Buffer | A file path or a Stream. Can also be a `file_id` previously uploaded | | [options] | Object | Additional Telegram query options | +| [fileOpts] | Object | Optional file related meta-data | -### telegramBot.sendAudio(chatId, audio, [options]) ⇒ Promise +### telegramBot.sendAudio(chatId, audio, [options], [fileOpts]) ⇒ Promise Send audio **Kind**: instance method of [TelegramBot](#TelegramBot) -**See**: https://core.telegram.org/bots/api#sendaudio +**See** + +- https://core.telegram.org/bots/api#sendaudio +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | Unique identifier for the message recipient | | audio | String | stream.Stream | Buffer | A file path, Stream or Buffer. Can also be a `file_id` previously uploaded. | | [options] | Object | Additional Telegram query options | +| [fileOpts] | Object | Optional file related meta-data | @@ -371,7 +381,11 @@ Send audio Send Document **Kind**: instance method of [TelegramBot](#TelegramBot) -**See**: https://core.telegram.org/bots/api#sendDocument +**See** + +- https://core.telegram.org/bots/api#sendDocument +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | @@ -396,46 +410,61 @@ Send .webp stickers. -### telegramBot.sendVideo(chatId, video, [options]) ⇒ Promise +### telegramBot.sendVideo(chatId, video, [options], [fileOpts]) ⇒ Promise Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). **Kind**: instance method of [TelegramBot](#TelegramBot) -**See**: https://core.telegram.org/bots/api#sendvideo +**See** + +- https://core.telegram.org/bots/api#sendvideo +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | Unique identifier for the message recipient | | video | String | stream.Stream | Buffer | A file path or Stream. Can also be a `file_id` previously uploaded. | | [options] | Object | Additional Telegram query options | +| [fileOpts] | Object | Optional file related meta-data | -### telegramBot.sendVideoNote(chatId, videoNote, [options]) ⇒ Promise +### telegramBot.sendVideoNote(chatId, videoNote, [options], [fileOpts]) ⇒ Promise Use this method to send rounded square videos of upto 1 minute long. **Kind**: instance method of [TelegramBot](#TelegramBot) **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://core.telegram.org/bots/api#sendvideonote +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | Unique identifier for the message recipient | | videoNote | String | stream.Stream | Buffer | A file path or Stream. Can also be a `file_id` previously uploaded. | | [options] | Object | Additional Telegram query options | +| [fileOpts] | Object | Optional file related meta-data | -### telegramBot.sendVoice(chatId, voice, [options]) ⇒ Promise +### telegramBot.sendVoice(chatId, voice, [options], [fileOpts]) ⇒ Promise Send voice **Kind**: instance method of [TelegramBot](#TelegramBot) -**See**: https://core.telegram.org/bots/api#sendvoice +**See** + +- https://core.telegram.org/bots/api#sendvoice +- https://github.com/yagop/node-telegram-bot-api/blob/master/doc/usage.md#sending-files + | Param | Type | Description | | --- | --- | --- | | chatId | Number | String | Unique identifier for the message recipient | | voice | String | stream.Stream | Buffer | A file path, Stream or Buffer. Can also be a `file_id` previously uploaded. | | [options] | Object | Additional Telegram query options | +| [fileOpts] | Object | Optional file related meta-data | diff --git a/doc/usage.md b/doc/usage.md index 3a17700..e3499d3 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -115,6 +115,45 @@ const url = 'https://telegram.org/img/t_logo.png'; bot.sendPhoto(chatId, url); ``` +If you wish to explicitly specify the filename or +[MIME type](http://en.wikipedia.org/wiki/Internet_media_type), +you may pass the an additional argument as file options, like so: + +```js +const fileOpts = { + // Explicitly specify the file name. + filename: 'customfilename', + // Explicitly specify the MIME type. + contentType: 'audio/mpeg' +}; +bot.sendAudio(chatId, data, {}, fileOpts); +``` + + +### File Options (metadata) + +When sending files, the library automatically resolves +the `filename` and `contentType` properties. +**For now, this has to be manually activated using environment +variable `NTBA_FIX_350`.** + +In order of highest-to-lowest precedence in searching for +a value, when resolving the `filename`: + +1. Is `fileOptions.filename` explictly defined? +1. Does `Stream#path` exist? +1. Is `filepath` provided? +1. Default to `"filename"` + +And the `contentType`: + +1. Is `fileOptions.contentType` explictly-defined? +1. Does `Stream#path` exist? +1. Try detecting file-type from the `Buffer` +1. Is `filepath` provided? +1. Is `fileOptions.filename` explicitly defined? +1. Default to `"application/octet-stream` + ### Performance Issue diff --git a/src/telegram.js b/src/telegram.js index b69fa59..3d011bd 100644 --- a/src/telegram.js +++ b/src/telegram.js @@ -284,6 +284,9 @@ class TelegramBot extends EventEmitter { * Format data to be uploaded; handles file paths, streams and buffers * @param {String} type * @param {String|stream.Stream|Buffer} data + * @param {Object} fileOpts File options + * @param {String} [fileOpts.filename] File name + * @param {String} [fileOpts.contentType] Content type (i.e. MIME) * @return {Array} formatted * @return {Object} formatted[0] formData * @return {String} formatted[1] fileId @@ -291,55 +294,68 @@ class TelegramBot extends EventEmitter { * @see https://npmjs.com/package/file-type * @private */ - _formatSendData(type, data) { - let formData; - let fileName; - let fileId; + _formatSendData(type, data, fileOpts = {}) { + let filedata = data; + let filename = fileOpts.filename; + let contentType = fileOpts.contentType; + if (data instanceof stream.Stream) { - // Will be 'null' if could not be parsed. Default to 'filename'. - // For example, 'data.path' === '/?id=123' from 'request("https://example.com/?id=123")' - fileName = URL.parse(path.basename(data.path.toString())).pathname || 'filename'; - formData = {}; - formData[type] = { - value: data, - options: { - filename: qs.unescape(fileName), - contentType: mime.lookup(fileName) - } - }; - } else if (Buffer.isBuffer(data)) { - const filetype = fileType(data); - if (!filetype) { - throw new errors.FatalError('Unsupported Buffer file type'); + 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())); + filename = qs.unescape(url.pathname); } - formData = {}; - formData[type] = { - value: data, - options: { - filename: `data.${filetype.ext}`, - contentType: filetype.mime + } else if (Buffer.isBuffer(data)) { + if (!filename && !process.env.NTBA_FIX_350) { + deprecate('Buffers will have their filenames default to "filename" instead of "data".'); + 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.'); + throw new errors.FatalError('Unsupported Buffer file-type'); } - }; - } else if (!this.options.filepath) { - /** - * When the constructor option 'filepath' is set to - * 'false', we do not support passing file-paths. - */ - fileId = data; - } else if (fs.existsSync(data)) { - fileName = path.basename(data); - formData = {}; - formData[type] = { - value: fs.createReadStream(data), - options: { - filename: fileName, - contentType: mime.lookup(fileName) + } + } 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 { - fileId = data; + return [null, data]; } - return [formData, fileId]; + + 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".'); + } + + // TODO: Add missing file extension. + + return [{ + [type]: { + value: filedata, + options: { + filename, + contentType, + }, + }, + }, null]; } /** @@ -685,16 +701,18 @@ class TelegramBot extends EventEmitter { * @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} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}) { + sendPhoto(chatId, photo, options = {}, fileOpts = {}) { const opts = { qs: options, }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('photo', photo); + const sendData = this._formatSendData('photo', photo, fileOpts); opts.formData = sendData[0]; opts.qs.photo = sendData[1]; } catch (ex) { @@ -709,16 +727,18 @@ class TelegramBot extends EventEmitter { * @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} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}) { + sendAudio(chatId, audio, options = {}, fileOpts = {}) { const opts = { qs: options }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('audio', audio); + const sendData = this._formatSendData('audio', audio, fileOpts); opts.formData = sendData[0]; opts.qs.audio = sendData[1]; } catch (ex) { @@ -736,6 +756,7 @@ class TelegramBot extends EventEmitter { * @param {Object} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}, fileOpts = {}) { const opts = { @@ -743,15 +764,12 @@ class TelegramBot extends EventEmitter { }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('document', doc); + const sendData = this._formatSendData('document', doc, fileOpts); opts.formData = sendData[0]; opts.qs.document = sendData[1]; } catch (ex) { return Promise.reject(ex); } - if (opts.formData && Object.keys(fileOpts).length) { - opts.formData.document.options = fileOpts; - } return this._request('sendDocument', opts); } @@ -785,16 +803,18 @@ class TelegramBot extends EventEmitter { * @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} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}) { + sendVideo(chatId, video, options = {}, fileOpts = {}) { const opts = { qs: options }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('video', video); + const sendData = this._formatSendData('video', video, fileOpts); opts.formData = sendData[0]; opts.qs.video = sendData[1]; } catch (ex) { @@ -809,17 +829,19 @@ class TelegramBot extends EventEmitter { * @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} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}) { + sendVideoNote(chatId, videoNote, options = {}, fileOpts = {}) { const opts = { qs: options }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('video_note', videoNote); + const sendData = this._formatSendData('video_note', videoNote, fileOpts); opts.formData = sendData[0]; opts.qs.video_note = sendData[1]; } catch (ex) { @@ -834,16 +856,18 @@ class TelegramBot extends EventEmitter { * @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} [fileOpts] Optional file related meta-data * @return {Promise} * @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 = {}) { + sendVoice(chatId, voice, options = {}, fileOpts = {}) { const opts = { qs: options }; opts.qs.chat_id = chatId; try { - const sendData = this._formatSendData('voice', voice); + const sendData = this._formatSendData('voice', voice, fileOpts); opts.formData = sendData[0]; opts.qs.voice = sendData[1]; } catch (ex) { diff --git a/test/telegram.js b/test/telegram.js index b300ab2..c0501cf 100644 --- a/test/telegram.js +++ b/test/telegram.js @@ -630,13 +630,6 @@ describe('TelegramBot', function telegramSuite() { assert.ok(is.object(resp.document)); }); }); - it('should send a document with custom file options', function test() { - const document = fs.createReadStream(`${__dirname}/data/photo.gif`); - const fileOpts = { filename: 'customfilename.gif' }; - return bot.sendDocument(USERID, document, {}, fileOpts).then(resp => { - assert.equal(resp.document.file_name, fileOpts.filename); - }); - }); }); describe('#sendSticker', function sendStickerSuite() { diff --git a/test/test.sendfile.js b/test/test.sendfile.js new file mode 100644 index 0000000..864d900 --- /dev/null +++ b/test/test.sendfile.js @@ -0,0 +1,109 @@ +const assert = require('assert'); +const fs = require('fs'); +const path = require('path'); +const TelegramBot = require('..'); + +const paths = { + audio: path.join(__dirname, "data/audio.mp3"), +}; + + +// TODO:Enable all other tests +describe.only('sending files', function sendfileSuite() { + const bot = new TelegramBot("token"); + + before(function beforeSuite() { + process.env.NTBA_FIX_350 = 1; + }); + after(function afterSuite() { + delete process.env.NTBA_FIX_350; + }); + + describe('using fileOptions', function sendfileOptionsSuite() { + const type = 'file'; + const stream = fs.createReadStream(paths.audio); + const nonPathStream = fs.createReadStream(paths.audio); + const buffer = fs.readFileSync(paths.audio); + const nonDetectableBuffer = fs.readFileSync(__filename); + const filepath = paths.audio; + const fileId = 'fileId'; + const files = [stream, nonPathStream, buffer, nonDetectableBuffer, filepath]; + + delete nonPathStream.path; + + describe('filename', function filenameSuite() { + it('(1) fileOptions.filename', function test() { + const filename = 'custom-filename'; + files.forEach((file) => { + const [{ [type]: data }] = bot._formatSendData(type, file, { filename }); + assert.equal(data.options.filename, filename); + }); + }); + + it('(2) Stream#path', function test() { + if (!stream.path) { + return this.skip('Stream#path unsupported'); + } + const [{ [type]: data }] = bot._formatSendData(type, stream); + assert.equal(data.options.filename, path.basename(paths.audio)); + }); + + it('(3) filepath', function test() { + const [{ [type]: data }] = bot._formatSendData(type, filepath); + assert.equal(data.options.filename, path.basename(paths.audio)); + }); + + it('(4) final default', function test() { + [nonPathStream, buffer, nonDetectableBuffer].forEach((file) => { + const [{ [type]: data }] = bot._formatSendData(type, file); + assert.equal(data.options.filename, 'filename'); + }); + }); + }); + + describe('contentType', function contentTypeSuite() { + it('(1) fileOpts.contentType', function test() { + const contentType = 'application/custom-type'; + files.forEach((file) => { + const [{ [type]: data }] = bot._formatSendData(type, file, { contentType }); + assert.equal(data.options.contentType, contentType); + }); + }); + + it('(2) Stream#path', function test() { + if (!stream.path) { + return this.skip('Stream#path unsupported'); + } + const [{ [type]: data }] = bot._formatSendData(type, stream); + assert.equal(data.options.contentType, 'audio/mpeg'); + }); + + it('(3) Buffer file-type', function test() { + const [{ [type]: data }] = bot._formatSendData(type, buffer); + assert.equal(data.options.contentType, 'audio/mpeg'); + }); + + it('(4) filepath', function test() { + const [{ [type]: data }] = bot._formatSendData(type, filepath); + assert.equal(data.options.contentType, 'audio/mpeg'); + }); + + it('(5) fileOptions.filename', function test() { + [nonPathStream, nonDetectableBuffer].forEach((file) => { + const [{ [type]: data }] = bot._formatSendData(type, file, { + filename: 'image.gif', + }); + assert.equal(data.options.contentType, 'image/gif'); + }); + }); + + it('(6) Final default', function test() { + [nonPathStream, nonDetectableBuffer].forEach((file) => { + const [{ [type]: data }] = bot._formatSendData(type, file); + assert.equal(data.options.contentType, 'application/octet-stream'); + }); + }); + }); + + }); +});