handz
Version:
Facebook-chat-api protect and deploy by Kanzu and HZI Team
1,306 lines (1,148 loc) • 63.4 kB
JavaScript
// @ts-nocheck
/* eslint-disable no-undef */
/* eslint-disable no-prototype-builtins */
"use strict";
var url = require("url");
var log = require("npmlog");
const _ = require('lodash');
var stream = require("stream");
var bluebird = require("bluebird");
var querystring = require("querystring");
var request = bluebird.promisify(require("request").defaults({ jar: true }));
/**
* @param {any} url
*/
function setProxy(url) {
if (typeof url == undefined) return request = bluebird.promisify(require("request").defaults({ jar: true }));
return request = bluebird.promisify(require("request").defaults({ jar: true, proxy: url }));
}
/**
* @param {string | URL} url
* @param {{ userAgent: any; }} options
* @param {{ region: any; }} [ctx]
* @param {undefined} [customHeader]
*/
function getHeaders(url, options, ctx, customHeader) {
var 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 || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.114 Safari/537.36"),
Connection: "keep-alive",
"sec-fetch-site": 'same-origin',
"sec-fetch-mode": 'cors'
};
if (customHeader) Object.assign(headers, customHeader);
if (ctx && ctx.region) headers["X-MSGR-Region"] = ctx.region;
return headers;
}
/**
* @param {{ _read: any; _readableState: any; }} obj
*/
function isReadableStream(obj) {
return (
obj instanceof stream.Stream &&
(getType(obj._read) === "Function" ||
getType(obj._read) === "AsyncFunction") &&
getType(obj._readableState) === "Object"
);
}
/**
* @param {any} url
* @param {any} jar
* @param {{ [x: string]: any; fb_dtsg?: any; jazoest?: any; hasOwnProperty?: any; }} qs
* @param {any} options
* @param {any} ctx
*/
function get(url, jar, qs, options, ctx) {
// I'm still confused about this
if (getType(qs) === "Object")
for (var prop in qs)
if (qs.hasOwnProperty(prop) && getType(qs[prop]) === "Object") qs[prop] = JSON.stringify(qs[prop]);
var 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 res;
});
}
function post(url, jar, form, options, ctx, customHeader) {
var op = {
headers: getHeaders(url, options),
timeout: 60000,
url: url,
method: "POST",
form: form,
jar: jar,
gzip: true
};
return request(op).then(function(res) {
return res;
});
}
/**
* @param {any} url
* @param {any} jar
* @param {{ __user: any; __req: string; __rev: any; __a: number;
// __af: siteData.features,
fb_dtsg: any; jazoest: any; }} form
* @param {{ __user: any; __req: string; __rev: any; __a: number;
// __af: siteData.features,
fb_dtsg: any; jazoest: any; }} qs
* @param {any} options
* @param {any} ctx
*/
function postFormData(url, jar, form, qs, options, ctx) {
var headers = getHeaders(url, options, ctx);
headers["Content-Type"] = "multipart/form-data";
var op = {
headers: headers,
timeout: 60000,
url: url,
method: "POST",
formData: form,
qs: qs,
jar: jar,
gzip: true
};
return request(op).then(function(res) {
return res;
});
}
/**
* @param {string | number | any[]} val
* @param {number} [len]
*/
function padZeros(val, len) {
val = String(val);
len = len || 2;
while (val.length < len) val = "0" + val;
return val;
}
/**
* @param {any} clientID
*/
function generateThreadingID(clientID) {
var k = Date.now();
var l = Math.floor(Math.random() * 4294967295);
var m = clientID;
return "<" + k + ":" + l + "-" + m + "@mail.projektitan.com>";
}
/**
* @param {string | any[]} data
*/
function binaryToDecimal(data) {
var ret = "";
while (data !== "0") {
var end = 0;
var fullName = "";
var 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() {
var ret = Date.now();
var value = Math.floor(Math.random() * 4294967295);
var str = ("0000000000000000000000" + value.toString(2)).slice(-22);
var msgs = ret.toString(2) + str;
return binaryToDecimal(msgs);
}
var h;
var i = {};
var j = {
_: "vtuan",
A: "vtuanhihi",
B: "vtuanhihi",
C: "vtuanhihi",
D: "vtuanhihi",
E: "vtuanhihi",
F: "vtuanhihi",
G: "vtuanhihi",
H: "vtuanhihi",
I: "vtuanhihi",
J: "vtuanhihi",
K: "vtuanhihi",
L: "vtuanhihi",
M: "vtuanhihi",
N: "vtuanhihi",
O: "vtuanhihi",
P: "vtuanhihi",
Q: "vtuanhihi",
R: "vtuanhihi",
S: "vtuanhihi",
T: "vtuanhihi",
U: "vtuanhihivtuanhihi",
V: "vtuanhihi",
W: "vtuanhihi",
X: "vtuanhihi",
Y: "vtuanhihi",
Z: "vtuanhihivtuanhihivtuanhihi"
};
(function() {
var l = [];
for (var m in j) {
i[j[m]] = m;
l.push(j[m]);
}
l.reverse();
h = new RegExp(l.join("|"), "g");
})();
/**
* @param {string | number | boolean} str
*/
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
/**
* @param {string} str
*/
function presenceDecode(str) {
return decodeURIComponent(
str.replace(/[_A-Z]/g, function(/** @type {string | number} */m) {
return j[m];
})
);
}
/**
* @param {string} userID
*/
function generatePresence(userID) {
var 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() {
var 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} */
var sectionLength = Date.now();
/** @type {string} */
var id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
/** @type {number} */
var r = Math.floor((sectionLength + Math.random() * 16) % 16);
/** @type {number} */
sectionLength = Math.floor(sectionLength / 16);
/** @type {string} */
var _guid = (c == "x" ? r : (r & 7) | 8).toString(16);
return _guid;
});
return id;
}
/**
* @param {{ mercury: any; blob_attachment: any; attach_type: any; sticker_attachment: any; extensible_attachment: { story_attachment: { target: { __typename: string; }; }; }; metadata: { stickerID: { toString: () => any; }; packID: { toString: () => any; }; spriteURI: any; spriteURI2x: any; width: any; height: any; frameCount: any; frameRate: any; framesPerRow: any; framesPerCol: any; fbid: { toString: () => any; }; url: any; dimensions: { split: (arg0: string) => any[]; width: any; height: any; }; duration: any; }; url: any; name: any; fileName: any; thumbnail_url: any; preview_url: any; preview_width: any; preview_height: any; large_preview_url: any; large_preview_width: any; large_preview_height: any; share: { share_id: { toString: () => any; }; title: any; description: any; source: any; media: { image: any; image_size: { width: any; height: any; }; playable: any; duration: any; animated_image_size: any; }; subattachments: any; uri: any; target: any; style_list: any; }; }} attachment1
* @param {{ caption?: any; description?: any; id: any; is_malicious?: any; mime_type?: any; file_size?: any; filename?: any; image_data: any; href?: any; }} [attachment2]
*/
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.
attachment2 = attachment2 || { id: "", image_data: {} };
attachment1 = attachment1.mercury ? attachment1.mercury : attachment1;
var blob = attachment1.blob_attachment;
var 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",
filename: attachment1.name,
ID: attachment2.id.toString(),
url: attachment1.url,
isMalicious: attachment2.is_malicious,
contentType: attachment2.mime_type,
name: attachment1.name, // @Legacy
mimeType: attachment2.mime_type, // @Legacy
fileSize: attachment2.file_size // @Legacy
};
case "photo":
return {
type: "photo",
ID: attachment1.metadata.fbid.toString(),
filename: attachment1.fileName,
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: attachment1.fileName // @Legacy
};
case "animated_image":
return {
type: "animated_image",
ID: attachment2.id.toString(),
filename: attachment2.filename,
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
mimeType: attachment2.mime_type, // @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,
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,
duration: attachment1.metadata.duration,
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,
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,
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",
filename: blob.filename,
ID: blob.legacy_attachment_id,
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,
duration: blob.playable_duration_in_ms,
videoType: blob.video_type.toLowerCase(),
thumbnailUrl: blob.large_image.uri // @Legacy
};
case "MessageAudio":
return {
type: "audio",
filename: blob.filename,
ID: blob.url_shimhash,
audioType: blob.audio_type,
duration: blob.playable_duration_in_ms,
url: blob.playable_url,
isVoiceMail: blob.is_voicemail
};
case "StickerAttachment":
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(/** @type {{ [x: string]: any; }} */obj, /** @type {{ key: string | number; value: { text: any; }; }} */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",
filename: blob.filename,
ID: blob.message_file_fbid,
url: blob.url,
isMalicious: blob.is_malicious,
contentType: blob.content_type,
name: blob.filename,
mimeType: "",
fileSize: -1
};
default:
throw new Error(
"unrecognized attach_file of type " +
type +
"`" +
JSON.stringify(attachment1, null, 4) +
" attachment2: " +
JSON.stringify(attachment2, null, 4) +
"`"
);
}
}
/**
* @param {any[]} attachments
* @param {{ [x: string]: string | number; }} attachmentIds
* @param {{ [x: string]: any; }} attachmentMap
* @param {any} shareMap
*/
function formatAttachment(attachments, attachmentIds, attachmentMap, shareMap) {
attachmentMap = shareMap || attachmentMap;
return attachments ?
attachments.map(function(/** @type {any} */val, /** @type {string | number} */i) {
if (!attachmentMap ||
!attachmentIds ||
!attachmentMap[attachmentIds[i]]
) {
return _formatAttachment(val);
}
return _formatAttachment(val, attachmentMap[attachmentIds[i]]);
}) : [];
}
/**
* @param {{ delta: { messageMetadata: any; data: { prng: string; }; body: string; attachments: any; participants: any; }; }} m
*/
function formatDeltaMessage(m) {
var md = m.delta.messageMetadata;
var mdata =
m.delta.data === undefined ? [] :
m.delta.data.prng === undefined ? [] :
JSON.parse(m.delta.data.prng);
var m_id = mdata.map((/** @type {{ i: any; }} */u) => u.i);
var m_offset = mdata.map((/** @type {{ o: any; }} */u) => u.o);
var m_length = mdata.map((/** @type {{ l: any; }} */u) => u.l);
var mentions = {};
var body = m.delta.body || "";
var args = body == "" ? [] : body.trim().split(/\s+/);
for (var 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()),
threadID: formatID((md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()),
messageID: md.messageId,
args: args,
body: body,
attachments: (m.delta.attachments || []).map((/** @type {any} */v) => _formatAttachment(v)),
mentions: mentions,
timestamp: md.timestamp,
isGroup: !!md.threadKey.threadFbId,
participantIDs: m.delta.participants || []
};
}
/**
* @param {string} id
*/
function formatID(id) {
if (id != undefined && id != null) return id.replace(/(fb)?id[:.]/, "");
else return id;
}
/**
* @param {{ message: any; type: string; realtime_viewer_fbid: { toString: () => any; }; }} m
*/
function formatMessage(m) {
var originalMessage = m.message ? m.message : m;
var 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(/** @type {{ toString: () => any; }} */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;
}
/**
* @param {{ message: any; }} m
*/
function formatEvent(m) {
var originalMessage = m.message ? m.message : m;
var logMessageType = originalMessage.log_message_type;
var 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
});
}
/**
* @param {{ action_type: any; }} m
*/
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
/**
* @param {{ type: any; }} m
*/
function getAdminTextMessageType(m) {
switch (m.type) {
case "joinable_group_link_mode_change":
return "log:link-status";
case "magic_words":
return "log:magic-words";
case "change_thread_theme":
return "log:thread-color";
case "change_thread_icon":
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";
case "pin_messages_v2":
return "log:thread-pinned";
}
}
/**
* @param {string} name
*/
function getGenderByPhysicalMethod(name) {
var GirlName = ["LAN", "HÂN", "LINH", "MAI", "HOA", "THU", "BĂNG", "MỸ", "CHÂU", "THẢO", "THOA", "MẪN", "THÙY", "THỦY", "NGA", "NGÂN", "NGHI", "THƯ", "NGỌC", "BÍCH", "VÂN", "DIỆP", "CHI", "TIÊN", "XUÂN", "GIANG", "NHUNG", "DUNG", "NHƯ", "YẾN", "QUYÊN", "YẾN", "TƯỜNG", "VY", "PHƯƠNG", "LIÊN", "LAN", "HÀ", "MAI", "ĐAN", "HẠ", "QUYÊN", "LY", "HÒA", "OANH", "HƯƠNG", "HẰNG", "QUỲNH", "HẠNH", "NHIÊN", "NHẠN"];
var BoyName = ["HƯNG", "HUY", "KHẢI", "KHANG", "KHOA", "KHÔI", "KIÊN", "KIỆT", "LONG", "MINH", "ÂN", "BẢO", "BÌNH", "CƯỜNG", "ĐẠT", "ĐỨC", "DŨNG", "DUY", "HOÀNG", "HÙNG", "HƯNG", "NGHĨA", "NGUYÊN", "THẮNG", "THIỆN", "THỊNH", "TÒA", "TRIẾT", "TRUNG", "TRƯỜNG", "TUẤN", "NHÂN", "VŨ", "VINH", "PHONG", "PHÚC", "QUÂN", "QUANG", "SƠN", "TÀI", "THẮNG", "ĐĂNG", "VĂN", "VĨ", "QUANG", "MẠNH"];
var OtherName = ["ANH", "THANH", "TÂM", "DƯƠNG", "AN", "LÂM", "MIÊN", "TÚ", "LÂM", "BẰNG", "KHÁNH", "NHẬT", "VỸ", ".",",","/","%", "&","*","-","+"];
try {
var NameArray = name.split(" ");
name = NameArray[NameArray.length - 1];
var Name;
if (name == " " || name == null) return "UNKNOWN";
switch (GirlName.includes(name.toUpperCase())) {
case true: {
if (!OtherName.includes(name.toUpperCase()) && !BoyName.includes(name.toUpperCase())) Name = "FEMALE";
else Name = ['FEMALE','MALE'][Math.floor(Math.random() * 2)]; // just temp 🌚
}
break;
case false: {
if (!OtherName.includes(name.toUpperCase()) && !GirlName.includes(name.toUpperCase())) Name = "MALE";
else Name = ['FEMALE','MALE'][Math.floor(Math.random() * 2)]; // just temp 🌚
}
break;
}
}
catch (e) {
return "UNKNOWN";
}
return Name || "UNKNOWN";
}
/**
* @param {{ [x: string]: { [x: string]: { [x: string]: any; }; }; class: any; untypedData: any; name: any; addedParticipants: any; leftParticipantFbId: any; messageMetadata: { threadKey: { threadFbId: any; otherUserFbId: any; }; adminText: any; actorFbId: any; }; participants: any; }} m
*/
function formatDeltaEvent(m) {
var { updateData,getData,hasData } = require('./Extra/ExtraGetThread');
var logMessageType;
var logMessageData;
switch (m.class) {
case "AdminTextMessage":
logMessageType = getAdminTextMessageType(m);
logMessageData = m.untypedData;
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 "UserLocation": {
logMessageType = "log:user-location";
logMessageData = {
Image: m.attachments[0].mercury.extensible_attachment.story_attachment.media.image,
Location: m.attachments[0].mercury.extensible_attachment.story_attachment.target.location_title,
coordinates: m.attachments[0].mercury.extensible_attachment.story_attachment.target.coordinate,
url: m.attachments[0].mercury.extensible_attachment.story_attachment.url
};
}
}
switch (hasData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()))) {
case true: {
switch (logMessageType) {
case "log:thread-color": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
x.emoji = (logMessageData.theme_emoji || x.emoji);
x.color = (logMessageData['theme_color'] || x.color);
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:thread-icon": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
x.emoji = (logMessageData['thread_icon'] || x.emoji);
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:user-nickname": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
x.nicknames[logMessageData.participant_id] = (logMessageData.nickname.length == 0 ? x.userInfo.find(i => i.id == String(logMessageData.participant_id)).name : logMessageData.nickname);
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:thread-admins": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
switch (logMessageData.ADMIN_EVENT) {
case "add_admin": {
x.adminIDs.push({ id: logMessageData.TARGET_ID });
}
break;
case "remove_admin": {
x.adminIDs = x.adminIDs.filter(item => item.id != logMessageData.TARGET_ID);
}
break;
}
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:thread-approval-mode": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
if (x.approvalMode == true) {
x.approvalMode = false;
}
else {
x.approvalMode = true;
}
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:thread-name": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
x.threadName = (logMessageData.name || formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:subscribe": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
for (let o of logMessageData.addedParticipants) {
if (x.userInfo.some(i => i.id == o.userFbId)) continue;
else {
x.userInfo.push({
id: o.userFbId,
name: o.fullName,
gender: getGenderByPhysicalMethod(o.fullName)
});
x.participantIDs.push(o.userFbId);
}
}
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
case "log:unsubscribe": {
let x = getData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()));
x.participantIDs = x.participantIDs.filter(item => item != logMessageData.leftParticipantFbId);
x.userInfo = x.userInfo.filter(item => item.id != logMessageData.leftParticipantFbId);
if (x.adminIDs.some(i => i.id == logMessageData.leftParticipantFbId)) {
x.adminIDs = x.adminIDs.filter(item => item.id != logMessageData.leftParticipantFbId);
}
updateData(formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),x);
}
break;
}
}
}
return {
type: "event",
threadID: formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),
logMessageType: logMessageType,
logMessageData: logMessageData,
logMessageBody: m.messageMetadata.adminText,
author: m.messageMetadata.actorFbId,
participantIDs: m.participants || []
};
}
/**
* @param {{ st: any; from: { toString: () => any; }; to: any; thread_fbid: any; hasOwnProperty: (arg0: string) => any; from_mobile: any; realtime_viewer_fbid: any; }} event
*/
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"
};
}
/**
* @param {{ threadKey: { otherUserFbId: any; threadFbId: any; }; actorFbId: any; actionTimestampMs: any; }} delta
*/
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"
};
}
/**
* @param {{ reader: { toString: () => any; }; time: any; thread_fbid: any; }} event
*/
function formatReadReceipt(event) {
return {
reader: event.reader.toString(),
time: event.time,
threadID: formatID((event.thread_fbid || event.reader).toString()),
type: "read_receipt"
};
}
/**
* @param {{ chat_ids: any[]; thread_fbids: any[]; timestamp: any; }} event
*/
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"
};
}
/**
* @param {string} str
* @param {string | any[]} startToken
* @param {string} endToken
*/
function getFrom(str, startToken, endToken) {
var start = str.indexOf(startToken) + startToken.length;
if (start < startToken.length) return "";
var lastHalf = str.substring(start);
var end = lastHalf.indexOf(endToken);
if (end === -1) throw Error("Could not find endTime `" + endToken + "` in the given string.");
return lastHalf.substring(0, end);
}
/**
* @param {string} html
*/
function makeParsable(html) {
let 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
let maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/);
if (maybeMultipleObjects.length === 1) return maybeMultipleObjects;
return "[" + maybeMultipleObjects.join("},{") + "]";
}
/**
* @param {any} form
*/
function arrToForm(form) {
return arrayToObject(form,
function(/** @type {{ name: any; }} */v) {
return v.name;
},
function(/** @type {{ val: any; }} */v) {
return v.val;
}
);
}
/**
* @param {any[]} arr
* @param {{ (v: any): any; (arg0: any): string | number; }} getKey
* @param {{ (v: any): any; (arg0: any): any; }} getValue
*/
function arrayToObject(arr, getKey, getValue) {
return arr.reduce(function(/** @type {{ [x: string]: any; }} */
acc, /** @type {any} */val) {
acc[getKey(val)] = getValue(val);
return acc;
}, {});
}
function getSignatureID() {
return Math.floor(Math.random() * 2147483648).toString(16);
}
function generateTimestampRelative() {
var d = new Date();
return d.getHours() + ":" + padZeros(d.getMinutes());
}
/**
* @param {any} html
* @param {any} userID
* @param {{ fb_dtsg: any; ttstamp: any; globalOptions: any; }} ctx
*/
function makeDefaults(html, userID, ctx) {
var reqCounter = 1;
var fb_dtsg = getFrom(html, 'name="fb_dtsg" value="', '"');
// @Hack Ok we've done hacky things, this is definitely on top 5.
// We totally assume the object is flat and try parsing until a }.
// If it works though it's cool because we get a bunch of extra data things.
//
// Update: we don't need this. Leaving it in in case we ever do.
// Ben - July 15th 2017
// var siteData = getFrom(html, "[\"SiteData\",[],", "},");
// try {
// siteData = JSON.parse(siteData + "}");
// } catch(e) {
// log.warn("makeDefaults", "Couldn't parse SiteData. Won't have access to some variables.");
// siteData = {};
// }
var ttstamp = "2";
for (var i = 0; i < fb_dtsg.length; i++) ttstamp += fb_dtsg.charCodeAt(i);
var revision = getFrom(html, 'revision":', ",");
/**
* @param {{ [x: string]: any; hasOwnProperty: (arg0: string) => any; }} obj
*/
function mergeWithDefaults(obj) {
// @TODO This is missing a key called __dyn.
// After some investigation it seems like __dyn is some sort of set that FB
// calls BitMap. It seems like certain responses have a "define" key in the
// res.jsmods arrays. I think the code iterates over those and calls `set`
// on the bitmap for each of those keys. Then it calls
// bitmap.toCompressedString() which returns what __dyn is.
//
// So far the API has been working without this.
//
// Ben - July 15th 2017
var newObj = {
__user: userID,
__req: (reqCounter++).toString(36),
__rev: revision,
__a: 1,
// __af: siteData.features,
fb_dtsg: ctx.fb_dtsg ? ctx.fb_dtsg : fb_dtsg,
jazoest: ctx.ttstamp ? ctx.ttstamp : ttstamp
// __spin_r: siteData.__spin_r,
// __spin_b: siteData.__spin_b,
// __spin_t: siteData.__spin_t,
};
// @TODO this is probably not needed.
// Ben - July 15th 2017
// if (siteData.be_key) {
// newObj[siteData.be_key] = siteData.be_mode;
// }
// if (siteData.pkg_cohort_key) {
// newObj[siteData.pkg_cohort_key] = siteData.pkg_cohort;
// }
if (!obj) return newObj;
for (var prop in obj)
if (obj.hasOwnProperty(prop))
if (!newObj[prop]) newObj[prop] = obj[prop];
return newObj;
}
/**
* @param {any} url
* @param {any} jar
* @param {any} form
* @param {any} ctxx
*/
function postWithDefaults(url, jar, form, ctxx) {
return post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx);
}
/**
* @param {any} url
* @param {any} jar
* @param {any} qs
* @param {any} ctxx
*/
function getWithDefaults(url, jar, qs, ctxx) {
return get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx);
}
/**
* @param {any} url
* @param {any} jar
* @param {any} form
* @param {any} qs
* @param {any} ctxx
*/
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
};
}
/**
* @param {{ jar: { setCookie: (arg0: string, arg1: string) => void; }; fb_dtsg: string; ttstamp: string; }} ctx
* @param {{ postFormData: (arg0: string, arg1: any, arg2: any, arg3: {}) => any; post: (arg0: string, arg1: any, arg2: any) => any; get: (arg0: any, arg1: any) => Promise<any>; }} defaultFuncs
* @param {string | number} [retryCount]
*/
function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
if (retryCount == undefined) retryCount = 0;
return function(/** @type {{ body: string; statusCode: string | number; request: { uri: { protocol: string; hostname: string; pathname: string; }; headers: { [x: string]: string; }; formData: any; method: string; }; }} */data) {
return bluebird.try(function() {
log.verbose("parseAndCheckLogin", data.body);
if (data.statusCode >= 500 && data.statusCode < 600) {
if (retryCount >= 5) {
throw {
error: "Request retry failed. Check the `res` and `statusCode` property on this error.",
statusCode: data.statusCode,
res: data.body
};
}
retryCount++;
var retryTime = Math.floor(Math.random() * 5000);
log.warn("parseAndCheckLogin", "Got status code " + data.statusCode + " - " + retryCount + ". attempt to retry in " + retryTime + " milliseconds...");
var url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.reques