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 = /]+>/; 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); } } }