dunbot / source /apis /lib /Client.js
Duongkum999's picture
Upload 56 files
91d9d20 verified
var utils = require("../utils");
var mqtt = require("mqtt");
var websocket = require("websocket-stream");
var HttpsProxyAgent = require("https-proxy-agent");
var EventEmitter = require("events");
var topics = [
"/legacy_web",
"/webrtc",
"/rtc_multi",
"/onevc",
"/br_sr",
"/sr_res",
"/t_ms",
"/thread_typing",
"/orca_typing_notifications",
"/notify_disconnect",
"/orca_presence",
"/legacy_web_mtouch",
"/t_rtc_multi",
"/ls_foreground_state",
"/ls_resp",
"/inbox",
"/mercury",
"/messaging_events",
"/orca_message_notifications",
"/pp",
"/webrtc_response"
];
function notificationConnect(ctx) {
var next = true;
return utils
.get("https://www.facebook.com/notifications", ctx.jar)
.catch(function (error) {
if (error.type === "logout.")
ctx.isLogin = false;
next = false;
console.log(error);
})
.finally(function () {
if (next && ctx.listenNotif)
return setTimeout(notificationConnect, 1000, ctx);
});
}
function markDelivery(apis, threadID, messageID) {
if (threadID && messageID)
apis.markAsDelivered(threadID, messageID, error => !error ? apis.markAsRead(threadID) : null);
}
function parseAndReCallback(http, apis, ctx, globalCallback, deltails) {
function getExtension(original_extension, fullFileName = "") {
if (original_extension)
return original_extension;
else {
var extension = fullFileName.split(".").pop();
if (extension === fullFileName)
return "";
else
return extension;
}
}
function formatAttachment(attachment1, attachment2) {
var fullFileName = attachment1.filename;
var fileSize = Number(attachment1.fileSize || 0);
var durationVideo = attachment1.genericMetadata ? Number(attachment1.genericMetadata.videoLength) : undefined;
var durationAudio = attachment1.genericMetadata ? Number(attachment1.genericMetadata.duration) : undefined;
var mimeType = attachment1.mimeType;
attachment2 = attachment2 || { id: "", image_data: {} };
attachment1 = attachment1.mercury || attachment1;
var blob = attachment1.blob_attachment || attachment1.sticker_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;
}
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,
filename: attachment1.name,
fileSize,
original_extension: getExtension(attachment1.original_extension, fullFileName),
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,
fileSize,
original_extension: getExtension(attachment1.original_extension, fullFileName),
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,
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,
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,
fileSize,
original_extension: getExtension(blob.original_extension, fullFileName),
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,
original_extension: getExtension(blob.original_extension, fullFileName),
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,
original_extension: getExtension(blob.original_extension, fullFileName),
fileSize: fileSize,
duration: durationVideo,
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,
fileSize,
duration: durationAudio,
original_extension: getExtension(blob.original_extension, fullFileName),
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]);
} finally { }
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,
longitude,
image: imageUrl,
width,
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 : 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,
filename: blob.filename,
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) + "`");
}
}
if (deltails.class === "NewMessage") {
function formatMessage() {
var md = deltails.messageMetadata;
var mdata = !deltails.data ? [] : !deltails.data.prng ? [] : JSON.parse(deltails.data.prng);
var m_id = mdata.map(u => u.i);
var m_offset = mdata.map(u => u.o);
var m_length = mdata.map(u => u.l);
var mentions = {};
for (var i = 0; i < m_id.length; i++)
mentions[m_id[i]] = deltails.body.substring(m_offset[i], m_offset[i] + m_length[i]);
return {
type: "message",
senderID: utils.formatID(md.actorFbId.toString()),
body: deltails.body || "",
threadID: utils.formatID((md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()),
messageID: md.messageId,
attachments: (deltails.attachments || []).map(v => formatAttachment(v)),
mentions,
timestamp: md.timestamp,
isGroup: !!md.threadKey.threadFbId,
participantIDs: deltails.participants || []
}
}
(function resolveAttachmentUrl(i) {
if (i === (deltails.attachments || []).length) {
try {
var message = formatMessage();
(message.senderID !== ctx.userID || ctx.globalOptions.listenSelf) ? globalCallback(null, message) : null;
if (ctx.globalOptions.autoMarkDelivery)
markDelivery(apis, message.threadID, message.messageID);
} catch (error) {
error = {
error: "Problem parsing message object. Please open an issue at https://github.com/GiaKhang1810/mira-bot-v1/issues.",
detail: error,
response: deltails,
type: "parse_error"
}
globalCallback(error);
}
} else {
if (deltails.attachments[i].mercury.attach_type === "photo") {
apis.resolvePhotoUrl(deltails.attachments[i].fbid, (e, u) => e ? deltails.attachments[i].mercury.metadata.url = u : null, resolveAttachmentUrl(i + 1));
} else {
return resolveAttachmentUrl(i + 1);
}
}
})(0);
}
if (deltails.class === "ClientPayload") {
var ClientPayload = JSON.parse(String.fromCharCode.apply(null, deltails.payload));
if (ClientPayload && ClientPayload.deltas) {
for (var i in ClientPayload.deltas) {
var delta = ClientPayload.deltas[i];
if (delta.deltaMessageReaction && ctx.globalOptions.listenEvents) {
var reaction = {
type: "message_reaction",
threadID: (delta.deltaMessageReaction.threadKey.threadFbId ? delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey.otherUserFbId).toString(),
messageID: delta.deltaMessageReaction.messageId,
reaction: delta.deltaMessageReaction.reaction,
senderID: delta.deltaMessageReaction.senderId.toString(),
userID: (delta.deltaMessageReaction.userId || delta.deltaMessageReaction.senderId).toString()
}
globalCallback(null, reaction);
} else if (delta.deltaRecallMessageData && ctx.globalOptions.listenEvents) {
var unsend = {
type: "message_unsend",
threadID: (delta.deltaRecallMessageData.threadKey.threadFbId ? delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey.otherUserFbId).toString(),
messageID: delta.deltaRecallMessageData.messageID,
senderID: delta.deltaRecallMessageData.senderID.toString(),
deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
timestamp: delta.deltaRecallMessageData.timestamp
}
globalCallback(null, unsend);
} else if (delta.deltaRemoveMessage && ctx.globalOptions.listenEvents) {
var del = {
type: "message_self_delete",
threadID: (delta.deltaRemoveMessage.threadKey.threadFbId ? delta.deltaRemoveMessage.threadKey.threadFbId : delta.deltaRemoveMessage.threadKey.otherUserFbId).toString(),
messageID: delta.deltaRemoveMessage.messageIds.length === 1 ? delta.deltaRemoveMessage.messageIds[0] : delta.deltaRemoveMessage.messageIds,
senderID: ctx.userID,
deletionTimestamp: delta.deltaRemoveMessage.deletionTimestamp,
timestamp: delta.deltaRemoveMessage.timestamp
}
globalCallback(null, del);
} else if (delta.deltaMessageReply) {
var mdata = !delta.deltaMessageReply.message ? [] : !delta.deltaMessageReply.message.data ? [] : !delta.deltaMessageReply.message.data.prng ? [] : JSON.parse(delta.deltaMessageReply.message.data.prng);
var m_id = mdata.map(u => u.i);
var m_offset = mdata.map(u => u.o);
var m_length = mdata.map(u => u.l);
var mentions = {}
for (var i = 0; i < m_id.length; i++)
mentions[m_id[i]] = (delta.deltaMessageReply.message.body || "").substring(m_offset[i], m_offset[i] + m_length[i]);
var callbackToReturn = {
type: "message_reply",
threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ? delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey.otherUserFbId).toString(),
messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
attachments: (delta.deltaMessageReply.message.attachments || []).map(att => {
var mercury = JSON.parse(att.mercuryJSON);
Object.assign(att, mercury);
return att;
}).map(att => {
var x;
try {
x = formatAttachment(att);
} catch (ex) {
x = att;
x.error = ex;
x.type = "unknown";
}
return x;
}),
body: delta.deltaMessageReply.message.body || "",
isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
mentions,
timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
participantIDs: (delta.deltaMessageReply.message.messageMetadata.cid.canonicalParticipantFbids || delta.deltaMessageReply.message.participants || []).map(e => e.toString())
}
if (delta.deltaMessageReply.repliedToMessage) {
mdata = !delta.deltaMessageReply.repliedToMessage ? [] : !delta.deltaMessageReply.repliedToMessage.data ? [] : !delta.deltaMessageReply.repliedToMessage.data.prng ? [] : JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
m_id = mdata.map(u => u.i);
m_offset = mdata.map(u => u.o);
m_length = mdata.map(u => u.l);
var rmentions = {}
for (var i = 0; i < m_id.length; i++)
rmentions[m_id[i]] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(m_offset[i], m_offset[i] + m_length[i]);
callbackToReturn.messageReply = {
threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ? delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.otherUserFbId).toString(),
messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(att => {
var mercury = JSON.parse(att.mercuryJSON);
Object.assign(att, mercury);
return att;
}).map(att => {
var x;
try {
x = formatAttachment(att);
} catch (ex) {
x = att;
x.error = ex;
x.type = "unknown";
}
return x;
}),
body: delta.deltaMessageReply.repliedToMessage.body || "",
isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
mentions: rmentions,
timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp
};
} else if (delta.deltaMessageReply.replyToMessageId) {
return http
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, {
queries: JSON.stringify({
o0: {
doc_id: "2848441488556444",
query_params: {
thread_and_message_id: {
thread_id: callbackToReturn.threadID,
message_id: delta.deltaMessageReply.replyToMessageId.id
}
}
}
})
})
.then(utils.parseAndCheckLogin(ctx, http))
.then(resData => {
if (resData[resData.length - 1].error_results > 0)
throw resData[0].o0.errors;
if (resData[resData.length - 1].successful_results === 0)
throw { error: "forcedFetch: there was no successful_results", response: resData };
var fetchData = resData[0].o0.data.message;
var mobj = {}
for (var n in fetchData.message.ranges)
mobj[fetchData.message.ranges[n].entity.id] = (fetchData.message.text || "").substr(fetchData.message.ranges[n].offset, fetchData.message.ranges[n].length);
callbackToReturn.messageReply = {
threadID: callbackToReturn.threadID,
messageID: fetchData.message_id,
senderID: fetchData.message_sender.id.toString(),
attachments: fetchData.message.blob_attachment.map(att => {
var x;
try {
x = formatAttachment({ blob_attachment: att });
} catch (ex) {
x = att;
x.error = ex;
x.type = "unknown";
}
return x;
}),
body: fetchData.message.text || "",
isGroup: callbackToReturn.isGroup,
mentions: mobj,
timestamp: parseInt(fetchData.timestamp_precise)
};
})
.catch(console.log)
.finally(function () {
if (ctx.globalOptions.autoMarkDelivery)
markDelivery(apis, callbackToReturn.threadID, callbackToReturn.messageID);
(callbackToReturn.senderID !== ctx.userID || ctx.globalOptions.listenSelf) ? globalCallback(null, callbackToReturn) : null;
});
} else
callbackToReturn.delta = delta;
if (ctx.globalOptions.autoMarkDelivery)
markDelivery(apis, callbackToReturn.threadID, callbackToReturn.messageID);
return (callbackToReturn.senderID !== ctx.userID || ctx.globalOptions.listenSelf) ? globalCallback(null, callbackToReturn) : null;
}
}
return;
}
}
if (deltails.class !== "NewMessage" && !ctx.globalOptions.listenEvents)
return;
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":
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() {
var logMessageType;
var logMessageData;
switch (deltails.class) {
case "AdminTextMessage":
logMessageData = deltails.untypedData;
logMessageType = getAdminTextMessageType(deltails.type);
break;
case "ThreadName":
logMessageType = "log:thread-name";
logMessageData = { name: deltails.name };
break;
case "ParticipantsAddedToGroupThread":
logMessageType = "log:subscribe";
logMessageData = { addedParticipants: deltails.addedParticipants };
break;
case "ParticipantLeftGroupThread":
logMessageType = "log:unsubscribe";
logMessageData = { leftParticipantFbId: deltails.leftParticipantFbId };
break;
case "ApprovalQueue":
logMessageType = "log:approval-queue";
logMessageData = {
approvalQueue: {
action: deltails.action,
recipientFbId: deltails.recipientFbId,
requestSource: deltails.requestSource,
...deltails.messageMetadata
}
}
}
return {
type: "event",
threadID: utils.formatID((deltails.messageMetadata.threadKey.threadFbId || deltails.messageMetadata.threadKey.otherUserFbId).toString()),
messageID: deltails.messageMetadata.messageId.toString(),
logMessageType: logMessageType,
logMessageData: logMessageData,
logMessageBody: deltails.messageMetadata.adminText,
timestamp: deltails.messageMetadata.timestamp,
author: deltails.messageMetadata.actorFbId,
participantIDs: deltails.participants
}
}
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":
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() {
var logMessageType;
var logMessageData;
switch (deltails.class) {
case "AdminTextMessage":
logMessageData = deltails.untypedData;
logMessageType = getAdminTextMessageType(deltails.type);
break;
case "ThreadName":
logMessageType = "log:thread-name";
logMessageData = { name: deltails.name };
break;
case "ParticipantsAddedToGroupThread":
logMessageType = "log:subscribe";
logMessageData = { addedParticipants: deltails.addedParticipants };
break;
case "ParticipantLeftGroupThread":
logMessageType = "log:unsubscribe";
logMessageData = { leftParticipantFbId: deltails.leftParticipantFbId };
break;
case "ApprovalQueue":
logMessageType = "log:approval-queue";
logMessageData = {
approvalQueue: {
action: deltails.action,
recipientFbId: deltails.recipientFbId,
requestSource: deltails.requestSource,
...deltails.messageMetadata
}
}
}
return {
type: "event",
threadID: utils.formatID((deltails.messageMetadata.threadKey.threadFbId || deltails.messageMetadata.threadKey.otherUserFbId).toString()),
messageID: deltails.messageMetadata.messageId.toString(),
logMessageType: logMessageType,
logMessageData: logMessageData,
logMessageBody: deltails.messageMetadata.adminText,
timestamp: deltails.messageMetadata.timestamp,
author: deltails.messageMetadata.actorFbId,
participantIDs: deltails.participants
}
}
switch (deltails.class) {
case "ReadReceipt":
try {
var readReceipt = {
reader: (deltails.threadKey.otherUserFbId || deltails.actorFbId).toString(),
time: deltails.actionTimestampMs,
threadID: utils.formatID((deltails.threadKey.otherUserFbId || deltails.threadKey.threadFbId).toString()),
type: "read_receipt"
}
globalCallback(null, readReceipt);
} catch (error) {
error = {
error: "Problem parsing message object. Please open an issue at https://github.com/GiaKhang1810/mira-bot-v1/issues.",
detail: error,
response: deltails,
type: "parse_error"
}
globalCallback(error);
}
break;
case "AdminTextMessage":
switch (deltails.type) {
case "change_thread_theme":
case "change_thread_nickname":
case "change_thread_icon":
case "change_thread_quick_reaction":
case "change_thread_admins":
case "group_poll":
case "joinable_group_link_mode_change":
case "magic_words":
case "change_thread_approval_mode":
case "messenger_call_log":
case "participant_joined_group_call":
try {
var detailsEvent = formatDeltaEvent();
globalCallback(null, detailsEvent);
} catch (error) {
error = {
error: "Problem parsing message object. Please open an issue at https://github.com/GiaKhang1810/mira-bot-v1/issues.",
detail: error,
response: deltails,
type: "parse_error"
}
}
break;
default:
break;
}
break;
case "ForcedFetch":
if (!deltails.threadKey)
return;
var mid = deltails.messageId;
var tid = deltails.threadKey.threadFbId;
if (mid && tid) {
var form = {
queries: JSON.stringify({
o0: {
doc_id: "2848441488556444",
query_params: {
thread_and_message_id: {
thread_id: tid.toString(),
message_id: mid
}
}
}
})
}
http
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
.then(utils.parseAndCheckLogin(ctx, http))
.then(resData => {
if (resData[resData.length - 1].error_results > 0)
throw resData[0].o0.errors;
if (resData[resData.length - 1].successful_results === 0)
throw { error: "forcedFetch: there was no successful_results", response: resData }
var fetchData = resData[0].o0.data.message;
if (utils.getType(fetchData) !== "Object")
return;
switch (fetchData.__typename) {
case "ThreadImageMessage":
(fetchData.message_sender.id.toString() !== ctx.userID || ctx.globalOptions.listenEventsSelf) ? globalCallback(null, {
type: "event",
threadID: utils.formatID(tid.toString()),
messageID: fetchData.message_id,
logMessageType: "log:thread-image",
logMessageData: {
attachmentID: fetchData.image_with_metadata && fetchData.image_with_metadata.legacy_attachment_id,
width: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.x,
height: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.y,
url: fetchData.image_with_metadata && fetchData.image_with_metadata.preview.uri
},
logMessageBody: fetchData.snippet,
timestamp: fetchData.timestamp_precise,
author: fetchData.message_sender.id
}) : null;
break;
case "UserMessage":
globalCallback(null, {
type: "message",
senderID: utils.formatID(fetchData.message_sender.id),
body: fetchData.message.text || "",
threadID: utils.formatID(tid.toString()),
messageID: fetchData.message_id,
attachments: [{
type: "share",
ID: fetchData.extensible_attachment.legacy_attachment_id,
url: fetchData.extensible_attachment.story_attachment.url,
title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
description: fetchData.extensible_attachment.story_attachment.description.text,
source: fetchData.extensible_attachment.story_attachment.source,
image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
playable: (fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false,
duration: (fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
subattachments: fetchData.extensible_attachment.subattachments,
properties: fetchData.extensible_attachment.story_attachment.properties
}],
mentions: {},
timestamp: parseInt(fetchData.timestamp_precise),
participantIDs: (fetchData.participants || (fetchData.messageMetadata ? fetchData.messageMetadata.cid ? fetchData.messageMetadata.cid.canonicalParticipantFbids : fetchData.messageMetadata.participantIds : []) || []),
isGroup: (fetchData.message_sender.id != tid.toString())
});
break;
}
})
.catch(console.log);
}
break;
case "ThreadName":
case "ParticipantsAddedToGroupThread":
case "ParticipantLeftGroupThread":
case "ApprovalQueue":
try {
var detailsEvent = formatDeltaEvent();
globalCallback(null, detailsEvent);
} catch (error) {
error = {
error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
detail: error,
response: deltails,
type: "parse_error"
}
globalCallback(error);
}
break;
}
}
function connectClientWs(http, apis, ctx, globalCallback) {
var chatOn = ctx.globalOptions.online;
var foreground = false;
var sessionID = Math.floor(Math.random() * 9007199254740991) + 1;
var username = JSON.stringify({
u: ctx.userID,
s: sessionID,
chat_on: chatOn,
fg: foreground,
d: utils.getGUID(),
ct: "websocket",
aid: "219994525426954",
mqtt_sid: "",
cp: 3,
ecp: 10,
st: [],
pm: [],
dc: "",
no_auto_fg: true,
gas: null,
pack: [],
a: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Kbody, like Gecko) Chrome/127.0.0.0 Safari/537.36",
aids: null
});
var host = ctx.endpoint ? ctx.endpoint + "&sid=" + sessionID : ctx.region ? "wss://edge-chat.facebook.com/chat?region=" + ctx.region.toLocaleLowerCase() + "&sid=" + sessionID : "wss://edge-chat.facebook.com/chat?sid=" + sessionID;
var options = {
clientId: "mqttwsclient",
protocolId: "MQIsdp",
protocolVersion: 3,
username,
clean: true,
wsOptions: {
headers: {
"Cookie": ctx.jar.getCookies("https://www.facebook.com").join("; "),
"Origin": "https://www.facebook.com",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Kbody, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Referer": "https://www.facebook.com/",
"Host": new URL(host).hostname
},
origin: "https://www.facebook.com",
protocolVersion: 13
},
keepalive: 10,
reschedulePings: true
}
if (ctx.proxy) {
var agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
options.wsOptions.agent = agent;
}
ctx.Client = new mqtt.Client(_ => websocket(host, options.wsOptions), options);
var Client = ctx.Client;
Client
.on("error", function (error) {
if (error.message === "Invalid header flag bits, must be 0x0 for puback packet")
return;
if (ctx.Client)
ctx.Client.end(false, _ => ctx.Client = null);
if (ctx.globalOptions.autoReconnect) {
return getSeqID(http, apis, ctx, globalCallback);
}
error = {
type: "disconnect",
message: "Connection refused: Server unavailable",
error
}
globalCallback(error);
})
.on("close", _ => { })
.on("connect", function () {
topics.map(topic => Client.subscribe(topic));
var topic;
var queue = {
sync_api_version: 10,
max_deltas_able_to_process: 1000,
delta_batch_size: 500,
encoding: "JSON",
entity_fbid: ctx.userID
}
if (ctx.syncToken) {
topic = "/messenger_sync_get_diffs";
queue.last_seq_id = ctx.lastSeqID;
queue.sync_token = ctx.syncToken;
} else {
topic = "/messenger_sync_create_queue";
queue.initial_titan_sequence_id = ctx.lastSeqID;
queue.device_params = null;
}
Client.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
Client.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
Client.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
ctx.listenNotif ? notificationConnect(ctx) : null;
})
.on("message", function (topic, message) {
var Message = JSON.parse(Buffer.from(message).toString());
if (Message.type === "jewel_requests_add") {
globalCallback(null, {
type: "friend_request_received",
actorFbId: Message.frodeltails.toString(),
timestamp: Date.now().toString()
});
}
else if (Message.type === "jewel_requests_remove_old") {
globalCallback(null, {
type: "friend_request_cancel",
actorFbId: Message.frodeltails.toString(),
timestamp: Date.now().toString()
});
}
else if (topic === "/t_ms") {
if (Message.firstDeltaSeqId && Message.syncToken) {
ctx.lastSeqID = Message.firstDeltaSeqId;
ctx.syncToken = Message.syncToken;
}
if (Message.lastIssuedSeqId) {
ctx.lastSeqID = parseInt(Message.lastIssuedSeqId);
}
for (var i in Message.deltas) {
var deltails = Message.deltas[i];
parseAndReCallback(http, apis, ctx, globalCallback, deltails);
}
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
var typ = {
type: "typ",
isTyping: !!Message.state,
from: Message.sender_fbid.toString(),
threadID: utils.formatID((Message.thread || Message.sender_fbid).toString())
};
globalCallback(null, typ);
} else if (topic === "/orca_presence") {
if (!ctx.globalOptions.updatePresence) {
for (var i in Message.list) {
var data = Message.list[i];
var userID = data["u"];
var presence = {
type: "presence",
userID: userID.toString(),
timestamp: data["l"] * 1000,
statuses: data["p"]
}
globalCallback(null, presence);
}
}
} else if (Message.type === "notifications_seen") {
var notif = {
type: "notification",
alertIDs: Message.alert_ids,
graphQLIDs: Message.graphql_ids,
notiGraphQLIDs: Message.notif_graphql_ids,
timestamp: Date.now().toString()
}
globalCallback(null, notif);
} else {
console.log(topic, Message);
}
});
}
function getSeqID(http, apis, ctx, globalCallback) {
var headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18"
}
utils
.get("https://www.facebook.com/", ctx.jar, null, null, headers)
.then(function (res) {
var reg = /<meta http-equiv="refresh" content="0;url=([^"]+)[^>]+>/;
var redirect = reg.exec(res.body);
if (redirect && redirect[1]) {
delete headers.noRef;
return utils
.get(redirect[1], ctx.jar, null, null, headers);
}
return res;
})
.then(function (res) {
var seqRegex = /irisSeqID:"(\d+)"/.exec(res.body);
if (seqRegex && seqRegex[1]) {
ctx.lastSeqID = seqRegex[1];
connectClientWs(http, apis, ctx, globalCallback);
} else {
var error = new Error("seqID is undefined.");
error.type = "logout.";
throw error;
}
})
.catch(function (error) {
if (error.type === "logout.")
ctx.isLogin = false;
console.log(error);
return globalCallback(error);
});
}
module.exports = function (http, apis, ctx) {
var globalCallback;
return class Client extends EventEmitter {
constructor() {
super();
globalCallback = (error, message) => error ? this.emit("error", error) : this.emit("message", message);
getSeqID(http, apis, ctx, globalCallback);
return this;
}
disconnect() {
globalCallback = () => { }
if (ctx.Client)
ctx.Client.end(false, _ => ctx.Client = null);
}
}
}