UNPKG

aminul-remake-fca

Version:

Aminul's remake of ws3-fca — next-generation Facebook Chat API fork

681 lines (627 loc) 22.2 kB
"use strict"; const utils = require("../utils"); // @NethWs3Dev function getExtension(original_extension, filename = "") { if (original_extension) { return original_extension; } else { const extension = filename.split(".").pop(); if (extension === filename) { return ""; } else { return extension; } } } function formatAttachmentsGraphQLResponse(attachment) { switch (attachment.__typename) { case "MessageImage": return { type: "photo", ID: attachment.legacy_attachment_id, filename: attachment.filename, original_extension: getExtension( attachment.original_extension, attachment.filename, ), thumbnailUrl: attachment.thumbnail.uri, previewUrl: attachment.preview.uri, previewWidth: attachment.preview.width, previewHeight: attachment.preview.height, largePreviewUrl: attachment.large_preview.uri, largePreviewHeight: attachment.large_preview.height, largePreviewWidth: attachment.large_preview.width, // You have to query for the real image. See below. url: attachment.large_preview.uri, // @Legacy width: attachment.large_preview.width, // @Legacy height: attachment.large_preview.height, // @Legacy name: attachment.filename, // @Legacy // @Undocumented attributionApp: attachment.attribution_app ? { attributionAppID: attachment.attribution_app.id, name: attachment.attribution_app.name, logo: attachment.attribution_app.square_logo, } : null, // @TODO No idea what this is, should we expose it? // Ben - July 15th 2017 // renderAsSticker: attachment.render_as_sticker, // This is _not_ the real URI, this is still just a large preview. // To get the URL we'll need to support a POST query to // // https://www.facebook.com/webgraphql/query/ // // With the following query params: // // query_id:728987990612546 // variables:{"id":"100009069356507","photoID":"10213724771692996"} // dpr:1 // // No special form though. }; case "MessageAnimatedImage": return { type: "animated_image", ID: attachment.legacy_attachment_id, filename: attachment.filename, original_extension: getExtension( attachment.original_extension, attachment.filename, ), previewUrl: attachment.preview_image.uri, previewWidth: attachment.preview_image.width, previewHeight: attachment.preview_image.height, url: attachment.animated_image.uri, width: attachment.animated_image.width, height: attachment.animated_image.height, thumbnailUrl: attachment.preview_image.uri, // @Legacy name: attachment.filename, // @Legacy facebookUrl: attachment.animated_image.uri, // @Legacy rawGifImage: attachment.animated_image.uri, // @Legacy animatedGifUrl: attachment.animated_image.uri, // @Legacy animatedGifPreviewUrl: attachment.preview_image.uri, // @Legacy animatedWebpUrl: attachment.animated_image.uri, // @Legacy animatedWebpPreviewUrl: attachment.preview_image.uri, // @Legacy // @Undocumented attributionApp: attachment.attribution_app ? { attributionAppID: attachment.attribution_app.id, name: attachment.attribution_app.name, logo: attachment.attribution_app.square_logo, } : null, }; case "MessageVideo": return { type: "video", ID: attachment.legacy_attachment_id, filename: attachment.filename, original_extension: getExtension( attachment.original_extension, attachment.filename, ), duration: attachment.playable_duration_in_ms, thumbnailUrl: attachment.large_image.uri, // @Legacy previewUrl: attachment.large_image.uri, previewWidth: attachment.large_image.width, previewHeight: attachment.large_image.height, url: attachment.playable_url, width: attachment.original_dimensions.x, height: attachment.original_dimensions.y, videoType: attachment.video_type.toLowerCase(), }; case "MessageFile": return { type: "file", ID: attachment.message_file_fbid, filename: attachment.filename, original_extension: getExtension( attachment.original_extension, attachment.filename, ), url: attachment.url, isMalicious: attachment.is_malicious, contentType: attachment.content_type, name: attachment.filename, // @Legacy mimeType: "", // @Legacy fileSize: -1, // @Legacy }; case "MessageAudio": return { type: "audio", ID: attachment.url_shimhash, // Not fowardable filename: attachment.filename, original_extension: getExtension( attachment.original_extension, attachment.filename, ), duration: attachment.playable_duration_in_ms, audioType: attachment.audio_type, url: attachment.playable_url, isVoiceMail: attachment.is_voicemail, }; default: return { error: "Don't know about attachment type " + attachment.__typename, }; } } function formatExtensibleAttachment(attachment) { if (attachment.story_attachment) { return { type: "share", ID: attachment.legacy_attachment_id, url: attachment.story_attachment.url, title: attachment.story_attachment.title_with_entities.text, description: attachment.story_attachment.description && attachment.story_attachment.description.text, source: attachment.story_attachment.source == null ? null : attachment.story_attachment.source.text, image: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).uri, width: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).width, height: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).height, playable: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.is_playable, duration: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.playable_duration_in_ms, playableUrl: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.playable_url, subattachments: attachment.story_attachment.subattachments, // Format example: // // [{ // key: "width", // value: { text: "1280" } // }] // // That we turn into: // // { // width: "1280" // } // properties: attachment.story_attachment.properties.reduce(function ( obj, cur, ) { obj[cur.key] = cur.value.text; return obj; }, {}), // Deprecated fields animatedImageSize: "", // @Legacy facebookUrl: "", // @Legacy styleList: "", // @Legacy target: "", // @Legacy thumbnailUrl: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).uri, // @Legacy thumbnailWidth: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).width, // @Legacy thumbnailHeight: attachment.story_attachment.media == null ? null : attachment.story_attachment.media.animated_image == null && attachment.story_attachment.media.image == null ? null : ( attachment.story_attachment.media.animated_image || attachment.story_attachment.media.image ).height, // @Legacy }; } else { return { error: "Don't know what to do with extensible_attachment." }; } } function formatReactionsGraphQL(reaction) { return { reaction: reaction.reaction, userID: reaction.user.id, }; } function formatEventData(event) { if (event == null) { return {}; } switch (event.__typename) { case "ThemeColorExtensibleMessageAdminText": return { color: event.theme_color, }; case "ThreadNicknameExtensibleMessageAdminText": return { nickname: event.nickname, participantID: event.participant_id, }; case "ThreadIconExtensibleMessageAdminText": return { threadIcon: event.thread_icon, }; case "InstantGameUpdateExtensibleMessageAdminText": return { gameID: event.game == null ? null : event.game.id, update_type: event.update_type, collapsed_text: event.collapsed_text, expanded_text: event.expanded_text, instant_game_update_data: event.instant_game_update_data, }; case "GameScoreExtensibleMessageAdminText": return { game_type: event.game_type, }; case "RtcCallLogExtensibleMessageAdminText": return { event: event.event, is_video_call: event.is_video_call, server_info_data: event.server_info_data, }; case "GroupPollExtensibleMessageAdminText": return { event_type: event.event_type, total_count: event.total_count, question: event.question, }; case "AcceptPendingThreadExtensibleMessageAdminText": return { accepter_id: event.accepter_id, requester_id: event.requester_id, }; case "ConfirmFriendRequestExtensibleMessageAdminText": return { friend_request_recipient: event.friend_request_recipient, friend_request_sender: event.friend_request_sender, }; case "AddContactExtensibleMessageAdminText": return { contact_added_id: event.contact_added_id, contact_adder_id: event.contact_adder_id, }; case "AdExtensibleMessageAdminText": return { ad_client_token: event.ad_client_token, ad_id: event.ad_id, ad_preferences_link: event.ad_preferences_link, ad_properties: event.ad_properties, }; // never data case "ParticipantJoinedGroupCallExtensibleMessageAdminText": case "ThreadEphemeralTtlModeExtensibleMessageAdminText": case "StartedSharingVideoExtensibleMessageAdminText": case "LightweightEventCreateExtensibleMessageAdminText": case "LightweightEventNotifyExtensibleMessageAdminText": case "LightweightEventNotifyBeforeEventExtensibleMessageAdminText": case "LightweightEventUpdateTitleExtensibleMessageAdminText": case "LightweightEventUpdateTimeExtensibleMessageAdminText": case "LightweightEventUpdateLocationExtensibleMessageAdminText": case "LightweightEventDeleteExtensibleMessageAdminText": return {}; default: return { error: "Don't know what to with event data type " + event.__typename, }; } } function formatMessagesGraphQLResponse(data) { const messageThread = data.o0.data.message_thread; const threadID = messageThread.thread_key.thread_fbid ? messageThread.thread_key.thread_fbid : messageThread.thread_key.other_user_id; const messages = messageThread.messages.nodes.map(function (d) { switch (d.__typename) { case "UserMessage": // Give priority to stickers. They're seen as normal messages but we've // been considering them as attachments. var maybeStickerAttachment; if (d.sticker) { maybeStickerAttachment = [ { type: "sticker", ID: d.sticker.id, url: d.sticker.url, packID: d.sticker.pack ? d.sticker.pack.id : null, spriteUrl: d.sticker.sprite_image, spriteUrl2x: d.sticker.sprite_image_2x, width: d.sticker.width, height: d.sticker.height, caption: d.snippet, // Not sure what the heck caption was. description: d.sticker.label, // Not sure about this one either. frameCount: d.sticker.frame_count, frameRate: d.sticker.frame_rate, framesPerRow: d.sticker.frames_per_row, framesPerCol: d.sticker.frames_per_col, stickerID: d.sticker.id, // @Legacy spriteURI: d.sticker.sprite_image, // @Legacy spriteURI2x: d.sticker.sprite_image_2x, // @Legacy }, ]; } var mentionsObj = {}; if (d.message !== null) { d.message.ranges.forEach((e) => { mentionsObj[e.entity.id] = d.message.text.substr( e.offset, e.length, ); }); } return { type: "message", attachments: maybeStickerAttachment ? maybeStickerAttachment : d.blob_attachments && d.blob_attachments.length > 0 ? d.blob_attachments.map(formatAttachmentsGraphQLResponse) : d.extensible_attachment ? [formatExtensibleAttachment(d.extensible_attachment)] : [], body: d.message !== null ? d.message.text : "", isGroup: messageThread.thread_type === "GROUP", messageID: d.message_id, senderID: d.message_sender.id, threadID: threadID, timestamp: d.timestamp_precise, mentions: mentionsObj, isUnread: d.unread, // New messageReactions: d.message_reactions ? d.message_reactions.map(formatReactionsGraphQL) : null, isSponsored: d.is_sponsored, snippet: d.snippet, }; case "ThreadNameMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "change_thread_name", snippet: d.snippet, eventData: { threadName: d.thread_name, }, // @Legacy author: d.message_sender.id, logMessageType: "log:thread-name", logMessageData: { name: d.thread_name }, }; case "ThreadImageMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "change_thread_image", snippet: d.snippet, eventData: d.image_with_metadata == null ? {} /* removed image */ : { /* image added */ threadImage: { attachmentID: d.image_with_metadata.legacy_attachment_id, width: d.image_with_metadata.original_dimensions.x, height: d.image_with_metadata.original_dimensions.y, url: d.image_with_metadata.preview.uri, }, }, // @Legacy logMessageType: "log:thread-icon", logMessageData: { thread_icon: d.image_with_metadata ? d.image_with_metadata.preview.uri : null, }, }; case "ParticipantLeftMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "remove_participants", snippet: d.snippet, eventData: { // Array of IDs. participantsRemoved: d.participants_removed.map(function (p) { return p.id; }), }, // @Legacy logMessageType: "log:unsubscribe", logMessageData: { leftParticipantFbId: d.participants_removed.map(function (p) { return p.id; }), }, }; case "ParticipantsAddedMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "add_participants", snippet: d.snippet, eventData: { // Array of IDs. participantsAdded: d.participants_added.map(function (p) { return p.id; }), }, // @Legacy logMessageType: "log:subscribe", logMessageData: { addedParticipants: d.participants_added.map(function (p) { return p.id; }), }, }; case "VideoCallMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "video_call", snippet: d.snippet, // @Legacy logMessageType: "other", }; case "VoiceCallMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, eventType: "voice_call", snippet: d.snippet, // @Legacy logMessageType: "other", }; case "GenericAdminTextMessage": return { type: "event", messageID: d.message_id, threadID: threadID, isGroup: messageThread.thread_type === "GROUP", senderID: d.message_sender.id, timestamp: d.timestamp_precise, snippet: d.snippet, eventType: d.extensible_message_admin_text_type.toLowerCase(), eventData: formatEventData(d.extensible_message_admin_text), // @Legacy logMessageType: utils.getAdminTextMessageType( d.extensible_message_admin_text_type, ), logMessageData: d.extensible_message_admin_text, // Maybe different? }; default: return { error: "Don't know about message type " + d.__typename }; } }); return messages; } module.exports = function (defaultFuncs, api, ctx) { return function getThreadHistoryGraphQL( threadID, amount, timestamp, callback, ) { let resolveFunc = function () {}; let rejectFunc = function () {}; const returnPromise = new Promise(function (resolve, reject) { resolveFunc = resolve; rejectFunc = reject; }); if (!callback) { callback = function (err, data) { if (err) { return rejectFunc(err); } resolveFunc(data); }; } // `queries` has to be a string. I couldn't tell from the dev utils. This // took me a really long time to figure out. I deserve a cookie for this. const form = { av: ctx.globalOptions.pageID, queries: JSON.stringify({ o0: { // This doc_id was valid on February 2nd 2017. doc_id: "1498317363570230", query_params: { id: threadID, message_limit: amount, load_messages: 1, load_read_receipts: false, before: timestamp, }, }, }), }; defaultFuncs .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form) .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) .then(function (resData) { if (resData.error) { throw resData; } // This returns us an array of things. The last one is the success / // failure one. // @TODO What do we do in this case? if (resData[resData.length - 1].error_results !== 0) { throw new Error("There was an error_result."); } callback(null, formatMessagesGraphQLResponse(resData[0])); }) .catch(function (err) { utils.error("getThreadHistoryGraphQL", err); return callback(err); }); return returnPromise; }; };