diff --git a/lib/interactive.js b/lib/interactive.js index 1493bcb..669faa1 100644 --- a/lib/interactive.js +++ b/lib/interactive.js @@ -14,6 +14,7 @@ const events = require('events'); const path = require('path'); const notifier = require('node-notifier'); const readline = require('readline'); +const { existsSync } = require('fs'); let messenger; @@ -42,9 +43,9 @@ const rlInterface = readline.createInterface({ const colorList = [ safeColors.green, + safeColors.cyan, safeColors.red, safeColors.blue, - safeColors.cyan, safeColors.magenta, safeColors.yellow ]; @@ -59,27 +60,26 @@ function getDisplayName(author) { if(!author) author = { name: 'unknown' }; - if(currentConversation && currentConversation.isGroup && Settings.properties.groupColors) { - colorPosition = author.name.length % colorList.length; + if(currentConversation && Settings.properties.groupColors) { + if (currentConversation.isGroup) + colorPosition = author.name.length % colorList.length; + else if (author.id === messenger.userId) + colorPosition = 1; } - - let displayName; + + let displayName; if (!Settings.properties.useCustomNicknames) { displayName = author.name; } else displayName = author.custom_nickname || author.name; - if (author.id === messenger.userId) - return displayName; - else { - return colorList[colorPosition](displayName); - } + return colorList[colorPosition](displayName); } function renderMessage(author, message) { let msg = `${getDisplayName(author)}: `; - + if (Settings.properties.showTimestamps) { - + const timeDifference = Date.now() - message.timestamp; const daysAgo = Math.round(timeDifference / msInADay); @@ -152,11 +152,11 @@ function renderMessage(author, message) { const a = message.attachment; let uri; if (a.preview && a.preview.uri) { - uri = a.preview.uri; + uri = a.preview.uri; } else if (a.preview_image && a.preview_image.uri) { - uri = a.preview_image.uri; + uri = a.preview_image.uri; } - + if (uri) { atts[attsNo] = uri; const x = `${attsNo}`; @@ -420,17 +420,18 @@ InteractiveCli.prototype.handleCommands = function(command) { case '/h': case '/?': case '/help': - console.log('/b /back /menu .... Get back to conversation selection'.cyan); - console.log('/q /exit /quit .... Quit the application'.cyan); - console.log('/logout ........... Exit and flush credentials'.cyan); - console.log('/s /switch [#] .... Quick switch to conversation number #'.cyan); - console.log('/search [query] ... Search your friends to chat'.cyan); - console.log('/v /view [#] ...... View the attachment by the number given after the type'.cyan); - console.log('/r /refresh ....... Refresh the current converation'.cyan); - console.log('/timestamp ........ Toggle timestamp for messages'.cyan); - console.log('/linelimit [#] .... Set max number of messages to display in a conversation'.cyan); - console.log('/nolimit .......... Unset a line limit, display all available messages'.cyan); - console.log('/help ............. Print this message'.cyan); + console.log('/b /back /menu ...... Get back to conversation selection'.cyan); + console.log('/q /exit /quit ...... Quit the application'.cyan); + console.log('/logout ............. Exit and flush credentials'.cyan); + console.log('/s /switch [#] ...... Quick switch to conversation number #'.cyan); + console.log('/a /attach [path] ... Attach file from path (if has space, must wrap with "")'.cyan); + console.log('/search [query] ..... Search your friends to chat'.cyan); + console.log('/v /view [#] ........ View the attachment by the number given after the type'.cyan); + console.log('/r /refresh ......... Refresh the current converation'.cyan); + console.log('/timestamp .......... Toggle timestamp for messages'.cyan); + console.log('/linelimit [#] ...... Set max number of messages to display in a conversation'.cyan); + console.log('/nolimit ............ Unset a line limit, display all available messages'.cyan); + console.log('/help ............... Print this message'.cyan); rlInterface.prompt(true); break; @@ -486,6 +487,25 @@ InteractiveCli.prototype.handleCommands = function(command) { interactive.handleCommands("/refresh"); } break; + case '/a': + case '/attach': + rlInterface.prompt(true); + let filePath = options[1]; + if (action === 1) { + let filePath = options[1]; + if (filePath) { + if (existsSync(filePath)) { + emitter.emit('sendAttachment', filePath, recipientId); + } else { + console.log("File not found!".cyan); + } + } else { + console.log("Missing file to be attached!".cyan); + } + } else { + console.log("This command is only available in a conversation!".cyan); + } + break; default: console.log('Unknown command. Type /help for commands.'.cyan); @@ -552,8 +572,9 @@ InteractiveCli.prototype.run = function(){ emitter.on('sendMessage', listeners.sendMessageListener.bind(listeners)); emitter.on('getThreadId', listeners.getThreadIdListener.bind(listeners)); emitter.on('startSearch', listeners.searchListener.bind(listeners)); - - console.log("Fetching conversations...".cyan); + emitter.on('sendAttachment', listeners.sendAttachmentListener.bind(listeners)); + + console.log("Fetching conversations...".cyan); messenger.getFriends((err, friends) => { if (err) { console.log(`An error occured initially fetching friends list: ${err}`); @@ -562,7 +583,7 @@ InteractiveCli.prototype.run = function(){ } const entry = { - id: userId, + id: userId, firstName: "Me", name: "Me", vanity: "unknown", @@ -577,10 +598,10 @@ InteractiveCli.prototype.run = function(){ rlInterface.prompt(true); }); - // Set up the line reader + // Set up the line reader rlInterface.on("line", interactive.handler); rlInterface.on("close", interactive.exit); - rlInterface.prompt(true); + rlInterface.prompt(true); }); }); }; @@ -599,7 +620,7 @@ InteractiveCli.prototype.handleExit = function(logout){ InteractiveCli.prototype.exit = function(){ console.log('Thanks for using fb-messenger-cli'.cyan); console.log('Bye!'.cyan); - + process.exit(0); }; diff --git a/lib/listeners.js b/lib/listeners.js index c18fc58..1d9190a 100644 --- a/lib/listeners.js +++ b/lib/listeners.js @@ -94,6 +94,20 @@ class Listeners { } } + sendAttachmentListener(filePath, recipientId) { + const errorHandler = (err) => { + if (err) { + console.log('Attachment did not send properly'); + } + } + if (this.messenger.users[recipientId]) { + this.messenger.sendAttachment(this.messenger.users[recipientId].vanity, recipientId, filePath, errorHandler); + } else { + // This is a group and not a single user + this.messenger.sendGroupAttachment(recipientId, filePath, errorHandler); + } + } + searchListener(searchStr, callback) { const parts = searchStr.split(' '); diff --git a/lib/messenger.js b/lib/messenger.js index ddf947b..e457c01 100644 --- a/lib/messenger.js +++ b/lib/messenger.js @@ -8,6 +8,7 @@ const request = require('request'); // For making HTTP requests const Settings = require('./settings'); +const fs = require('fs'); function getThreadName(thread, participant) { if (!Settings.properties.useCustomNicknames) { @@ -152,6 +153,128 @@ class Messenger { }); } + // Send an attachment in a thread + // recipient: Url name of recipient, also called vanity (eg: alexandre.rose) + // recipientId : Facebook numeric id of recipient. Can be a person or a thread + // filePath : path of the file + // callback(err) does not get any data + sendAttachment(recipient, recipientId, filePath, callback) { + const recipientUrl = `${this.baseUrl }/t/${ recipient}`; // Your recipient; + const utcTimestamp = new Date().getTime(); + const localTime = new Date().toLocaleTimeString().replace(/\s+/g, '').toLowerCase(); + const messageId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + + // header is identical in both the upload endpoint and message-send endpoint + const headers = { + 'origin': 'https://www.messenger.com', + 'accept-encoding': 'gzip, deflate', + 'x-msgr-region': 'ATN', + 'accept-language': 'en-US,en;q=0.8', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36', + 'content-type': 'application/x-www-form-urlencoded', + 'accept': '*/*', + 'referer': recipientUrl, + 'cookie': this.cookie, + 'authority': 'www.messenger.com' + }; + + request.post("https://upload.messenger.com/ajax/mercury/upload.php", { + headers: headers, + formData: { + // the ones commented out were parameters I wasn't sure whether + // they were needed but are confirmed unneeded now. + '__a': '1', + // '__be': '0', + // '__dyn': '7AzkXh8Z398jgDxKy1l0BwRyaF3oyfJLFwgoqwWhEoyUnwgU9GGEcVovkwy3eE99XDG4UiwExW14DwPxSFEW2O9xicG4EnwkUC9z8Kew', + // '__pc': 'EXP1:messengerdotcom_pkg', + // '__req': '2q', + // '__rev': '2335431', + '__user': this.userId, + // 'dpr': '1.5', + 'fb_dtsg': this.fbdtsg, + // 'jazoest': '21948', + 'upload': fs.createReadStream(filePath) + }, + gzip: true, + }, (err, res, body) => { + // issue at network level + if (err) { + console.log("Attachment upload failed"); + return; + } + let bodyJson = JSON.parse(body.replace('for (;;);', '')); + + // if Facebook denies the file + if (bodyJson.error) { + // error details: bodyJson.errorSummary, bodyJson.errorDescription + console.log("Attachment denied by Facebook"); + return; + } + + let formData = { + // everything's identical to the formData in sendMessage except: + // 1. no "body" + // 2. "has_attachment" set to true + // 3. no "html_body" + // 4. the id to the attachment is added in the if/else block below + 'action_type': 'ma-type:user-generated-message', + 'author': `fbid:${this.userId}`, + 'timestamp': utcTimestamp, + 'timestamp_absolute': 'Today', + 'timestamp_relative': localTime, + 'timestamp_time_passed': '0', + 'is_unread': 'false', + 'is_forward': 'false', + 'is_filtered_content': 'false', + 'is_filtered_content_bh': 'false', + 'is_filtered_content_account': 'false', + 'is_filtered_content_quasar': 'false', + 'is_filtered_content_invalid_app': 'false', + 'is_spoof_warning': 'false', + 'source': 'source:messenger:web', + 'has_attachment': 'true', + 'specific_to_list][0]': `fbid:${recipientId}`, + 'specific_to_list[1]': `fbid:${this.userId}`, + 'status': '0', + 'offline_threading_id': messageId, + 'message_id': messageId, + 'ephemeral_ttl_mode': '0', + 'manual_retry_cnt': '0', + 'other_user_fbid': recipientId, + 'client': 'mercury', + '__user': this.userId, + '__a': '1', + '__req': '2q', + '__be': '0', + '__pc': 'EXP1:messengerdotcom_pkg', + 'fb_dtsg': this.fbdtsg, + 'ttstamp': '265817073691196867855211811758658172458277511215256110114', + '__rev': '2335431' + } + + let metadata = bodyJson.payload.metadata[0]; + if (metadata.image_id) { + formData['image_ids[0]'] = metadata.image_id; + } else if (metadata.video_id) { + formData['video_ids[0]'] = metadata.video_id; + } else if (metadata.audio_id) { + formData['audio_ids[0]'] = metadata.audio_id; + } else { + formData['file_ids[0]'] = metadata.file_id; + } + + request.post("https://www.messenger.com/messaging/send/?dpr=1", { + headers: headers, + formData: formData + }, (err) => { + if (err) { + callback(err); + } + callback(); + }); + }); + } + sendGroupMessage(recipientId, body, callback) { const recipientUrl = `${this.baseUrl }/t/${ recipientId}`; // Your recipient; const utcTimestamp = new Date().getTime(); @@ -198,6 +321,112 @@ class Messenger { }); } + // Send an attachment in a Group thread + // recipientId : Facebook numeric id of recipient. Can be a person or a thread + // filePath : path of the file + // callback(err) does not get any data + sendGroupAttachment(recipientId, filePath, callback) { + const recipientUrl = `${this.baseUrl }/t/${ recipientId}`; // Your recipient; + const utcTimestamp = new Date().getTime(); + const localTime = new Date().toLocaleTimeString().replace(/\s+/g, '').toLowerCase(); + const messageId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); + + // header is identical in both the upload endpoint and message-send endpoint + const headers = { + 'origin': 'https://www.messenger.com', + 'accept-encoding': 'gzip, deflate', + 'x-msgr-region': 'ATN', + 'accept-language': 'en-US,en;q=0.8', + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36', + 'content-type': 'application/x-www-form-urlencoded', + 'accept': '*/*', + 'referer': recipientUrl, + 'cookie': this.cookie, + 'authority': 'www.messenger.com' + }; + + request.post("https://upload.messenger.com/ajax/mercury/upload.php", { + headers: headers, + formData: { + // the ones commented out were parameters I wasn't sure whether + // they were needed but are confirmed unneeded now. + '__a': '1', + // '__be': '0', + // '__dyn': '7AzkXh8Z398jgDxKy1l0BwRyaF3oyfJLFwgoqwWhEoyUnwgU9GGEcVovkwy3eE99XDG4UiwExW14DwPxSFEW2O9xicG4EnwkUC9z8Kew', + // '__pc': 'EXP1:messengerdotcom_pkg', + // '__req': '2q', + // '__rev': '2335431', + '__user': this.userId, + // 'dpr': '1.5', + 'fb_dtsg': this.fbdtsg, + // 'jazoest': '21948', + 'upload': fs.createReadStream(filePath) + }, + gzip: true, + }, (err, res, body) => { + // issue at network level + if (err) { + console.log("Attachment upload failed"); + return; + } + let bodyJson = JSON.parse(body.replace('for (;;);', '')); + + // if Facebook denies the file + if (bodyJson.error) { + // error details: bodyJson.errorSummary, bodyJson.errorDescription + console.log("Attachment denied by Facebook"); + return; + } + + let formData = { + // everything's identical to the formData in sendMessage except: + // 1. no "body" + // 2. "has_attachment" set to true + // 3. the id to the attachment is added in the if/else block below + 'action_type': 'ma-type:user-generated-message', + 'author': `fbid:${this.userId}`, + 'timestamp': utcTimestamp, + 'source': 'source:messenger:web', + 'has_attachment': 'true', + 'specific_to_list][0]': `fbid:${recipientId}`, + 'specific_to_list[1]': `fbid:${this.userId}`, + 'status': '0', + 'offline_threading_id': messageId, + 'message_id': messageId, + 'thread_fbid': recipientId, + '__user': this.userId, + '__a': '1', + '__req': '2q', + '__be': '0', + '__pc': 'EXP1:messengerdotcom_pkg', + 'fb_dtsg': this.fbdtsg, + 'ttstamp': '265817073691196867855211811758658172458277511215256110114', + '__rev': '2335431' + } + + let metadata = bodyJson.payload.metadata[0]; + if (metadata.image_id) { + formData['image_ids[0]'] = metadata.image_id; + } else if (metadata.video_id) { + formData['video_ids[0]'] = metadata.video_id; + } else if (metadata.audio_id) { + formData['audio_ids[0]'] = metadata.audio_id; + } else { + formData['file_ids[0]'] = metadata.file_id; + } + + request.post("https://www.messenger.com/messaging/send/?dpr=1", { + headers: headers, + formData: formData + }, (err) => { + if (err) { + callback(err); + } + callback(); + }); + }); + } + getMessages(recipient, recipientId, count, callback) { const recipientUrl = `${this.baseUrl }/t/${ recipient}`; let offSetString, limitString;