|
|
|
|
|
"use strict"; |
|
|
|
|
|
let request = promisifyPromise(require("request").defaults({ jar: true, proxy: process.env.FB_PROXY })); |
|
|
const stream = require("stream"); |
|
|
const log = require("npmlog"); |
|
|
const querystring = require("querystring"); |
|
|
const url = require("url"); |
|
|
|
|
|
class CustomError extends Error { |
|
|
constructor(obj) { |
|
|
if (typeof obj === 'string') |
|
|
obj = { message: obj }; |
|
|
if (typeof obj !== 'object' || obj === null) |
|
|
throw new TypeError('Object required'); |
|
|
obj.message ? super(obj.message) : super(); |
|
|
Object.assign(this, obj); |
|
|
} |
|
|
} |
|
|
|
|
|
function callbackToPromise(func) { |
|
|
return function (...args) { |
|
|
return new Promise((resolve, reject) => { |
|
|
func(...args, (err, data) => { |
|
|
if (err) |
|
|
reject(err); |
|
|
else |
|
|
resolve(data); |
|
|
}); |
|
|
}); |
|
|
}; |
|
|
} |
|
|
|
|
|
function isHasCallback(func) { |
|
|
if (typeof func !== "function") |
|
|
return false; |
|
|
return func.toString().split("\n")[0].match(/(callback|cb)\s*\)/) !== null; |
|
|
} |
|
|
|
|
|
|
|
|
function promisifyPromise(promise) { |
|
|
const keys = Object.keys(promise); |
|
|
let promise_; |
|
|
if ( |
|
|
typeof promise === "function" |
|
|
&& isHasCallback(promise) |
|
|
) |
|
|
promise_ = callbackToPromise(promise); |
|
|
else |
|
|
promise_ = promise; |
|
|
|
|
|
for (const key of keys) { |
|
|
if (!promise[key]?.toString) |
|
|
continue; |
|
|
|
|
|
if ( |
|
|
typeof promise[key] === "function" |
|
|
&& isHasCallback(promise[key]) |
|
|
) { |
|
|
promise_[key] = callbackToPromise(promise[key]); |
|
|
} |
|
|
else { |
|
|
promise_[key] = promise[key]; |
|
|
} |
|
|
} |
|
|
|
|
|
return promise_; |
|
|
} |
|
|
|
|
|
|
|
|
function delay(ms) { |
|
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
|
} |
|
|
|
|
|
|
|
|
function tryPromise(tryFunc) { |
|
|
return new Promise((resolve, reject) => { |
|
|
try { |
|
|
resolve(tryFunc()); |
|
|
} catch (error) { |
|
|
reject(error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
function setProxy(url) { |
|
|
if (typeof url == "undefined") |
|
|
return request = promisifyPromise(require("request").defaults({ |
|
|
jar: true |
|
|
})); |
|
|
return request = promisifyPromise(require("request").defaults({ |
|
|
jar: true, |
|
|
proxy: url |
|
|
})); |
|
|
} |
|
|
|
|
|
function getHeaders(url, options, ctx, customHeader) { |
|
|
const headers = { |
|
|
"Content-Type": "application/x-www-form-urlencoded", |
|
|
Referer: "https://www.facebook.com/", |
|
|
Host: url.replace("https://", "").split("/")[0], |
|
|
Origin: "https://www.facebook.com", |
|
|
"User-Agent": options.userAgent, |
|
|
Connection: "keep-alive", |
|
|
"sec-fetch-site": "same-origin" |
|
|
}; |
|
|
if (customHeader) { |
|
|
Object.assign(headers, customHeader); |
|
|
} |
|
|
if (ctx && ctx.region) { |
|
|
headers["X-MSGR-Region"] = ctx.region; |
|
|
} |
|
|
|
|
|
return headers; |
|
|
} |
|
|
|
|
|
function isReadableStream(obj) { |
|
|
return ( |
|
|
obj instanceof stream.Stream && |
|
|
(getType(obj._read) === "Function" || |
|
|
getType(obj._read) === "AsyncFunction") && |
|
|
getType(obj._readableState) === "Object" |
|
|
); |
|
|
} |
|
|
|
|
|
function get(url, jar, qs, options, ctx) { |
|
|
|
|
|
if (getType(qs) === "Object") { |
|
|
for (const prop in qs) { |
|
|
if (qs.hasOwnProperty(prop) && getType(qs[prop]) === "Object") { |
|
|
qs[prop] = JSON.stringify(qs[prop]); |
|
|
} |
|
|
} |
|
|
} |
|
|
const op = { |
|
|
headers: getHeaders(url, options, ctx), |
|
|
timeout: 60000, |
|
|
qs: qs, |
|
|
url: url, |
|
|
method: "GET", |
|
|
jar: jar, |
|
|
gzip: true |
|
|
}; |
|
|
|
|
|
return request(op).then(function (res) { |
|
|
return Array.isArray(res) ? res[0] : res; |
|
|
}); |
|
|
} |
|
|
|
|
|
function post(url, jar, form, options, ctx, customHeader) { |
|
|
const op = { |
|
|
headers: getHeaders(url, options, ctx, customHeader), |
|
|
timeout: 60000, |
|
|
url: url, |
|
|
method: "POST", |
|
|
form: form, |
|
|
jar: jar, |
|
|
gzip: true |
|
|
}; |
|
|
|
|
|
return request(op).then(function (res) { |
|
|
return Array.isArray(res) ? res[0] : res; |
|
|
}); |
|
|
} |
|
|
|
|
|
function postFormData(url, jar, form, qs, options, ctx) { |
|
|
const headers = getHeaders(url, options, ctx); |
|
|
headers["Content-Type"] = "multipart/form-data"; |
|
|
const op = { |
|
|
headers: headers, |
|
|
timeout: 60000, |
|
|
url: url, |
|
|
method: "POST", |
|
|
formData: form, |
|
|
qs: qs, |
|
|
jar: jar, |
|
|
gzip: true |
|
|
}; |
|
|
|
|
|
return request(op).then(function (res) { |
|
|
return Array.isArray(res) ? res[0] : res; |
|
|
}); |
|
|
} |
|
|
|
|
|
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]; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
let sectionLength = Date.now(); |
|
|
|
|
|
const id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { |
|
|
|
|
|
const r = Math.floor((sectionLength + Math.random() * 16) % 16); |
|
|
|
|
|
sectionLength = Math.floor(sectionLength / 16); |
|
|
|
|
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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(), |
|
|
spriteURI: attachment1.metadata.spriteURI, |
|
|
spriteURI2x: attachment1.metadata.spriteURI2x |
|
|
}; |
|
|
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 |
|
|
}; |
|
|
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, |
|
|
width: attachment1.metadata.dimensions.split(",")[0], |
|
|
height: attachment1.metadata.dimensions.split(",")[1], |
|
|
name: fullFileName |
|
|
}; |
|
|
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, |
|
|
facebookUrl: attachment1.url, |
|
|
thumbnailUrl: attachment1.thumbnail_url, |
|
|
rawGifImage: attachment2.image_data.raw_gif_image, |
|
|
rawWebpImage: attachment2.image_data.raw_webp_image, |
|
|
animatedGifUrl: attachment2.image_data.animated_gif_url, |
|
|
animatedGifPreviewUrl: attachment2.image_data.animated_gif_preview_url, |
|
|
animatedWebpUrl: attachment2.image_data.animated_webp_url, |
|
|
animatedWebpPreviewUrl: attachment2.image_data.animated_webp_preview_url |
|
|
}; |
|
|
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, |
|
|
facebookUrl: attachment1.share.uri, |
|
|
target: attachment1.share.target, |
|
|
styleList: attachment1.share.style_list |
|
|
}; |
|
|
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 |
|
|
}; |
|
|
case "error": |
|
|
return { |
|
|
type: "error", |
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
width: blob.original_dimensions.x, |
|
|
height: blob.original_dimensions.y, |
|
|
name: blob.filename |
|
|
}; |
|
|
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, |
|
|
name: blob.filename, |
|
|
facebookUrl: blob.animated_image.uri, |
|
|
rawGifImage: blob.animated_image.uri, |
|
|
animatedGifUrl: blob.animated_image.uri, |
|
|
animatedGifPreviewUrl: blob.preview_image.uri, |
|
|
animatedWebpUrl: blob.animated_image.uri, |
|
|
animatedWebpPreviewUrl: blob.preview_image.uri |
|
|
}; |
|
|
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 |
|
|
}; |
|
|
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, |
|
|
spriteURI: blob.sprite_image, |
|
|
spriteURI2x: blob.sprite_image_2x |
|
|
}; |
|
|
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) { |
|
|
|
|
|
} |
|
|
|
|
|
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, |
|
|
target: blob.story_attachment.target, |
|
|
styleList: blob.story_attachment.style_list |
|
|
}; |
|
|
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, |
|
|
target: blob.story_attachment.target, |
|
|
styleList: blob.story_attachment.style_list |
|
|
}; |
|
|
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 || (md.cid ? md.cid.canonicalParticipantFbids : []) || [] |
|
|
}; |
|
|
} |
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function getAdminTextMessageType(type) { |
|
|
switch (type) { |
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: logMessageType, |
|
|
logMessageData: logMessageData, |
|
|
logMessageBody: m.messageMetadata.adminText, |
|
|
timestamp: m.messageMetadata.timestamp, |
|
|
author: m.messageMetadata.actorFbId, |
|
|
participantIDs: (m.participants || []).map(p => p.toString()) |
|
|
}; |
|
|
} |
|
|
|
|
|
function formatTyp(event) { |
|
|
return { |
|
|
isTyping: !!event.st, |
|
|
from: event.from.toString(), |
|
|
threadID: formatID( |
|
|
(event.to || event.thread_fbid || event.from).toString() |
|
|
), |
|
|
|
|
|
|
|
|
fromMobile: event.hasOwnProperty("from_mobile") ? event.from_mobile : true, |
|
|
userID: (event.realtime_viewer_fbid || event.from).toString(), |
|
|
type: "typ" |
|
|
}; |
|
|
} |
|
|
|
|
|
function formatDeltaReadReceipt(delta) { |
|
|
|
|
|
|
|
|
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 new 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*/, ""); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 fb_dtsg = getFrom(html, 'name="fb_dtsg" value="', '"'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let ttstamp = "2"; |
|
|
for (let i = 0; i < fb_dtsg.length; i++) { |
|
|
ttstamp += fb_dtsg.charCodeAt(i); |
|
|
} |
|
|
const revision = getFrom(html, 'revision":', ","); |
|
|
|
|
|
function mergeWithDefaults(obj) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const newObj = { |
|
|
__user: userID, |
|
|
__req: (reqCounter++).toString(36), |
|
|
__rev: revision, |
|
|
__a: 1, |
|
|
|
|
|
fb_dtsg: ctx.fb_dtsg ? ctx.fb_dtsg : fb_dtsg, |
|
|
jazoest: ctx.ttstamp ? ctx.ttstamp : ttstamp |
|
|
|
|
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!obj) return newObj; |
|
|
|
|
|
for (const prop in obj) { |
|
|
if (obj.hasOwnProperty(prop)) { |
|
|
if (!newObj[prop]) { |
|
|
newObj[prop] = obj[prop]; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return newObj; |
|
|
} |
|
|
|
|
|
function postWithDefaults(url, jar, form, ctxx, customHeader = {}) { |
|
|
return post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx, customHeader); |
|
|
} |
|
|
|
|
|
function getWithDefaults(url, jar, qs, ctxx, customHeader = {}) { |
|
|
return get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx, customHeader); |
|
|
} |
|
|
|
|
|
function postFormDataWithDefault(url, jar, form, qs, ctxx) { |
|
|
return postFormData( |
|
|
url, |
|
|
jar, |
|
|
mergeWithDefaults(form), |
|
|
mergeWithDefaults(qs), |
|
|
ctx.globalOptions, |
|
|
ctxx || ctx |
|
|
); |
|
|
} |
|
|
|
|
|
return { |
|
|
get: getWithDefaults, |
|
|
post: postWithDefaults, |
|
|
postFormData: postFormDataWithDefault |
|
|
}; |
|
|
} |
|
|
|
|
|
function parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall) { |
|
|
if (retryCount == undefined) { |
|
|
retryCount = 0; |
|
|
} |
|
|
if (sourceCall == undefined) { |
|
|
try { |
|
|
throw new Error(); |
|
|
} |
|
|
catch (e) { |
|
|
sourceCall = e; |
|
|
} |
|
|
} |
|
|
return function (data) { |
|
|
return tryPromise(function () { |
|
|
log.verbose("parseAndCheckLogin", data.body); |
|
|
if (data.statusCode >= 500 && data.statusCode < 600) { |
|
|
if (retryCount >= 5) { |
|
|
throw new CustomError({ |
|
|
message: "Request retry failed. Check the `res` and `statusCode` property on this error.", |
|
|
statusCode: data.statusCode, |
|
|
res: data.body, |
|
|
error: "Request retry failed. Check the `res` and `statusCode` property on this error.", |
|
|
sourceCall: sourceCall |
|
|
}); |
|
|
} |
|
|
retryCount++; |
|
|
const retryTime = Math.floor(Math.random() * 5000); |
|
|
log.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 defaultFuncs.postFormData( |
|
|
url, |
|
|
ctx.jar, |
|
|
data.request.formData, |
|
|
{} |
|
|
); |
|
|
}) |
|
|
.then(parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall)); |
|
|
} |
|
|
else { |
|
|
return delay(retryTime) |
|
|
.then(function () { |
|
|
return defaultFuncs.post(url, ctx.jar, data.request.formData); |
|
|
}) |
|
|
.then(parseAndCheckLogin(ctx, defaultFuncs, retryCount, sourceCall)); |
|
|
} |
|
|
} |
|
|
if (data.statusCode !== 200) |
|
|
throw new CustomError({ |
|
|
message: "parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.", |
|
|
statusCode: data.statusCode, |
|
|
res: data.body, |
|
|
error: "parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.", |
|
|
sourceCall: sourceCall |
|
|
}); |
|
|
|
|
|
let res = null; |
|
|
try { |
|
|
res = JSON.parse(makeParsable(data.body)); |
|
|
} catch (e) { |
|
|
throw new CustomError({ |
|
|
message: "JSON.parse error. Check the `detail` property on this error.", |
|
|
detail: e, |
|
|
res: data.body, |
|
|
error: "JSON.parse error. Check the `detail` property on this error.", |
|
|
sourceCall: sourceCall |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (res.redirect && data.request.method === "GET") { |
|
|
return defaultFuncs |
|
|
.get(res.redirect, ctx.jar) |
|
|
.then(parseAndCheckLogin(ctx, defaultFuncs, undefined, sourceCall)); |
|
|
} |
|
|
|
|
|
|
|
|
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 cookie = formatCookie(res.jsmods.require[0][3], "facebook"); |
|
|
const cookie2 = formatCookie(res.jsmods.require[0][3], "messenger"); |
|
|
ctx.jar.setCookie(cookie, "https://www.facebook.com"); |
|
|
ctx.jar.setCookie(cookie2, "https://www.messenger.com"); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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]; |
|
|
|
|
|
|
|
|
ctx.ttstamp = "2"; |
|
|
for (let j = 0; j < ctx.fb_dtsg.length; j++) { |
|
|
ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (res.error === 1357001) { |
|
|
throw new CustomError({ |
|
|
message: "Facebook blocked login. Please visit https://facebook.com and check your account.", |
|
|
error: "Not logged in.", |
|
|
res: res, |
|
|
statusCode: data.statusCode, |
|
|
sourceCall: sourceCall |
|
|
}); |
|
|
} |
|
|
return res; |
|
|
}); |
|
|
}; |
|
|
} |
|
|
|
|
|
function checkLiveCookie(ctx, defaultFuncs) { |
|
|
return defaultFuncs |
|
|
.get("https://m.facebook.com/me", ctx.jar) |
|
|
.then(function (res) { |
|
|
if (res.body.indexOf(ctx.i_userID || ctx.userID) === -1) { |
|
|
throw new CustomError({ |
|
|
message: "Not logged in.", |
|
|
error: "Not logged in." |
|
|
}); |
|
|
} |
|
|
return true; |
|
|
}); |
|
|
} |
|
|
|
|
|
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, |
|
|
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) { |
|
|
|
|
|
|
|
|
|
|
|
return JSON.parse(String.fromCharCode.apply(null, payload)); |
|
|
} |
|
|
|
|
|
function getAppState(jar) { |
|
|
return jar |
|
|
.getCookies("https://www.facebook.com") |
|
|
.concat(jar.getCookies("https://facebook.com")) |
|
|
.concat(jar.getCookies("https://www.messenger.com")); |
|
|
} |
|
|
module.exports = { |
|
|
CustomError, |
|
|
isReadableStream, |
|
|
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, |
|
|
checkLiveCookie |
|
|
}; |
|
|
|