UNPKG

nv-fca

Version:

A node.js package for automating Facebook Messenger bot, and is one of the most advanced next-generation Facebook Chat API (FCA) by @NethWs3Dev (Kenneth Aceberos)

1,436 lines (1,301 loc) 45.7 kB
/* eslint-disable no-prototype-builtins */ "use strict"; const chalk = require("chalk"); const gradient = require("gradient-string"); const echaceb = gradient(["#0061ff", "#681297"]); const ws = echaceb("ws3-fca"); const getRandom = arr => arr[Math.floor(Math.random() * arr.length)]; const defaultUserAgent = "facebookexternalhit/1.1"; const windowsUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; function randomUserAgent() { const platform = { platform: ['Windows NT 10.0; Win64; x64', 'Macintosh; Intel Mac OS X 14.7; rv:132.0'], browsers: { chrome: ['122.0.0.0', '121.0.0.0'], firefox: ['123.0', '122.0'], edge: ['122.0.2365.92'] } }; const browserName = getRandom(Object.keys(platform.browsers)); const version = getRandom(platform.browsers[browserName]); const plat = getRandom(platform.platform); const userAgentArray = [ defaultUserAgent, windowsUserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:45.0) Gecko/20100101 Firefox/45.0", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.3", ]; const ua = getRandom([ browserName === 'firefox' ? `Mozilla/5.0 (${plat}) Gecko/20100101 Firefox/${version}` : `Mozilla/5.0 (${plat}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`, getRandom(userAgentArray) ]); return ua; } const headers = { "content-type": "application/x-www-form-urlencoded", "referer": "https://www.facebook.com/", "origin": "https://www.facebook.com", "connection": "keep-alive", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-User": "?1" }; let request = require("request").defaults({ jar: true }); const stream = require("stream"); const querystring = require("querystring"); const url = require("url"); function setProxy(proxy) { request = require("request").defaults({ jar: true, ...(proxy && { proxy }) }); return; } function getHeaders(url, options, ctx, customHeader) { const headers1 = { "host": new URL(url).hostname, ...headers, "User-Agent": customHeader?.customUserAgent ?? options?.userAgent ?? defaultUserAgent } /*if (headers1["User-Agent"]) { delete headers1["User-Agent"]; headers1["User-Agent"] = customHeader?.customUserAgent ?? options?.userAgent ?? defaultUserAgent; */ if (ctx && ctx.region) headers1["X-MSGR-Region"] = ctx.region; if (customHeader) { Object.assign(headers1, customHeader); if (customHeader.noRef) delete headers1.referer; } return headers1; } function isReadableStream(obj) { return obj instanceof stream.Stream && typeof obj._read == "function" && getType(obj._readableState) == "Object"; } function cleanGet(url) { let callback; var returnPromise = new Promise(function(resolve, reject) { callback = (error, res) => error ? reject(error) : resolve(res); }); request.get(url, { timeout: 60000 }, callback); return returnPromise; } function get(url, jar, qs, options, ctx, customHeader) { let callback; var returnPromise = new Promise(function (resolve, reject) { callback = (error, res) => error ? reject(error) : resolve(res); }); if (getType(qs) == "Object") for (let prop in qs) { if (getType(qs[prop]) == 'Object') qs[prop] = JSON.stringify(qs[prop]); } var op = { headers: getHeaders(url, options, ctx, customHeader), timeout: 60000, qs, jar, gzip: true } request.get(url, op, callback); return returnPromise; } function post(url, jar, form, options, ctx, customHeader) { let callback; var returnPromise = new Promise(function (resolve, reject) { callback = (error, res) => error ? reject(error) : resolve(res); }); var op = { headers: getHeaders(url, options, ctx, customHeader), timeout: 60000, form, jar, gzip: true } request.post(url, op, callback); return returnPromise; } function postFormData(url, jar, form, qs, options, ctx) { let callback; var returnPromise = new Promise(function (resolve, reject) { callback = (error, res) => error ? reject(error) : resolve(res); }); if (getType(qs) == "Object") for (let prop in qs) { if (getType(qs[prop]) == 'Object') qs[prop] = JSON.stringify(qs[prop]); } var op = { headers: getHeaders(url, options, ctx, { 'content-type': 'multipart/form-data' }), timeout: 60000, formData: form, qs, jar, gzip: true } request.post(url, op, callback); return returnPromise; } function padZeros(val, len) { val = String(val); len = len || 2; while (val.length < len) val = "0" + val; return val; } function generateThreadingID(clientID) { const k = Date.now(); const l = Math.floor(Math.random() * 4294967295); const m = clientID; return "<" + k + ":" + l + "-" + m + "@mail.projektitan.com>"; } function binaryToDecimal(data) { let ret = ""; while (data !== "0") { let end = 0; let fullName = ""; let i = 0; for (; i < data.length; i++) { end = 2 * end + parseInt(data[i], 10); if (end >= 10) { fullName += "1"; end -= 10; } else { fullName += "0"; } } ret = end.toString() + ret; data = fullName.slice(fullName.indexOf("1")); } return ret; } function generateOfflineThreadingID() { const ret = Date.now(); const value = Math.floor(Math.random() * 4294967295); const str = ("0000000000000000000000" + value.toString(2)).slice(-22); const msgs = ret.toString(2) + str; return binaryToDecimal(msgs); } let h; const i = {}; const j = { _: "%", A: "%2", B: "000", C: "%7d", D: "%7b%22", E: "%2c%22", F: "%22%3a", G: "%2c%22ut%22%3a1", H: "%2c%22bls%22%3a", I: "%2c%22n%22%3a%22%", J: "%22%3a%7b%22i%22%3a0%7d", K: "%2c%22pt%22%3a0%2c%22vis%22%3a", L: "%2c%22ch%22%3a%7b%22h%22%3a%22", M: "%7b%22v%22%3a2%2c%22time%22%3a1", N: ".channel%22%2c%22sub%22%3a%5b", O: "%2c%22sb%22%3a1%2c%22t%22%3a%5b", P: "%2c%22ud%22%3a100%2c%22lc%22%3a0", Q: "%5d%2c%22f%22%3anull%2c%22uct%22%3a", R: ".channel%22%2c%22sub%22%3a%5b1%5d", S: "%22%2c%22m%22%3a0%7d%2c%7b%22i%22%3a", T: "%2c%22blc%22%3a1%2c%22snd%22%3a1%2c%22ct%22%3a", U: "%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a", V: "%2c%22blc%22%3a0%2c%22snd%22%3a0%2c%22ct%22%3a", W: "%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a", X: "%2c%22ri%22%3a0%7d%2c%22state%22%3a%7b%22p%22%3a0%2c%22ut%22%3a1", Y: "%2c%22pt%22%3a0%2c%22vis%22%3a1%2c%22bls%22%3a0%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a", Z: "%2c%22sb%22%3a1%2c%22t%22%3a%5b%5d%2c%22f%22%3anull%2c%22uct%22%3a0%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a" }; (function() { const l = []; for (const m in j) { i[j[m]] = m; l.push(j[m]); } l.reverse(); h = new RegExp(l.join("|"), "g"); })(); function presenceEncode(str) { return encodeURIComponent(str) .replace(/([_A-Z])|%../g, function(m, n) { return n ? "%" + n.charCodeAt(0).toString(16) : m; }) .toLowerCase() .replace(h, function(m) { return i[m]; }); } // eslint-disable-next-line no-unused-vars function presenceDecode(str) { return decodeURIComponent( str.replace(/[_A-Z]/g, function(m) { return j[m]; }) ); } function generatePresence(userID) { const time = Date.now(); return ( "E" + presenceEncode( JSON.stringify({ v: 3, time: parseInt(time / 1000, 10), user: userID, state: { ut: 0, t2: [], lm2: null, uct2: time, tr: null, tw: Math.floor(Math.random() * 4294967295) + 1, at: time }, ch: { ["p_" + userID]: 0 } }) ) ); } function generateAccessiblityCookie() { const time = Date.now(); return encodeURIComponent( JSON.stringify({ sr: 0, "sr-ts": time, jk: 0, "jk-ts": time, kb: 0, "kb-ts": time, hcm: 0, "hcm-ts": time }) ); } function getGUID() { /** @type {number} */ let sectionLength = Date.now(); /** @type {string} */ const id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { /** @type {number} */ const r = Math.floor((sectionLength + Math.random() * 16) % 16); /** @type {number} */ sectionLength = Math.floor(sectionLength / 16); /** @type {string} */ const _guid = (c == "x" ? r : (r & 7) | 8).toString(16); return _guid; }); return id; } function getExtension(original_extension, fullFileName = "") { if (original_extension) { return original_extension; } else { const extension = fullFileName.split(".").pop(); if (extension === fullFileName) { return ""; } else { return extension; } } } function _formatAttachment(attachment1, attachment2) { // TODO: THIS IS REALLY BAD // This is an attempt at fixing Facebook's inconsistencies. Sometimes they give us // two attachment objects, but sometimes only one. They each contain part of the // data that you'd want so we merge them for convenience. // Instead of having a bunch of if statements guarding every access to image_data, // we set it to empty object and use the fact that it'll return undefined. const fullFileName = attachment1.filename; const fileSize = Number(attachment1.fileSize || 0); const durationVideo = attachment1.genericMetadata ? Number(attachment1.genericMetadata.videoLength) : undefined; const durationAudio = attachment1.genericMetadata ? Number(attachment1.genericMetadata.duration) : undefined; const mimeType = attachment1.mimeType; attachment2 = attachment2 || { id: "", image_data: {} }; attachment1 = attachment1.mercury || attachment1; let blob = attachment1.blob_attachment || attachment1.sticker_attachment; let type = blob && blob.__typename ? blob.__typename : attachment1.attach_type; if (!type && attachment1.sticker_attachment) { type = "StickerAttachment"; blob = attachment1.sticker_attachment; } else if (!type && attachment1.extensible_attachment) { if ( attachment1.extensible_attachment.story_attachment && attachment1.extensible_attachment.story_attachment.target && attachment1.extensible_attachment.story_attachment.target.__typename && attachment1.extensible_attachment.story_attachment.target.__typename === "MessageLocation" ) { type = "MessageLocation"; } else { type = "ExtensibleAttachment"; } blob = attachment1.extensible_attachment; } // TODO: Determine whether "sticker", "photo", "file" etc are still used // KEEP IN SYNC WITH getThreadHistory switch (type) { case "sticker": return { type: "sticker", ID: attachment1.metadata.stickerID.toString(), url: attachment1.url, packID: attachment1.metadata.packID.toString(), spriteUrl: attachment1.metadata.spriteURI, spriteUrl2x: attachment1.metadata.spriteURI2x, width: attachment1.metadata.width, height: attachment1.metadata.height, caption: attachment2.caption, description: attachment2.description, frameCount: attachment1.metadata.frameCount, frameRate: attachment1.metadata.frameRate, framesPerRow: attachment1.metadata.framesPerRow, framesPerCol: attachment1.metadata.framesPerCol, stickerID: attachment1.metadata.stickerID.toString(), // @Legacy spriteURI: attachment1.metadata.spriteURI, // @Legacy spriteURI2x: attachment1.metadata.spriteURI2x // @Legacy }; case "file": return { type: "file", ID: attachment2.id.toString(), fullFileName: fullFileName, filename: attachment1.name, fileSize: fileSize, original_extension: getExtension(attachment1.original_extension, fullFileName), mimeType: mimeType, url: attachment1.url, isMalicious: attachment2.is_malicious, contentType: attachment2.mime_type, name: attachment1.name // @Legacy }; case "photo": return { type: "photo", ID: attachment1.metadata.fbid.toString(), filename: attachment1.fileName, fullFileName: fullFileName, fileSize: fileSize, original_extension: getExtension(attachment1.original_extension, fullFileName), mimeType: mimeType, thumbnailUrl: attachment1.thumbnail_url, previewUrl: attachment1.preview_url, previewWidth: attachment1.preview_width, previewHeight: attachment1.preview_height, largePreviewUrl: attachment1.large_preview_url, largePreviewWidth: attachment1.large_preview_width, largePreviewHeight: attachment1.large_preview_height, url: attachment1.metadata.url, // @Legacy width: attachment1.metadata.dimensions.split(",")[0], // @Legacy height: attachment1.metadata.dimensions.split(",")[1], // @Legacy name: fullFileName // @Legacy }; case "animated_image": return { type: "animated_image", ID: attachment2.id.toString(), filename: attachment2.filename, fullFileName: fullFileName, original_extension: getExtension(attachment2.original_extension, fullFileName), mimeType: mimeType, previewUrl: attachment1.preview_url, previewWidth: attachment1.preview_width, previewHeight: attachment1.preview_height, url: attachment2.image_data.url, width: attachment2.image_data.width, height: attachment2.image_data.height, name: attachment1.name, // @Legacy facebookUrl: attachment1.url, // @Legacy thumbnailUrl: attachment1.thumbnail_url, // @Legacy rawGifImage: attachment2.image_data.raw_gif_image, // @Legacy rawWebpImage: attachment2.image_data.raw_webp_image, // @Legacy animatedGifUrl: attachment2.image_data.animated_gif_url, // @Legacy animatedGifPreviewUrl: attachment2.image_data.animated_gif_preview_url, // @Legacy animatedWebpUrl: attachment2.image_data.animated_webp_url, // @Legacy animatedWebpPreviewUrl: attachment2.image_data.animated_webp_preview_url // @Legacy }; case "share": return { type: "share", ID: attachment1.share.share_id.toString(), url: attachment2.href, title: attachment1.share.title, description: attachment1.share.description, source: attachment1.share.source, image: attachment1.share.media.image, width: attachment1.share.media.image_size.width, height: attachment1.share.media.image_size.height, playable: attachment1.share.media.playable, duration: attachment1.share.media.duration, subattachments: attachment1.share.subattachments, properties: {}, animatedImageSize: attachment1.share.media.animated_image_size, // @Legacy facebookUrl: attachment1.share.uri, // @Legacy target: attachment1.share.target, // @Legacy styleList: attachment1.share.style_list // @Legacy }; case "video": return { type: "video", ID: attachment1.metadata.fbid.toString(), filename: attachment1.name, fullFileName: fullFileName, original_extension: getExtension(attachment1.original_extension, fullFileName), mimeType: mimeType, duration: durationVideo, previewUrl: attachment1.preview_url, previewWidth: attachment1.preview_width, previewHeight: attachment1.preview_height, url: attachment1.url, width: attachment1.metadata.dimensions.width, height: attachment1.metadata.dimensions.height, videoType: "unknown", thumbnailUrl: attachment1.thumbnail_url // @Legacy }; case "error": return { type: "error", // Save error attachments because we're unsure of their format, // and whether there are cases they contain something useful for debugging. attachment1: attachment1, attachment2: attachment2 }; case "MessageImage": return { type: "photo", ID: blob.legacy_attachment_id, filename: blob.filename, fullFileName: fullFileName, fileSize: fileSize, original_extension: getExtension(blob.original_extension, fullFileName), mimeType: mimeType, thumbnailUrl: blob.thumbnail.uri, previewUrl: blob.preview.uri, previewWidth: blob.preview.width, previewHeight: blob.preview.height, largePreviewUrl: blob.large_preview.uri, largePreviewWidth: blob.large_preview.width, largePreviewHeight: blob.large_preview.height, url: blob.large_preview.uri, // @Legacy width: blob.original_dimensions.x, // @Legacy height: blob.original_dimensions.y, // @Legacy name: blob.filename // @Legacy }; case "MessageAnimatedImage": return { type: "animated_image", ID: blob.legacy_attachment_id, filename: blob.filename, fullFileName: fullFileName, original_extension: getExtension(blob.original_extension, fullFileName), mimeType: mimeType, previewUrl: blob.preview_image.uri, previewWidth: blob.preview_image.width, previewHeight: blob.preview_image.height, url: blob.animated_image.uri, width: blob.animated_image.width, height: blob.animated_image.height, thumbnailUrl: blob.preview_image.uri, // @Legacy name: blob.filename, // @Legacy facebookUrl: blob.animated_image.uri, // @Legacy rawGifImage: blob.animated_image.uri, // @Legacy animatedGifUrl: blob.animated_image.uri, // @Legacy animatedGifPreviewUrl: blob.preview_image.uri, // @Legacy animatedWebpUrl: blob.animated_image.uri, // @Legacy animatedWebpPreviewUrl: blob.preview_image.uri // @Legacy }; case "MessageVideo": return { type: "video", ID: blob.legacy_attachment_id, filename: blob.filename, fullFileName: fullFileName, original_extension: getExtension(blob.original_extension, fullFileName), fileSize: fileSize, duration: durationVideo, mimeType: mimeType, previewUrl: blob.large_image.uri, previewWidth: blob.large_image.width, previewHeight: blob.large_image.height, url: blob.playable_url, width: blob.original_dimensions.x, height: blob.original_dimensions.y, videoType: blob.video_type.toLowerCase(), thumbnailUrl: blob.large_image.uri // @Legacy }; case "MessageAudio": return { type: "audio", ID: blob.url_shimhash, filename: blob.filename, fullFileName: fullFileName, fileSize: fileSize, duration: durationAudio, original_extension: getExtension(blob.original_extension, fullFileName), mimeType: mimeType, audioType: blob.audio_type, url: blob.playable_url, isVoiceMail: blob.is_voicemail }; case "StickerAttachment": case "Sticker": return { type: "sticker", ID: blob.id, url: blob.url, packID: blob.pack ? blob.pack.id : null, spriteUrl: blob.sprite_image, spriteUrl2x: blob.sprite_image_2x, width: blob.width, height: blob.height, caption: blob.label, description: blob.label, frameCount: blob.frame_count, frameRate: blob.frame_rate, framesPerRow: blob.frames_per_row, framesPerCol: blob.frames_per_column, stickerID: blob.id, // @Legacy spriteURI: blob.sprite_image, // @Legacy spriteURI2x: blob.sprite_image_2x // @Legacy }; case "MessageLocation": var urlAttach = blob.story_attachment.url; var mediaAttach = blob.story_attachment.media; var u = querystring.parse(url.parse(urlAttach).query).u; var where1 = querystring.parse(url.parse(u).query).where1; var address = where1.split(", "); var latitude; var longitude; try { latitude = Number.parseFloat(address[0]); longitude = Number.parseFloat(address[1]); } catch (err) { /* empty */ } var imageUrl; var width; var height; if (mediaAttach && mediaAttach.image) { imageUrl = mediaAttach.image.uri; width = mediaAttach.image.width; height = mediaAttach.image.height; } return { type: "location", ID: blob.legacy_attachment_id, latitude: latitude, longitude: longitude, image: imageUrl, width: width, height: height, url: u || urlAttach, address: where1, facebookUrl: blob.story_attachment.url, // @Legacy target: blob.story_attachment.target, // @Legacy styleList: blob.story_attachment.style_list // @Legacy }; case "ExtensibleAttachment": return { type: "share", ID: blob.legacy_attachment_id, url: blob.story_attachment.url, title: blob.story_attachment.title_with_entities.text, description: blob.story_attachment.description && blob.story_attachment.description.text, source: blob.story_attachment.source ? blob.story_attachment.source.text : null, image: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.uri, width: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.width, height: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.height, playable: blob.story_attachment.media && blob.story_attachment.media.is_playable, duration: blob.story_attachment.media && blob.story_attachment.media.playable_duration_in_ms, playableUrl: blob.story_attachment.media == null ? null : blob.story_attachment.media.playable_url, subattachments: blob.story_attachment.subattachments, properties: blob.story_attachment.properties.reduce(function(obj, cur) { obj[cur.key] = cur.value.text; return obj; }, {}), facebookUrl: blob.story_attachment.url, // @Legacy target: blob.story_attachment.target, // @Legacy styleList: blob.story_attachment.style_list // @Legacy }; case "MessageFile": return { type: "file", ID: blob.message_file_fbid, fullFileName: fullFileName, filename: blob.filename, fileSize: fileSize, mimeType: blob.mimetype, original_extension: blob.original_extension || fullFileName.split(".").pop(), url: blob.url, isMalicious: blob.is_malicious, contentType: blob.content_type, name: blob.filename }; default: throw new Error( "unrecognized attach_file of type " + type + "`" + JSON.stringify(attachment1, null, 4) + " attachment2: " + JSON.stringify(attachment2, null, 4) + "`" ); } } function formatAttachment(attachments, attachmentIds, attachmentMap, shareMap) { attachmentMap = shareMap || attachmentMap; return attachments ? attachments.map(function(val, i) { if ( !attachmentMap || !attachmentIds || !attachmentMap[attachmentIds[i]] ) { return _formatAttachment(val); } return _formatAttachment(val, attachmentMap[attachmentIds[i]]); }) : []; } function formatDeltaMessage(m) { const md = m.delta.messageMetadata; const mdata = m.delta.data === undefined ? [] : m.delta.data.prng === undefined ? [] : JSON.parse(m.delta.data.prng); const m_id = mdata.map(u => u.i); const m_offset = mdata.map(u => u.o); const m_length = mdata.map(u => u.l); const mentions = {}; for (let i = 0; i < m_id.length; i++) { mentions[m_id[i]] = m.delta.body.substring( m_offset[i], m_offset[i] + m_length[i] ); } return { type: "message", senderID: formatID(md.actorFbId.toString()), body: m.delta.body || "", threadID: formatID( (md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString() ), messageID: md.messageId, attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)), mentions: mentions, timestamp: md.timestamp, isGroup: !!md.threadKey.threadFbId, participantIDs: m.delta.participants }; } function formatID(id) { if (id != undefined && id != null) { return id.replace(/(fb)?id[:.]/, ""); } else { return id; } } function formatMessage(m) { const originalMessage = m.message ? m.message : m; const obj = { type: "message", senderName: originalMessage.sender_name, senderID: formatID(originalMessage.sender_fbid.toString()), participantNames: originalMessage.group_thread_info ? originalMessage.group_thread_info.participant_names : [originalMessage.sender_name.split(" ")[0]], participantIDs: originalMessage.group_thread_info ? originalMessage.group_thread_info.participant_ids.map(function(v) { return formatID(v.toString()); }) : [formatID(originalMessage.sender_fbid)], body: originalMessage.body || "", threadID: formatID( ( originalMessage.thread_fbid || originalMessage.other_user_fbid ).toString() ), threadName: originalMessage.group_thread_info ? originalMessage.group_thread_info.name : originalMessage.sender_name, location: originalMessage.coordinates ? originalMessage.coordinates : null, messageID: originalMessage.mid ? originalMessage.mid.toString() : originalMessage.message_id, attachments: formatAttachment( originalMessage.attachments, originalMessage.attachmentIds, originalMessage.attachment_map, originalMessage.share_map ), timestamp: originalMessage.timestamp, timestampAbsolute: originalMessage.timestamp_absolute, timestampRelative: originalMessage.timestamp_relative, timestampDatetime: originalMessage.timestamp_datetime, tags: originalMessage.tags, reactions: originalMessage.reactions ? originalMessage.reactions : [], isUnread: originalMessage.is_unread }; if (m.type === "pages_messaging") obj.pageID = m.realtime_viewer_fbid.toString(); obj.isGroup = obj.participantIDs.length > 2; return obj; } function formatEvent(m) { const originalMessage = m.message ? m.message : m; let logMessageType = originalMessage.log_message_type; let logMessageData; if (logMessageType === "log:generic-admin-text") { logMessageData = originalMessage.log_message_data.untypedData; logMessageType = getAdminTextMessageType( originalMessage.log_message_data.message_type ); } else { logMessageData = originalMessage.log_message_data; } return Object.assign(formatMessage(originalMessage), { type: "event", logMessageType: logMessageType, logMessageData: logMessageData, logMessageBody: originalMessage.log_message_body }); } function formatHistoryMessage(m) { switch (m.action_type) { case "ma-type:log-message": return formatEvent(m); default: return formatMessage(m); } } // Get a more readable message type for AdminTextMessages function getAdminTextMessageType(type) { switch (type) { case 'unpin_messages_v2': return 'log:unpin-message'; case 'pin_messages_v2': return 'log:pin-message'; case "change_thread_theme": return "log:thread-color"; case "change_thread_icon": case 'change_thread_quick_reaction': return "log:thread-icon"; case "change_thread_nickname": return "log:user-nickname"; case "change_thread_admins": return "log:thread-admins"; case "group_poll": return "log:thread-poll"; case "change_thread_approval_mode": return "log:thread-approval-mode"; case "messenger_call_log": case "participant_joined_group_call": return "log:thread-call"; default: return type; } } function formatDeltaEvent(m) { let logMessageType; let logMessageData; // log:thread-color => {theme_color} // log:user-nickname => {participant_id, nickname} // log:thread-icon => {thread_icon} // log:thread-name => {name} // log:subscribe => {addedParticipants - [Array]} // log:unsubscribe => {leftParticipantFbId} switch (m.class) { case "AdminTextMessage": logMessageData = m.untypedData; logMessageType = getAdminTextMessageType(m.type); break; case "ThreadName": logMessageType = "log:thread-name"; logMessageData = { name: m.name }; break; case "ParticipantsAddedToGroupThread": logMessageType = "log:subscribe"; logMessageData = { addedParticipants: m.addedParticipants }; break; case "ParticipantLeftGroupThread": logMessageType = "log:unsubscribe"; logMessageData = { leftParticipantFbId: m.leftParticipantFbId }; break; case "ApprovalQueue": logMessageType = "log:approval-queue"; logMessageData = { approvalQueue: { action: m.action, recipientFbId: m.recipientFbId, requestSource: m.requestSource, ...m.messageMetadata } }; } return { type: "event", threadID: formatID( ( m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId ).toString() ), messageID: m.messageMetadata.messageId.toString(), logMessageType, logMessageData, logMessageBody: m.messageMetadata.adminText, timestamp: m.messageMetadata.timestamp, author: m.messageMetadata.actorFbId, participantIDs: m.participants }; } function formatTyp(event) { return { isTyping: !!event.st, from: event.from.toString(), threadID: formatID( (event.to || event.thread_fbid || event.from).toString() ), // When receiving typ indication from mobile, `from_mobile` isn't set. // If it is, we just use that value. fromMobile: event.hasOwnProperty("from_mobile") ? event.from_mobile : true, userID: (event.realtime_viewer_fbid || event.from).toString(), type: "typ" }; } function formatDeltaReadReceipt(delta) { // otherUserFbId seems to be used as both the readerID and the threadID in a 1-1 chat. // In a group chat actorFbId is used for the reader and threadFbId for the thread. return { reader: (delta.threadKey.otherUserFbId || delta.actorFbId).toString(), time: delta.actionTimestampMs, threadID: formatID( (delta.threadKey.otherUserFbId || delta.threadKey.threadFbId).toString() ), type: "read_receipt" }; } function formatReadReceipt(event) { return { reader: event.reader.toString(), time: event.time, threadID: formatID((event.thread_fbid || event.reader).toString()), type: "read_receipt" }; } function formatRead(event) { return { threadID: formatID( ( (event.chat_ids && event.chat_ids[0]) || (event.thread_fbids && event.thread_fbids[0]) ).toString() ), time: event.timestamp, type: "read" }; } function getFrom(str, startToken, endToken) { const start = str.indexOf(startToken) + startToken.length; if (start < startToken.length) return ""; const lastHalf = str.substring(start); const end = lastHalf.indexOf(endToken); if (end === -1) { throw Error( "Could not find endTime `" + endToken + "` in the given string." ); } return lastHalf.substring(0, end); } function makeParsable(html) { const withoutForLoop = html.replace(/for\s*\(\s*;\s*;\s*\)\s*;\s*/, ""); // (What the fuck FB, why windows style newlines?) // So sometimes FB will send us base multiple objects in the same response. // They're all valid JSON, one after the other, at the top level. We detect // that and make it parse-able by JSON.parse. // Ben - July 15th 2017 // // It turns out that Facebook may insert random number of spaces before // next object begins (issue #616) // rav_kr - 2018-03-19 const maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/); if (maybeMultipleObjects.length === 1) return maybeMultipleObjects; return "[" + maybeMultipleObjects.join("},{") + "]"; } function arrToForm(form) { return arrayToObject( form, function(v) { return v.name; }, function(v) { return v.val; } ); } function arrayToObject(arr, getKey, getValue) { return arr.reduce(function(acc, val) { acc[getKey(val)] = getValue(val); return acc; }, {}); } function getSignatureID() { return Math.floor(Math.random() * 2147483648).toString(16); } function generateTimestampRelative() { const d = new Date(); return d.getHours() + ":" + padZeros(d.getMinutes()); } function makeDefaults(html, userID, ctx) { let reqCounter = 1; const revision = getFrom(html, 'revision":', ","); function mergeWithDefaults(obj) { const newObj = { av: userID, __user: userID, __req: (reqCounter++).toString(36), __rev: revision, __a: 1, ...(ctx && { fb_dtsg: ctx.fb_dtsg, jazoest: ctx.jazoest }) } if (!obj) return newObj; for (var prop in obj) { if (obj.hasOwnProperty(prop)) { if (!newObj[prop]) newObj[prop] = obj[prop]; } } return newObj; } return { get: (url, jar, qs, ctxx, customHeader = {}) => get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx, customHeader), post: (url, jar, form, ctxx, customHeader = {}) => post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx, customHeader), postFormData: (url, jar, form, qs, ctxx) => postFormData(url, jar, mergeWithDefaults(form), mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx) }; } function parseAndCheckLogin(ctx, http, retryCount) { var delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); var _try = (tryData) => new Promise(function(resolve, reject) { try { resolve(tryData()); } catch (error) { reject(error); } }); if (retryCount == undefined) retryCount = 0; return function(data) { function any() { if (data.statusCode >= 500 && data.statusCode < 600) { if (retryCount >= 5) { const err = new Error("Request retry failed. Check the `res` and `statusCode` property on this error."); err.statusCode = data.statusCode; err.res = data.body; err.error = "Request retry failed. Check the `res` and `statusCode` property on this error."; throw err; } retryCount++; const retryTime = Math.floor(Math.random() * 5000); console.warn("parseAndCheckLogin", "Got status code " + data.statusCode + " - " + retryCount + ". attempt to retry in " + retryTime + " milliseconds..."); const url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.request.uri.pathname; if (data.request.headers["content-type"].split(";")[0] === "multipart/form-data") { return delay(retryTime) .then(function() { return http .postFormData(url, ctx.jar, data.request.formData); }) .then(parseAndCheckLogin(ctx, http, retryCount)); } else { return delay(retryTime) .then(function() { return http .post(url, ctx.jar, data.request.formData); }) .then(parseAndCheckLogin(ctx, http, retryCount)); } } if (data.statusCode === 404) return; if (data.statusCode !== 200) throw new Error("parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response."); let res = null; try { res = JSON.parse(makeParsable(data.body)); } catch (e) { const err = new Error("JSON.parse error. Check the `detail` property on this error."); err.error = "JSON.parse error. Check the `detail` property on this error."; err.detail = e; err.res = data.body; throw err; } // In some cases the response contains only a redirect URL which should be followed if (res.redirect && data.request.method === "GET") { return http .get(res.redirect, ctx.jar) .then(parseAndCheckLogin(ctx, http)); } // TODO: handle multiple cookies? if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") { res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", ""); const requireCookie = res.jsmods.require[0][3]; ctx.jar.setCookie(formatCookie(requireCookie, "facebook"), "https://www.facebook.com"); ctx.jar.setCookie(formatCookie(requireCookie, "messenger"), "https://www.messenger.com"); } // On every request we check if we got a DTSG and we mutate the context so that we use the latest // one for the next requests. if (res.jsmods && Array.isArray(res.jsmods.require)) { const arr = res.jsmods.require; for (const i in arr) { if (arr[i][0] === "DTSG" && arr[i][1] === "setToken") { ctx.fb_dtsg = arr[i][3][0]; // Update ttstamp since that depends on fb_dtsg ctx.ttstamp = "2"; for (let j = 0; j < ctx.fb_dtsg.length; j++) { ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j); } } } } if (res.error === 1357001) { const err = new Error('Facebook blocked the login'); err.error = "Not logged in."; throw err; } return res; } return _try(any); }; } function saveCookies(jar) { return function(res) { const cookies = res.headers["set-cookie"] || []; cookies.forEach(function(c) { if (c.indexOf(".facebook.com") > -1) { jar.setCookie(c, "https://www.facebook.com"); } const c2 = c.replace(/domain=\.facebook\.com/, "domain=.messenger.com"); jar.setCookie(c2, "https://www.messenger.com"); }); return res; }; } const NUM_TO_MONTH = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ]; const NUM_TO_DAY = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function formatDate(date) { let d = date.getUTCDate(); d = d >= 10 ? d : "0" + d; let h = date.getUTCHours(); h = h >= 10 ? h : "0" + h; let m = date.getUTCMinutes(); m = m >= 10 ? m : "0" + m; let s = date.getUTCSeconds(); s = s >= 10 ? s : "0" + s; return ( NUM_TO_DAY[date.getUTCDay()] + ", " + d + " " + NUM_TO_MONTH[date.getUTCMonth()] + " " + date.getUTCFullYear() + " " + h + ":" + m + ":" + s + " GMT" ); } function formatCookie(arr, url) { return ( arr[0] + "=" + arr[1] + "; Path=" + arr[3] + "; Domain=" + url + ".com" ); } function formatThread(data) { return { threadID: formatID(data.thread_fbid.toString()), participants: data.participants.map(formatID), participantIDs: data.participants.map(formatID), name: data.name, nicknames: data.custom_nickname, snippet: data.snippet, snippetAttachments: data.snippet_attachments, snippetSender: formatID((data.snippet_sender || "").toString()), unreadCount: data.unread_count, messageCount: data.message_count, imageSrc: data.image_src, timestamp: data.timestamp, serverTimestamp: data.server_timestamp, // what is this? muteUntil: data.mute_until, isCanonicalUser: data.is_canonical_user, isCanonical: data.is_canonical, isSubscribed: data.is_subscribed, folder: data.folder, isArchived: data.is_archived, recipientsLoadable: data.recipients_loadable, hasEmailParticipant: data.has_email_participant, readOnly: data.read_only, canReply: data.can_reply, cannotReplyReason: data.cannot_reply_reason, lastMessageTimestamp: data.last_message_timestamp, lastReadTimestamp: data.last_read_timestamp, lastMessageType: data.last_message_type, emoji: data.custom_like_icon, color: data.custom_color, adminIDs: data.admin_ids, threadType: data.thread_type }; } function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1); } function formatProxyPresence(presence, userID) { if (presence.lat === undefined || presence.p === undefined) return null; return { type: "presence", timestamp: presence.lat * 1000, userID: userID, statuses: presence.p }; } function formatPresence(presence, userID) { return { type: "presence", timestamp: presence.la * 1000, userID: userID, statuses: presence.a }; } function decodeClientPayload(payload) { /* Special function which Client using to "encode" clients JSON payload */ return JSON.parse(String.fromCharCode.apply(null, payload)); } function getAppState(jar) { return jar .getCookies("https://www.facebook.com") .concat(jar.getCookies("https://www.messenger.com")); } function getAccessFromBusiness(jar, Options) { return function(res) { var html = res ? res.body : null; return get('https://business.facebook.com/content_management', jar, null, Options, null, { noRef: true }) .then(function(res) { var token = /"accessToken":"([^.]+)","clientID":/g.exec(res.body)[1]; return [html, token]; }) .catch(function() { return [html, null]; }); } } const meta = prop => new RegExp(`<meta property="${prop}" content="([^"]*)"`); module.exports = { //logs log(...args) { console.log(ws, chalk.green.bold("[LOG]"), ...args); }, error(...args) { console.error(ws, chalk.red.bold("[ERROR]"), ...args); }, warn(...args) { console.warn(ws, chalk.yellow.bold("[WARNING]"), ...args); }, //end logs isReadableStream, cleanGet, get, post, postFormData, generateThreadingID, generateOfflineThreadingID, getGUID, getFrom, makeParsable, arrToForm, getSignatureID, getJar: request.jar, generateTimestampRelative, makeDefaults, parseAndCheckLogin, saveCookies, getType, _formatAttachment, formatHistoryMessage, formatID, formatMessage, formatDeltaEvent, formatDeltaMessage, formatProxyPresence, formatPresence, formatTyp, formatDeltaReadReceipt, formatCookie, formatThread, formatReadReceipt, formatRead, generatePresence, generateAccessiblityCookie, formatDate, decodeClientPayload, getAppState, getAdminTextMessageType, setProxy, getAccessFromBusiness, presenceDecode, presenceEncode, headers, defaultUserAgent, windowsUserAgent, randomUserAgent, meta };