diff --git a/includes/login/src/addExternalModule.js b/includes/login/src/addExternalModule.js new file mode 100644 index 0000000000000000000000000000000000000000..c2c462a829bcea51d052283ce309577645e37df0 --- /dev/null +++ b/includes/login/src/addExternalModule.js @@ -0,0 +1,19 @@ +"use strict"; + +const utils = require("../utils"); + +module.exports = function (defaultFuncs, api, ctx) { + return function addExternalModule(moduleObj) { + if (utils.getType(moduleObj) == "Object") { + for (const apiName in moduleObj) { + if (utils.getType(moduleObj[apiName]) == "Function") { + api[apiName] = moduleObj[apiName](defaultFuncs, api, ctx); + } else { + throw new Error(`Item "${apiName}" in moduleObj must be a function, not ${utils.getType(moduleObj[apiName])}!`); + } + } + } else { + throw new Error(`moduleObj must be an object, not ${utils.getType(moduleObj)}!`); + } + } + }; diff --git a/includes/login/src/addUserToGroup.js b/includes/login/src/addUserToGroup.js new file mode 100644 index 0000000000000000000000000000000000000000..a13cf0a0c2fe5137642a82d6940ed68e5fd4cb26 --- /dev/null +++ b/includes/login/src/addUserToGroup.js @@ -0,0 +1,113 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function addUserToGroup(userID, threadID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + throw new utils.CustomError({ error: "please pass a threadID as a second argument." }); + } + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + if ( + utils.getType(threadID) !== "Number" && + utils.getType(threadID) !== "String" + ) { + throw new utils.CustomError({ + error: + "ThreadID should be of type Number or String and not " + + utils.getType(threadID) + + "." + }); + } + + if (utils.getType(userID) !== "Array") { + userID = [userID]; + } + + const messageAndOTID = utils.generateOfflineThreadingID(); + const form = { + client: "mercury", + action_type: "ma-type:log-message", + author: "fbid:" + (ctx.i_userID || ctx.userID), + thread_id: "", + timestamp: Date.now(), + timestamp_absolute: "Today", + timestamp_relative: utils.generateTimestampRelative(), + timestamp_time_passed: "0", + is_unread: false, + is_cleared: false, + is_forward: false, + is_filtered_content: false, + is_filtered_content_bh: false, + is_filtered_content_account: false, + is_spoof_warning: false, + source: "source:chat:web", + "source_tags[0]": "source:chat", + log_message_type: "log:subscribe", + status: "0", + offline_threading_id: messageAndOTID, + message_id: messageAndOTID, + threading_id: utils.generateThreadingID(ctx.clientID), + manual_retry_cnt: "0", + thread_fbid: threadID + }; + + for (let i = 0; i < userID.length; i++) { + if ( + utils.getType(userID[i]) !== "Number" && + utils.getType(userID[i]) !== "String" + ) { + throw new utils.CustomError({ + error: + "Elements of userID should be of type Number or String and not " + + utils.getType(userID[i]) + + "." + }); + } + + form["log_message_data[added_participants][" + i + "]"] = + "fbid:" + userID[i]; + } + + defaultFuncs + .post("https://www.facebook.com/messaging/send/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (!resData) { + throw new utils.CustomError({ error: "Add to group failed." }); + } + if (resData.error) { + throw new utils.CustomError(resData); + } + + return callback(); + }) + .catch(function (err) { + log.error("addUserToGroup", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeAdminStatus.js b/includes/login/src/changeAdminStatus.js new file mode 100644 index 0000000000000000000000000000000000000000..d4ed4d5b959b78a56961cce20bc48e71178fb04e --- /dev/null +++ b/includes/login/src/changeAdminStatus.js @@ -0,0 +1,79 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeAdminStatus(threadID, adminIDs, adminStatus, callback) { + if (utils.getType(threadID) !== "String") { + throw new utils.CustomError({ error: "changeAdminStatus: threadID must be a string" }); + } + + if (utils.getType(adminIDs) === "String") { + adminIDs = [adminIDs]; + } + + if (utils.getType(adminIDs) !== "Array") { + throw new utils.CustomError({ error: "changeAdminStatus: adminIDs must be an array or string" }); + } + + if (utils.getType(adminStatus) !== "Boolean") { + throw new utils.CustomError({ error: "changeAdminStatus: adminStatus must be a string" }); + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") { + throw new utils.CustomError({ error: "changeAdminStatus: callback is not a function" }); + } + + const form = { + "thread_fbid": threadID + }; + + let i = 0; + for (const u of adminIDs) { + form[`admin_ids[${i++}]`] = u; + } + form["add"] = adminStatus; + + defaultFuncs + .post("https://www.facebook.com/messaging/save_admins/?dpr=1", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + switch (resData.error) { + case 1976004: + throw new utils.CustomError({ error: "Cannot alter admin status: you are not an admin.", rawResponse: resData }); + case 1357031: + throw new utils.CustomError({ error: "Cannot alter admin status: this thread is not a group chat.", rawResponse: resData }); + default: + throw new utils.CustomError({ error: "Cannot alter admin status: unknown error.", rawResponse: resData }); + } + } + + callback(); + }) + .catch(function (err) { + log.error("changeAdminStatus", err); + return callback(err); + }); + + return returnPromise; + }; +}; + diff --git a/includes/login/src/changeArchivedStatus.js b/includes/login/src/changeArchivedStatus.js new file mode 100644 index 0000000000000000000000000000000000000000..057db1bbfc7c1b909e4df29440f0bbef5124f3e4 --- /dev/null +++ b/includes/login/src/changeArchivedStatus.js @@ -0,0 +1,55 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeArchivedStatus(threadOrThreads, archive, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const form = {}; + + if (utils.getType(threadOrThreads) === "Array") { + for (let i = 0; i < threadOrThreads.length; i++) { + form["ids[" + threadOrThreads[i] + "]"] = archive; + } + } else { + form["ids[" + threadOrThreads + "]"] = archive; + } + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/change_archived_status.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeArchivedStatus", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeAvatar.js b/includes/login/src/changeAvatar.js new file mode 100644 index 0000000000000000000000000000000000000000..52af747be855ed6a7aa9dffb89961ca3ebbeb2b2 --- /dev/null +++ b/includes/login/src/changeAvatar.js @@ -0,0 +1,126 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + function handleUpload(image, callback) { + const uploads = []; + + const form = { + profile_id: ctx.i_userID || ctx.userID, + photo_source: 57, + av: ctx.i_userID || ctx.userID, + file: image + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://www.facebook.com/profile/picture/upload/", + ctx.jar, + form, + {} + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + return resData; + }) + ); + + // resolve all promises + Promise + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("handleUpload", err); + return callback(err); + }); + } + + return function changeAvatar(image, caption = "", timestamp = null, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!timestamp && utils.getType(caption) === "Number") { + timestamp = caption; + caption = ""; + } + + if (!timestamp && !callback && (utils.getType(caption) == "Function" || utils.getType(caption) == "AsyncFunction")) { + callback = caption; + caption = ""; + timestamp = null; + } + + if (!callback) callback = function (err, data) { + if (err) { + return rejectFunc(err); + } + resolveFunc(data); + }; + + if (!utils.isReadableStream(image)) + return callback("Image is not a readable stream"); + + handleUpload(image, function (err, payload) { + if (err) { + return callback(err); + } + + const form = { + av: ctx.i_userID || ctx.userID, + fb_api_req_friendly_name: "ProfileCometProfilePictureSetMutation", + fb_api_caller_class: "RelayModern", + doc_id: "5066134240065849", + variables: JSON.stringify({ + input: { + caption, + existing_photo_id: payload[0].payload.fbid, + expiration_time: timestamp, + profile_id: ctx.i_userID || ctx.userID, + profile_pic_method: "EXISTING", + profile_pic_source: "TIMELINE", + scaled_crop_rect: { + height: 1, + width: 1, + x: 0, + y: 0 + }, + skip_cropping: true, + actor_id: ctx.i_userID || ctx.userID, + client_mutation_id: Math.round(Math.random() * 19).toString() + }, + isPage: false, + isProfile: true, + scale: 3 + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphql/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.errors) { + throw resData; + } + return callback(null, resData[0].data.profile_picture_set); + }) + .catch(function (err) { + log.error("changeAvatar", err); + return callback(err); + }); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeBio.js b/includes/login/src/changeBio.js new file mode 100644 index 0000000000000000000000000000000000000000..6b683d0de07081bac4b92110a22fa28f6dfa2528 --- /dev/null +++ b/includes/login/src/changeBio.js @@ -0,0 +1,77 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeBio(bio, publish, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + if (utils.getType(publish) == "Function" || utils.getType(publish) == "AsyncFunction") { + callback = publish; + } else { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + } + + if (utils.getType(publish) != "Boolean") { + publish = false; + } + + if (utils.getType(bio) != "String") { + bio = ""; + publish = false; + } + + const form = { + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "ProfileCometSetBioMutation", + // This doc_is is valid as of May 23, 2020 + doc_id: "2725043627607610", + variables: JSON.stringify({ + input: { + bio: bio, + publish_bio_feed_story: publish, + actor_id: ctx.i_userID || ctx.userID, + client_mutation_id: Math.round(Math.random() * 1024).toString() + }, + hasProfileTileViewID: false, + profileTileViewID: null, + scale: 1 + }), + av: ctx.i_userID || ctx.userID + }; + + defaultFuncs + .post( + "https://www.facebook.com/api/graphql/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.errors) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeBio", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeBlockedStatus.js b/includes/login/src/changeBlockedStatus.js new file mode 100644 index 0000000000000000000000000000000000000000..0db02599a5700de17e6f29c6e2ca3b7ddb94aa76 --- /dev/null +++ b/includes/login/src/changeBlockedStatus.js @@ -0,0 +1,47 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeBlockedStatus(userID, block, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + defaultFuncs + .post( + `https://www.facebook.com/messaging/${block ? "" : "un"}block_messages/`, + ctx.jar, + { + fbid: userID + } + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeBlockedStatus", err); + return callback(err); + }); + return returnPromise; + }; +}; diff --git a/includes/login/src/changeGroupImage.js b/includes/login/src/changeGroupImage.js new file mode 100644 index 0000000000000000000000000000000000000000..ef891cc9288b0a6bad422f4fea4a10d04d718692 --- /dev/null +++ b/includes/login/src/changeGroupImage.js @@ -0,0 +1,132 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + function handleUpload(image, callback) { + const uploads = []; + + const form = { + images_only: "true", + "attachment[]": image + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://upload.facebook.com/ajax/mercury/upload.php", + ctx.jar, + form, + {} + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return resData.payload.metadata[0]; + }) + ); + + // resolve all promises + Promise + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("handleUpload", err); + return callback(err); + }); + } + + return function changeGroupImage(image, threadID, callback) { + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + throw { error: "please pass a threadID as a second argument." }; + } + + if (!utils.isReadableStream(image)) { + throw { error: "please pass a readable stream as a first argument." }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const messageAndOTID = utils.generateOfflineThreadingID(); + const form = { + client: "mercury", + action_type: "ma-type:log-message", + author: "fbid:" + (ctx.i_userID || ctx.userID), + author_email: "", + ephemeral_ttl_mode: "0", + is_filtered_content: false, + is_filtered_content_account: false, + is_filtered_content_bh: false, + is_filtered_content_invalid_app: false, + is_filtered_content_quasar: false, + is_forward: false, + is_spoof_warning: false, + is_unread: false, + log_message_type: "log:thread-image", + manual_retry_cnt: "0", + message_id: messageAndOTID, + offline_threading_id: messageAndOTID, + source: "source:chat:web", + "source_tags[0]": "source:chat", + status: "0", + thread_fbid: threadID, + thread_id: "", + timestamp: Date.now(), + timestamp_absolute: "Today", + timestamp_relative: utils.generateTimestampRelative(), + timestamp_time_passed: "0" + }; + + handleUpload(image, function (err, payload) { + if (err) { + return callback(err); + } + + form["thread_image_id"] = payload[0]["image_id"]; + form["thread_id"] = threadID; + + defaultFuncs + .post("https://www.facebook.com/messaging/set_thread_image/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + // check for errors here + + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeGroupImage", err); + return callback(err); + }); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeNickname.js b/includes/login/src/changeNickname.js new file mode 100644 index 0000000000000000000000000000000000000000..6b4a65fec70e08efdecb54308ff53c5f8b0fd124 --- /dev/null +++ b/includes/login/src/changeNickname.js @@ -0,0 +1,59 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeNickname(nickname, threadID, participantID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const form = { + nickname: nickname, + participant_id: participantID, + thread_or_other_fbid: threadID + }; + + defaultFuncs + .post( + "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error === 1545014) { + throw { error: "Trying to change nickname of user isn't in thread" }; + } + if (resData.error === 1357031) { + throw { + error: + "Trying to change user nickname of a thread that doesn't exist. Have at least one message in the thread before trying to change the user nickname." + }; + } + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeNickname", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeThreadColor.js b/includes/login/src/changeThreadColor.js new file mode 100644 index 0000000000000000000000000000000000000000..a68711ce7a8ae88219457752573cb2da2aa1d712 --- /dev/null +++ b/includes/login/src/changeThreadColor.js @@ -0,0 +1,65 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeThreadColor(color, threadID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(err); + }; + } + + if (!isNaN(color)) { + color = color.toString(); + } + const validatedColor = color !== null ? color.toLowerCase() : color; // API only accepts lowercase letters in hex string + + const form = { + dpr: 1, + queries: JSON.stringify({ + o0: { + //This doc_id is valid as of January 31, 2020 + doc_id: "1727493033983591", + query_params: { + data: { + actor_id: ctx.i_userID || ctx.userID, + client_mutation_id: "0", + source: "SETTINGS", + theme_id: validatedColor, + thread_id: threadID + } + } + } + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData[resData.length - 1].error_results > 0) { + throw new utils.CustomError(resData[0].o0.errors); + } + + return callback(); + }) + .catch(function (err) { + log.error("changeThreadColor", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/changeThreadEmoji.js b/includes/login/src/changeThreadEmoji.js new file mode 100644 index 0000000000000000000000000000000000000000..587f4c0dd290cbd1543b6f0eb4fad9427eff0169 --- /dev/null +++ b/includes/login/src/changeThreadEmoji.js @@ -0,0 +1,55 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function changeThreadEmoji(emoji, threadID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + const form = { + emoji_choice: emoji, + thread_or_other_fbid: threadID + }; + + defaultFuncs + .post( + "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error === 1357031) { + throw { + error: + "Trying to change emoji of a chat that doesn't exist. Have at least one message in the thread before trying to change the emoji." + }; + } + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("changeThreadEmoji", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/createNewGroup.js b/includes/login/src/createNewGroup.js new file mode 100644 index 0000000000000000000000000000000000000000..a76773dfbfc3ab6a5165f44e21ad6be9905ecbc2 --- /dev/null +++ b/includes/login/src/createNewGroup.js @@ -0,0 +1,86 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function createNewGroup(participantIDs, groupTitle, callback) { + if (utils.getType(groupTitle) == "Function") { + callback = groupTitle; + groupTitle = null; + } + + if (utils.getType(participantIDs) !== "Array") { + throw { error: "createNewGroup: participantIDs should be an array." }; + } + + if (participantIDs.length < 2) { + throw { error: "createNewGroup: participantIDs should have at least 2 IDs." }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, threadID) { + if (err) { + return rejectFunc(err); + } + resolveFunc(threadID); + }; + } + + const pids = []; + for (const n in participantIDs) { + pids.push({ + fbid: participantIDs[n] + }); + } + pids.push({ fbid: ctx.i_userID || ctx.userID }); + + const form = { + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "MessengerGroupCreateMutation", + av: ctx.i_userID || ctx.userID, + //This doc_id is valid as of January 11th, 2020 + doc_id: "577041672419534", + variables: JSON.stringify({ + input: { + entry_point: "jewel_new_group", + actor_id: ctx.i_userID || ctx.userID, + participants: pids, + client_mutation_id: Math.round(Math.random() * 1024).toString(), + thread_settings: { + name: groupTitle, + joinable_mode: "PRIVATE", + thread_image_fbid: null + } + } + }) + }; + + defaultFuncs + .post( + "https://www.facebook.com/api/graphql/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.errors) { + throw resData; + } + return callback(null, resData.data.messenger_group_thread_create.thread.thread_key.thread_fbid); + }) + .catch(function (err) { + log.error("createNewGroup", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/createPoll.js b/includes/login/src/createPoll.js new file mode 100644 index 0000000000000000000000000000000000000000..ebc2cf1c34842f0e03837933c570622a855e953a --- /dev/null +++ b/includes/login/src/createPoll.js @@ -0,0 +1,71 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function createPoll(title, threadID, options, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + if (utils.getType(options) == "Function") { + callback = options; + options = null; + } else { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + } + if (!options) { + options = {}; // Initial poll options are optional + } + + const form = { + target_id: threadID, + question_text: title + }; + + // Set fields for options (and whether they are selected initially by the posting user) + let ind = 0; + for (const opt in options) { + // eslint-disable-next-line no-prototype-builtins + if (options.hasOwnProperty(opt)) { + form["option_text_array[" + ind + "]"] = opt; + form["option_is_selected_array[" + ind + "]"] = options[opt] + ? "1" + : "0"; + ind++; + } + } + + defaultFuncs + .post( + "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.payload.status != "success") { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("createPoll", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/deleteMessage.js b/includes/login/src/deleteMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..8fd9860831cc6c90a1d63f633bff7eeb9345fe27 --- /dev/null +++ b/includes/login/src/deleteMessage.js @@ -0,0 +1,56 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function deleteMessage(messageOrMessages, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const form = { + client: "mercury" + }; + + if (utils.getType(messageOrMessages) !== "Array") { + messageOrMessages = [messageOrMessages]; + } + + for (let i = 0; i < messageOrMessages.length; i++) { + form["message_ids[" + i + "]"] = messageOrMessages[i]; + } + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/delete_messages.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("deleteMessage", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/deleteThread.js b/includes/login/src/deleteThread.js new file mode 100644 index 0000000000000000000000000000000000000000..adc8d57022af60115cc9172793c27c5a3c1a7a07 --- /dev/null +++ b/includes/login/src/deleteThread.js @@ -0,0 +1,56 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function deleteThread(threadOrThreads, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const form = { + client: "mercury" + }; + + if (utils.getType(threadOrThreads) !== "Array") { + threadOrThreads = [threadOrThreads]; + } + + for (let i = 0; i < threadOrThreads.length; i++) { + form["ids[" + i + "]"] = threadOrThreads[i]; + } + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/delete_thread.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("deleteThread", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/editMessage.js b/includes/login/src/editMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..9c5fd9f6b64af95c88d151288b261c73f6c3251f --- /dev/null +++ b/includes/login/src/editMessage.js @@ -0,0 +1,59 @@ +"use_strict"; + +const { generateOfflineThreadingID } = require('../utils'); + +function isCallable(func) { + try { + Reflect.apply(func, null, []); + return true; + } catch (error) { + return false; + } +} + +module.exports = function (defaultFuncs, api, ctx) { + + return function editMessage(text, messageID, callback) { + if (!ctx.mqttClient) { + throw new Error('Not connected to MQTT'); + } + + + ctx.wsReqNumber += 1; + ctx.wsTaskNumber += 1; + + const taskPayload = { + message_id: messageID, + text: text, + }; + + const task = { + failure_count: null, + label: '742', + payload: JSON.stringify(taskPayload), + queue_name: 'edit_message', + task_id: ctx.wsTaskNumber, + }; + + const content = { + app_id: '2220391788200892', + payload: { + data_trace_id: null, + epoch_id: parseInt(generateOfflineThreadingID()), + tasks: [], + version_id: '6903494529735864', + }, + request_id: ctx.wsReqNumber, + type: 3, + }; + + content.payload.tasks.push(task); + content.payload = JSON.stringify(content.payload); + + if (isCallable(callback)) { + ctx.reqCallbacks[ctx.wsReqNumber] = callback; + } + + ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }); + }; +} \ No newline at end of file diff --git a/includes/login/src/forwardAttachment.js b/includes/login/src/forwardAttachment.js new file mode 100644 index 0000000000000000000000000000000000000000..f2bed29e9a6f795707f78074150319bc7982e282 --- /dev/null +++ b/includes/login/src/forwardAttachment.js @@ -0,0 +1,60 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function forwardAttachment(attachmentID, userOrUsers, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + if (!callback) { + callback = function (err) { + if (err) { + return rejectFunc(err); + } + resolveFunc(); + }; + } + + const form = { + attachment_id: attachmentID + }; + + if (utils.getType(userOrUsers) !== "Array") { + userOrUsers = [userOrUsers]; + } + + const timestamp = Math.floor(Date.now() / 1000); + + for (let i = 0; i < userOrUsers.length; i++) { + //That's good, the key of the array is really timestmap in seconds + index + //Probably time when the attachment will be sent? + form["recipient_map[" + (timestamp + i) + "]"] = userOrUsers[i]; + } + + defaultFuncs + .post( + "https://www.facebook.com/mercury/attachments/forward/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("forwardAttachment", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getCurrentUserID.js b/includes/login/src/getCurrentUserID.js new file mode 100644 index 0000000000000000000000000000000000000000..0075bc9001cde3cf73f68bc2ea7ef022cae86643 --- /dev/null +++ b/includes/login/src/getCurrentUserID.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = function (defaultFuncs, api, ctx) { + return function getCurrentUserID() { + return ctx.i_userID || ctx.userID; + }; +}; diff --git a/includes/login/src/getEmojiUrl.js b/includes/login/src/getEmojiUrl.js new file mode 100644 index 0000000000000000000000000000000000000000..7780de8e823ccac03d215af78407fa25cdff6eb3 --- /dev/null +++ b/includes/login/src/getEmojiUrl.js @@ -0,0 +1,29 @@ +"use strict"; + +const util = require("util"); + +module.exports = function () { + return function getEmojiUrl(c, size, pixelRatio) { + /* + Resolves Facebook Messenger emoji image asset URL for an emoji character. + Supported sizes are 32, 64, and 128. + Supported pixel ratios are '1.0' and '1.5' (possibly more; haven't tested) + */ + const baseUrl = "https://static.xx.fbcdn.net/images/emoji.php/v8/z%s/%s"; + pixelRatio = pixelRatio || "1.0"; + + const ending = util.format( + "%s/%s/%s.png", + pixelRatio, + size, + c.codePointAt(0).toString(16) + ); + let base = 317426846; + for (let i = 0; i < ending.length; i++) { + base = (base << 5) - base + ending.charCodeAt(i); + } + + const hashed = (base & 255).toString(16); + return util.format(baseUrl, hashed, ending); + }; +}; diff --git a/includes/login/src/getFriendsList.js b/includes/login/src/getFriendsList.js new file mode 100644 index 0000000000000000000000000000000000000000..0be7741e0e7905bfd103a77390e3ca59dc05043c --- /dev/null +++ b/includes/login/src/getFriendsList.js @@ -0,0 +1,83 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +// [almost] copy pasted from one of FB's minified file (GenderConst) +const GENDERS = { + 0: "unknown", + 1: "female_singular", + 2: "male_singular", + 3: "female_singular_guess", + 4: "male_singular_guess", + 5: "mixed", + 6: "neuter_singular", + 7: "unknown_singular", + 8: "female_plural", + 9: "male_plural", + 10: "neuter_plural", + 11: "unknown_plural" +}; + +function formatData(obj) { + return Object.keys(obj).map(function (key) { + const user = obj[key]; + return { + alternateName: user.alternateName, + firstName: user.firstName, + gender: GENDERS[user.gender], + userID: utils.formatID(user.id.toString()), + isFriend: user.is_friend != null && user.is_friend ? true : false, + fullName: user.name, + profilePicture: user.thumbSrc, + type: user.type, + profileUrl: user.uri, + vanity: user.vanity, + isBirthday: !!user.is_birthday + }; + }); +} + +module.exports = function (defaultFuncs, api, ctx) { + return function getFriendsList(callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + defaultFuncs + .postFormData( + "https://www.facebook.com/chat/user_info_all", + ctx.jar, + {}, + { viewer: ctx.i_userID || ctx.userID } + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (!resData) { + throw { error: "getFriendsList returned empty object." }; + } + if (resData.error) { + throw resData; + } + callback(null, formatData(resData.payload)); + }) + .catch(function (err) { + log.error("getFriendsList", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getMessage.js b/includes/login/src/getMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..e2f205fa92f84e06789c37d1b2f182e117609134 --- /dev/null +++ b/includes/login/src/getMessage.js @@ -0,0 +1,796 @@ +"use strict"; + +// Original +/** + * @author https://github.com/Schmavery/facebook-chat-api/pull/865 + */ + +const utils = require("../utils"); +const log = require("npmlog"); + + +function formatMessage(threadID, data) { + switch (data.__typename) { + case "ThreadNameMessage": + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:thread-name", + logMessageData: { + name: data.thread_name + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + case "ThreadImageMessage": + const metadata = data.image_with_metadata; + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:thread-image", + logMessageData: metadata ? { + attachmentID: metadata.legacy_attachment_id, + width: metadata.original_dimensions.x, + height: metadata.original_dimensions.y, + url: metadata.preview.uri + } : + { + attachmentID: null, + width: null, + height: null, + url: null + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + case "GenericAdminTextMessage": + switch (data.extensible_message_admin_text_type) { + case "CHANGE_THREAD_THEME": + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:thread-color", + logMessageData: colors.find(color => color.theme_color === data.extensible_message_admin_text.theme_color) || { + theme_color: data.extensible_message_admin_text.theme_color, + theme_id: null, + theme_emoji: null, + gradient: null, + should_show_icon: null, + theme_name_with_subtitle: null + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + case "CHANGE_THREAD_ICON": + const thread_icon = data.extensible_message_admin_text.thread_icon; + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:thread-icon", + logMessageData: { + thread_icon_url: `https://static.xx.fbcdn.net/images/emoji.php/v9/t3c/1/16/${thread_icon.codePointAt(0).toString(16)}.png`, + thread_icon: thread_icon + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + case "CHANGE_THREAD_NICKNAME": + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:user-nickname", + logMessageData: { + nickname: data.extensible_message_admin_text.nickname, + participant_id: data.extensible_message_admin_text.participant_id + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + case "GROUP_POLL": + const question = data.extensible_message_admin_text.question; + return { + type: "event", + threadID: threadID, + messageID: data.message_id, + logMessageType: "log:thread-poll", + logMessageData: { + question_json: JSON.stringify({ + id: question.id, + text: question.text, + total_count: data.extensible_message_admin_text.total_count, + viewer_has_voted: question.viewer_has_voted, + question_type: "", + creator_id: data.message_sender.id, + options: question.options.nodes.map(option => ({ + id: option.id, + text: option.text, + total_count: option.voters.nodes.length, + viewer_has_voted: option.viewer_has_voted, + voters: option.voters.nodes.map(voter => voter.id) + })) + }), + event_type: data.extensible_message_admin_text.event_type.toLowerCase(), + question_id: question.id + }, + logMessageBody: data.snippet, + timestamp: data.timestamp_precise, + author: data.message_sender.id + }; + default: + throw new Error(`Unknown admin text type: "${data.extensible_message_admin_text_type}", if this happens to you let me know when it happens. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.`); + } + case "UserMessage": + return { + senderID: data.message_sender.id, + body: data.message.text, + threadID: threadID, + messageID: data.message_id, + reactions: data.message_reactions.map(r => ({ + [r.user.id]: r.reaction + })), + attachments: data.blob_attachments && data.blob_attachments.length > 0 ? + data.blob_attachments.length.map(att => { + let x; + try { + x = utils._formatAttachment(att); + } catch (ex) { + x = att; + x.error = ex; + x.type = "unknown"; + } + return x; + }) : + data.extensible_attachment && Object.keys(data.extensible_attachment).length > 0 ? + [{ + type: "share", + ID: data.extensible_attachment.legacy_attachment_id, + url: data.extensible_attachment.story_attachment.url, + + title: data.extensible_attachment.story_attachment.title_with_entities.text, + description: data.extensible_attachment.story_attachment.description.text, + source: data.extensible_attachment.story_attachment.source, + + image: ((data.extensible_attachment.story_attachment.media || {}).image || {}).uri, + width: ((data.extensible_attachment.story_attachment.media || {}).image || {}).width, + height: ((data.extensible_attachment.story_attachment.media || {}).image || {}).height, + playable: (data.extensible_attachment.story_attachment.media || {}).is_playable || false, + duration: (data.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0, + + subattachments: data.extensible_attachment.subattachments, + properties: data.extensible_attachment.story_attachment.properties + }] : + [], + mentions: data.message.ranges.map(mention => ({ + [mention.entity.id]: data.message.text.substring(mention.offset, mention.offset + mention.length) + })), + timestamp: data.timestamp_precise + }; + default: + throw new Error(`Unknown message type: "${data.__typename}", if this happens to you let me know when it happens. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.`); + // If this happens to you let me know when it happens + // Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues. + // return Object.assign({ type: "unknown", data }); + } +} + + +function parseDelta(threadID, delta) { + if (delta.replied_to_message) { + return Object.assign({ + type: "message_reply" + }, formatMessage(threadID, delta), { + messageReply: formatMessage(threadID, delta.replied_to_message.message) + }); + } + else { + return formatMessage(threadID, delta); + } +} + + +module.exports = function (defaultFuncs, api, ctx) { + return function getMessage(threadID, messageID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, info) { + if (err) + return rejectFunc(err); + resolveFunc(info); + }; + } + + if (!threadID || !messageID) { + return callback({ error: "getMessage: need threadID and messageID" }); + } + + const form = { + "av": ctx.globalOptions.pageID, + "queries": JSON.stringify({ + "o0": { + //This doc_id is valid as of ? (prob January 18, 2020) + "doc_id": "1768656253222505", + "query_params": { + "thread_and_message_id": { + "thread_id": threadID, + "message_id": messageID + } + } + } + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .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: "getMessage: there was no successful_results", res: resData }; + } + + const fetchData = resData[0].o0.data.message; + if (fetchData) { + callback(null, parseDelta(threadID, fetchData)); + } + else { + throw fetchData; + } + }) + .catch((err) => { + log.error("getMessage", err); + callback(err); + }); + + return returnPromise; + }; +}; + +const colors = [ + { + theme_color: 'FF000000', + theme_id: '788274591712841', + theme_emoji: '🖤', + gradient: '["FFF0F0F0"]', + should_show_icon: '', + theme_name_with_subtitle: 'Monochrome' + }, + { + theme_color: 'FFFF5CA1', + theme_id: '169463077092846', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Hot Pink' + }, + { + theme_color: 'FF2825B5', + theme_id: '271607034185782', + theme_emoji: null, + gradient: '["FF5E007E","FF331290","FF2825B5"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Shadow' + }, + { + theme_color: 'FFD9A900', + theme_id: '2533652183614000', + theme_emoji: null, + gradient: '["FF550029","FFAA3232","FFD9A900"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Maple' + }, + { + theme_color: 'FFFB45DE', + theme_id: '2873642949430623', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Tulip' + }, + { + theme_color: 'FF5E007E', + theme_id: '193497045377796', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Grape' + }, + { + theme_color: 'FF7AA286', + theme_id: '1455149831518874', + theme_emoji: '🌑', + gradient: '["FF25C0E1","FFCE832A"]', + should_show_icon: '', + theme_name_with_subtitle: 'Dune' + }, + { + theme_color: 'FFFAAF00', + theme_id: '672058580051520', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Honey' + }, + { + theme_color: 'FF0084FF', + theme_id: '196241301102133', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Default Blue' + }, + { + theme_color: 'FFFFC300', + theme_id: '174636906462322', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Yellow' + }, + { + theme_color: 'FF44BEC7', + theme_id: '1928399724138152', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Teal Blue' + }, + { + theme_color: 'FF7646FF', + theme_id: '234137870477637', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Bright Purple' + }, + { + theme_color: 'FFF25C54', + theme_id: '3022526817824329', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Peach' + }, + { + theme_color: 'FFF01D6A', + theme_id: '724096885023603', + theme_emoji: null, + gradient: '["FF005FFF","FF9200FF","FFFF2E19"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Berry' + }, + { + theme_color: 'FFFF7CA8', + theme_id: '624266884847972', + theme_emoji: null, + gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Candy' + }, + { + theme_color: 'FF6E5B04', + theme_id: '365557122117011', + theme_emoji: '💛', + gradient: '["FFED9F9A","FFED9F9A","FFED9F9A"]', + should_show_icon: '', + theme_name_with_subtitle: 'Support' + }, + { + theme_color: 'FF0052CD', + theme_id: '230032715012014', + theme_emoji: 'âœŒī¸', + gradient: '["FF0052CD","FF00A1E6","FF0052CD"]', + should_show_icon: '', + theme_name_with_subtitle: 'Tie-Dye' + }, + { + theme_color: 'FF601DDD', + theme_id: '1060619084701625', + theme_emoji: 'â˜ī¸', + gradient: '["FFCA34FF","FF302CFF","FFBA009C"]', + should_show_icon: '', + theme_name_with_subtitle: 'Lo-Fi' + }, + { + theme_color: 'FF0099FF', + theme_id: '3273938616164733', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Classic' + }, + { + theme_color: 'FF1ADB5B', + theme_id: '370940413392601', + theme_emoji: null, + gradient: '["FFFFD200","FF6EDF00","FF00DFBB"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Citrus' + }, + { + theme_color: 'FFD696BB', + theme_id: '2058653964378557', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Lavender Purple' + }, + { + theme_color: 'FFC03232', + theme_id: '1059859811490132', + theme_emoji: '🙃', + gradient: '["FFDB4040","FFA32424"]', + should_show_icon: '', + theme_name_with_subtitle: 'Stranger Things' + }, + { + theme_color: 'FFFA3C4C', + theme_id: '2129984390566328', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Red' + }, + { + theme_color: 'FF13CF13', + theme_id: '2136751179887052', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Green' + }, + { + theme_color: 'FFFF7E29', + theme_id: '175615189761153', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Orange' + }, + { + theme_color: 'FFE68585', + theme_id: '980963458735625', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Coral Pink' + }, + { + theme_color: 'FF20CEF5', + theme_id: '2442142322678320', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Aqua Blue' + }, + { + theme_color: 'FF0EDCDE', + theme_id: '417639218648241', + theme_emoji: null, + gradient: '["FF19C9FF","FF00E6D2","FF0EE6B7"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Aqua' + }, + { + theme_color: 'FFFF9C19', + theme_id: '930060997172551', + theme_emoji: null, + gradient: '["FFFFDC2D","FFFF9616","FFFF4F00"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Mango' + }, + { + theme_color: 'FFF01D6A', + theme_id: '164535220883264', + theme_emoji: null, + gradient: '["FF005FFF","FF9200FF","FFFF2E19"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Berry' + }, + { + theme_color: 'FFFF7CA8', + theme_id: '205488546921017', + theme_emoji: null, + gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Candy' + }, + { + theme_color: 'FFFF6F07', + theme_id: '1833559466821043', + theme_emoji: '🌎', + gradient: '["FFFF6F07"]', + should_show_icon: '', + theme_name_with_subtitle: 'Earth' + }, + { + theme_color: 'FF0B0085', + theme_id: '339021464972092', + theme_emoji: '🔈', + gradient: '["FF2FA9E4","FF648FEB","FF9B73F2"]', + should_show_icon: '', + theme_name_with_subtitle: 'Music' + }, + { + theme_color: 'FF8A39EF', + theme_id: '1652456634878319', + theme_emoji: 'đŸŗī¸â€đŸŒˆ', + gradient: '["FFFF0018","FFFF0417","FFFF310E","FFFF5D06","FFFF7A01","FFFF8701","FFFFB001","FFD9C507","FF79C718","FF01C92D","FF01BE69","FF01B3AA","FF0BA1DF","FF3F77E6","FF724CEC","FF8A39EF","FF8A39EF"]', + should_show_icon: '', + theme_name_with_subtitle: 'Pride' + }, + { + theme_color: 'FF004D7C', + theme_id: '538280997628317', + theme_emoji: '🌀', + gradient: '["FF931410","FF931410","FF931410"]', + should_show_icon: '', + theme_name_with_subtitle: 'Doctor Strange' + }, + { + theme_color: 'FF4F4DFF', + theme_id: '3190514984517598', + theme_emoji: '🌤', + gradient: '["FF0080FF","FF9F1AFF"]', + should_show_icon: '', + theme_name_with_subtitle: 'Sky' + }, + { + theme_color: 'FFE84B28', + theme_id: '357833546030778', + theme_emoji: 'đŸ¯', + gradient: '["FFF69500","FFDA0050"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Lunar New Year' + }, + { + theme_color: 'FFB24B77', + theme_id: '627144732056021', + theme_emoji: 'đŸĨŗ', + gradient: '["FFF1614E","FF660F84"]', + should_show_icon: '', + theme_name_with_subtitle: 'Celebration!' + }, + { + theme_color: 'FF66A9FF', + theme_id: '390127158985345', + theme_emoji: 'đŸĨļ', + gradient: '["FF8CB3FF","FF409FFF"]', + should_show_icon: '', + theme_name_with_subtitle: 'Chill' + }, + { + theme_color: 'FF5797FC', + theme_id: '275041734441112', + theme_emoji: '😌', + gradient: '["FF4AC9E4","FF5890FF","FF8C91FF"]', + should_show_icon: '', + theme_name_with_subtitle: 'Care' + }, + { + theme_color: 'FFFF595C', + theme_id: '3082966625307060', + theme_emoji: 'đŸ’Ģ', + gradient: '["FFFF239A","FFFF8C21"]', + should_show_icon: '', + theme_name_with_subtitle: 'Astrology' + }, + { + theme_color: 'FF0171FF', + theme_id: '184305226956268', + theme_emoji: 'â˜ī¸', + gradient: '["FF0026ee","FF00b2ff"]', + should_show_icon: '', + theme_name_with_subtitle: 'J Balvin' + }, + { + theme_color: 'FFA033FF', + theme_id: '621630955405500', + theme_emoji: '🎉', + gradient: '["FFFF7061","FFFF5280","FFA033FF","FF0099FF"]', + should_show_icon: '', + theme_name_with_subtitle: 'Birthday' + }, + { + theme_color: 'FF006528', + theme_id: '539927563794799', + theme_emoji: '🍄', + gradient: '["FF00d52f","FF006528"]', + should_show_icon: '', + theme_name_with_subtitle: 'Cottagecore' + }, + { + theme_color: 'FF4D3EC2', + theme_id: '736591620215564', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Ocean' + }, + { + theme_color: 'FF4e4bf5', + theme_id: '3259963564026002', + theme_emoji: null, + gradient: '["FFAA00FF","FF0080FF"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Default' + }, + { + theme_color: 'FF3A12FF', + theme_id: '582065306070020', + theme_emoji: null, + gradient: '["FFFAAF00","FFFF2E2E","FF3A12FF"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Rocket' + }, + { + theme_color: 'FF3A1D8A', + theme_id: '273728810607574', + theme_emoji: null, + gradient: '["FFFB45DE","FF841DD5","FF3A1D8A"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Unicorn' + }, + { + theme_color: 'FF9FD52D', + theme_id: '262191918210707', + theme_emoji: null, + gradient: '["FF2A7FE3","FF00BF91","FF9FD52D"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Tropical' + }, + { + theme_color: 'FFF7B267', + theme_id: '909695489504566', + theme_emoji: null, + gradient: '["FFF25C54","FFF4845F","FFF7B267"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Sushi' + }, + { + theme_color: 'FF1ADB5B', + theme_id: '557344741607350', + theme_emoji: null, + gradient: '["FFFFD200","FF6EDF00","FF00DFBB"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Citrus' + }, + { + theme_color: 'FF4D3EC2', + theme_id: '280333826736184', + theme_emoji: null, + gradient: '["FFFF625B","FFC532AD","FF4D3EC2"]', + should_show_icon: '1', + theme_name_with_subtitle: 'Lollipop' + }, + { + theme_color: 'FFFF311E', + theme_id: '1257453361255152', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Rose' + }, + { + theme_color: 'FFA797FF', + theme_id: '571193503540759', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Lavender' + }, + { + theme_color: 'FF6EDF00', + theme_id: '3151463484918004', + theme_emoji: null, + gradient: null, + should_show_icon: '1', + theme_name_with_subtitle: 'Kiwi' + }, + { + theme_color: 'FF9D59D2', + theme_id: '737761000603635', + theme_emoji: '💛', + gradient: '["FFFFD600","FFFCB37B","FF9D59D2","FF282828"]', + should_show_icon: '', + theme_name_with_subtitle: 'Non-Binary' + }, + { + theme_color: 'FF57C39C', + theme_id: '1288506208402340', + theme_emoji: '💐', + gradient: '["FF57C39C","FF57C39C","FF57C39C"]', + should_show_icon: '', + theme_name_with_subtitle: 'Mother\'s Day' + }, + { + theme_color: 'FFF65900', + theme_id: '121771470870245', + theme_emoji: 'đŸ’Ĩ', + gradient: '["FFFF8328","FFFF7014","FFFF5C00"]', + should_show_icon: '', + theme_name_with_subtitle: 'APAHM' + }, + { + theme_color: 'FF978E21', + theme_id: '810978360551741', + theme_emoji: 'đŸĒē', + gradient: '["FF978E21","FF978E21","FF978E21"]', + should_show_icon: '', + theme_name_with_subtitle: 'Parenthood' + }, + { + theme_color: 'FFDD8800', + theme_id: '1438011086532622', + theme_emoji: '✨', + gradient: '["FFEDAB00","FFCD6300"]', + should_show_icon: '', + theme_name_with_subtitle: 'Star Wars' + }, + { + theme_color: 'FF7D09B9', + theme_id: '101275642962533', + theme_emoji: '🚀', + gradient: '["FF5C22D6","FF8F1EB5","FFC31B92"]', + should_show_icon: '', + theme_name_with_subtitle: 'Guardians of the Galaxy' + }, + { + theme_color: 'FF61C500', + theme_id: '158263147151440', + theme_emoji: '🐝', + gradient: '["FF61C500","FF61C500","FF61C500"]', + should_show_icon: '', + theme_name_with_subtitle: 'Bloom' + }, + { + theme_color: 'FF826044', + theme_id: '195296273246380', + theme_emoji: '🧋', + gradient: '["FF886546","FFA27F57","FFC6A26E"]', + should_show_icon: '', + theme_name_with_subtitle: 'Bubble Tea' + }, + { + theme_color: 'FFB02501', + theme_id: '6026716157422736', + theme_emoji: 'â›šī¸', + gradient: '["FFAC2503","FFB02501","FFB42400"]', + should_show_icon: '', + theme_name_with_subtitle: 'Basketball' + }, + { + theme_color: 'FF574DC1', + theme_id: '693996545771691', + theme_emoji: '🖤', + gradient: '["FF4533FF","FF574AE0","FF6C64BE"]', + should_show_icon: '', + theme_name_with_subtitle: 'Elephants&Flowers' + }, + { + theme_color: 'FFA8B8DA', + theme_id: '504518465021637', + theme_emoji: 'đŸŗī¸â€âš§ī¸', + gradient: '["FF55D0FF","FF7597D7","FFFF9FB3","FFFF9FB3"]', + should_show_icon: '', + theme_name_with_subtitle: 'Transgender' + } +]; \ No newline at end of file diff --git a/includes/login/src/getThreadHistory.js b/includes/login/src/getThreadHistory.js new file mode 100644 index 0000000000000000000000000000000000000000..05a26ede67607de6ad651baa6f6bec752c37d940 --- /dev/null +++ b/includes/login/src/getThreadHistory.js @@ -0,0 +1,666 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + + +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 console. 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) { + log.error("getThreadHistoryGraphQL", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getThreadInfo.js b/includes/login/src/getThreadInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..504f2cceaf6e16e0e4d28f6627daddbca2dbb1db --- /dev/null +++ b/includes/login/src/getThreadInfo.js @@ -0,0 +1,232 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +function formatEventReminders(reminder) { + return { + reminderID: reminder.id, + eventCreatorID: reminder.lightweight_event_creator.id, + time: reminder.time, + eventType: reminder.lightweight_event_type.toLowerCase(), + locationName: reminder.location_name, + // @TODO verify this + locationCoordinates: reminder.location_coordinates, + locationPage: reminder.location_page, + eventStatus: reminder.lightweight_event_status.toLowerCase(), + note: reminder.note, + repeatMode: reminder.repeat_mode.toLowerCase(), + eventTitle: reminder.event_title, + triggerMessage: reminder.trigger_message, + secondsToNotifyBefore: reminder.seconds_to_notify_before, + allowsRsvp: reminder.allows_rsvp, + relatedEvent: reminder.related_event, + members: reminder.event_reminder_members.edges.map(function (member) { + return { + memberID: member.node.id, + state: member.guest_list_state.toLowerCase() + }; + }) + }; +} + +function formatThreadGraphQLResponse(data) { + if (data.errors) + return data.errors; + const messageThread = data.message_thread; + if (!messageThread) + return null; + const threadID = messageThread.thread_key.thread_fbid + ? messageThread.thread_key.thread_fbid + : messageThread.thread_key.other_user_id; + + // Remove me + const lastM = messageThread.last_message; + const snippetID = + lastM && + lastM.nodes && + lastM.nodes[0] && + lastM.nodes[0].message_sender && + lastM.nodes[0].message_sender.messaging_actor + ? lastM.nodes[0].message_sender.messaging_actor.id + : null; + const snippetText = + lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null; + const lastR = messageThread.last_read_receipt; + const lastReadTimestamp = + lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise + ? lastR.nodes[0].timestamp_precise + : null; + + return { + threadID: threadID, + threadName: messageThread.name, + participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id), + userInfo: messageThread.all_participants.edges.map(d => ({ + id: d.node.messaging_actor.id, + name: d.node.messaging_actor.name, + firstName: d.node.messaging_actor.short_name, + vanity: d.node.messaging_actor.username, + url: d.node.messaging_actor.url, + thumbSrc: d.node.messaging_actor.big_image_src.uri, + profileUrl: d.node.messaging_actor.big_image_src.uri, + gender: d.node.messaging_actor.gender, + type: d.node.messaging_actor.__typename, + isFriend: d.node.messaging_actor.is_viewer_friend, + isBirthday: !!d.node.messaging_actor.is_birthday //not sure? + })), + unreadCount: messageThread.unread_count, + messageCount: messageThread.messages_count, + timestamp: messageThread.updated_time_precise, + muteUntil: messageThread.mute_until, + isGroup: messageThread.thread_type == "GROUP", + isSubscribed: messageThread.is_viewer_subscribed, + isArchived: messageThread.has_viewer_archived, + folder: messageThread.folder, + cannotReplyReason: messageThread.cannot_reply_reason, + eventReminders: messageThread.event_reminders + ? messageThread.event_reminders.nodes.map(formatEventReminders) + : null, + emoji: messageThread.customization_info + ? messageThread.customization_info.emoji + : null, + color: + messageThread.customization_info && + messageThread.customization_info.outgoing_bubble_color + ? messageThread.customization_info.outgoing_bubble_color.slice(2) + : null, + threadTheme: messageThread.thread_theme, + nicknames: + messageThread.customization_info && + messageThread.customization_info.participant_customizations + ? messageThread.customization_info.participant_customizations.reduce( + function (res, val) { + if (val.nickname) res[val.participant_id] = val.nickname; + return res; + }, + {} + ) + : {}, + adminIDs: messageThread.thread_admins, + approvalMode: Boolean(messageThread.approval_mode), + approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({ + inviterID: a.inviter.id, + requesterID: a.requester.id, + timestamp: a.request_timestamp, + request_source: a.request_source // @Undocumented + })), + + // @Undocumented + reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(), + mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(), + isPinProtected: messageThread.is_pin_protected, + relatedPageThread: messageThread.related_page_thread, + + // @Legacy + name: messageThread.name, + snippet: snippetText, + snippetSender: snippetID, + snippetAttachments: [], + serverTimestamp: messageThread.updated_time_precise, + imageSrc: messageThread.image ? messageThread.image.uri : null, + isCanonicalUser: messageThread.is_canonical_neo_user, + isCanonical: messageThread.thread_type != "GROUP", + recipientsLoadable: true, + hasEmailParticipant: false, + readOnly: false, + canReply: messageThread.cannot_reply_reason == null, + lastMessageTimestamp: messageThread.last_message + ? messageThread.last_message.timestamp_precise + : null, + lastMessageType: "message", + lastReadTimestamp: lastReadTimestamp, + threadType: messageThread.thread_type == "GROUP" ? 2 : 1, + + // update in Wed, 13 Jul 2022 19:41:12 +0700 + inviteLink: { + enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false, + link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null + } + }; +} + +module.exports = function (defaultFuncs, api, ctx) { + return function getThreadInfoGraphQL(threadID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(callback) != "Function" && utils.getType(callback) != "AsyncFunction") { + callback = function (err, data) { + if (err) { + return rejectFunc(err); + } + resolveFunc(data); + }; + } + + if (utils.getType(threadID) !== "Array") { + threadID = [threadID]; + } + + let form = {}; + // `queries` has to be a string. I couldn't tell from the dev console. This + // took me a really long time to figure out. I deserve a cookie for this. + threadID.map(function (t, i) { + form["o" + i] = { + doc_id: "3449967031715030", + query_params: { + id: t, + message_limit: 0, + load_messages: false, + load_read_receipts: false, + before: null + } + }; + }); + + form = { + queries: JSON.stringify(form), + batch_name: "MessengerGraphQLThreadFetcher" + }; + + 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 resData[0].o0.errors[0]; + // } + // if (!resData[0].o0.data.message_thread) { + // throw new Error("can't find this thread"); + // } + const threadInfos = {}; + for (let i = resData.length - 2; i >= 0; i--) { + const threadInfo = formatThreadGraphQLResponse(resData[i][Object.keys(resData[i])[0]].data); + threadInfos[threadInfo?.threadID || threadID[threadID.length - 1 - i]] = threadInfo; + } + if (Object.values(threadInfos).length == 1) { + callback(null, Object.values(threadInfos)[0]); + } + else { + callback(null, threadInfos); + } + }) + .catch(function (err) { + log.error("getThreadInfoGraphQL", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getThreadList.js b/includes/login/src/getThreadList.js new file mode 100644 index 0000000000000000000000000000000000000000..0855ec013776850c063db591e4ed334d7f5aa640 --- /dev/null +++ b/includes/login/src/getThreadList.js @@ -0,0 +1,241 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +function formatEventReminders(reminder) { + return { + reminderID: reminder.id, + eventCreatorID: reminder.lightweight_event_creator.id, + time: reminder.time, + eventType: reminder.lightweight_event_type.toLowerCase(), + locationName: reminder.location_name, + // @TODO verify this + locationCoordinates: reminder.location_coordinates, + locationPage: reminder.location_page, + eventStatus: reminder.lightweight_event_status.toLowerCase(), + note: reminder.note, + repeatMode: reminder.repeat_mode.toLowerCase(), + eventTitle: reminder.event_title, + triggerMessage: reminder.trigger_message, + secondsToNotifyBefore: reminder.seconds_to_notify_before, + allowsRsvp: reminder.allows_rsvp, + relatedEvent: reminder.related_event, + members: reminder.event_reminder_members.edges.map(function (member) { + return { + memberID: member.node.id, + state: member.guest_list_state.toLowerCase() + }; + }) + }; +} + +function formatThreadGraphQLResponse(messageThread) { + const threadID = messageThread.thread_key.thread_fbid + ? messageThread.thread_key.thread_fbid + : messageThread.thread_key.other_user_id; + + // Remove me + const lastM = messageThread.last_message; + const snippetID = + lastM && + lastM.nodes && + lastM.nodes[0] && + lastM.nodes[0].message_sender && + lastM.nodes[0].message_sender.messaging_actor + ? lastM.nodes[0].message_sender.messaging_actor.id + : null; + const snippetText = + lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null; + const lastR = messageThread.last_read_receipt; + const lastReadTimestamp = + lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise + ? lastR.nodes[0].timestamp_precise + : null; + + return { + threadID: threadID, + threadName: messageThread.name, + participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id), + userInfo: messageThread.all_participants.edges.map(d => ({ + id: d.node.messaging_actor.id, + name: d.node.messaging_actor.name, + firstName: d.node.messaging_actor.short_name, + vanity: d.node.messaging_actor.username, + url: d.node.messaging_actor.url, + thumbSrc: d.node.messaging_actor.big_image_src.uri, + profileUrl: d.node.messaging_actor.big_image_src.uri, + gender: d.node.messaging_actor.gender, + type: d.node.messaging_actor.__typename, + isFriend: d.node.messaging_actor.is_viewer_friend, + isBirthday: !!d.node.messaging_actor.is_birthday //not sure? + })), + unreadCount: messageThread.unread_count, + messageCount: messageThread.messages_count, + timestamp: messageThread.updated_time_precise, + muteUntil: messageThread.mute_until, + isGroup: messageThread.thread_type == "GROUP", + isSubscribed: messageThread.is_viewer_subscribed, + isArchived: messageThread.has_viewer_archived, + folder: messageThread.folder, + cannotReplyReason: messageThread.cannot_reply_reason, + eventReminders: messageThread.event_reminders + ? messageThread.event_reminders.nodes.map(formatEventReminders) + : null, + emoji: messageThread.customization_info + ? messageThread.customization_info.emoji + : null, + color: + messageThread.customization_info && + messageThread.customization_info.outgoing_bubble_color + ? messageThread.customization_info.outgoing_bubble_color.slice(2) + : null, + threadTheme: messageThread.thread_theme, + nicknames: + messageThread.customization_info && + messageThread.customization_info.participant_customizations + ? messageThread.customization_info.participant_customizations.reduce( + function (res, val) { + if (val.nickname) res[val.participant_id] = val.nickname; + return res; + }, + {} + ) + : {}, + adminIDs: messageThread.thread_admins, + approvalMode: Boolean(messageThread.approval_mode), + approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({ + inviterID: a.inviter.id, + requesterID: a.requester.id, + timestamp: a.request_timestamp, + request_source: a.request_source // @Undocumented + })), + + // @Undocumented + reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(), + mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(), + isPinProtected: messageThread.is_pin_protected, + relatedPageThread: messageThread.related_page_thread, + + // @Legacy + name: messageThread.name, + snippet: snippetText, + snippetSender: snippetID, + snippetAttachments: [], + serverTimestamp: messageThread.updated_time_precise, + imageSrc: messageThread.image ? messageThread.image.uri : null, + isCanonicalUser: messageThread.is_canonical_neo_user, + isCanonical: messageThread.thread_type != "GROUP", + recipientsLoadable: true, + hasEmailParticipant: false, + readOnly: false, + canReply: messageThread.cannot_reply_reason == null, + lastMessageTimestamp: messageThread.last_message + ? messageThread.last_message.timestamp_precise + : null, + lastMessageType: "message", + lastReadTimestamp: lastReadTimestamp, + threadType: messageThread.thread_type == "GROUP" ? 2 : 1, + + // update in Wed, 13 Jul 2022 19:41:12 +0700 + inviteLink: { + enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false, + link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null + } + }; +} + +function formatThreadList(data) { + // console.log(JSON.stringify(data.find(t => t.thread_key.thread_fbid === "5095817367161431"), null, 2)); + return data.map(t => formatThreadGraphQLResponse(t)); +} + +module.exports = function (defaultFuncs, api, ctx) { + return function getThreadList(limit, timestamp, tags, callback) { + if (!callback && (utils.getType(tags) === "Function" || utils.getType(tags) === "AsyncFunction")) { + callback = tags; + tags = [""]; + } + if (utils.getType(limit) !== "Number" || !Number.isInteger(limit) || limit <= 0) { + throw new utils.CustomError({ error: "getThreadList: limit must be a positive integer" }); + } + if (utils.getType(timestamp) !== "Null" && + (utils.getType(timestamp) !== "Number" || !Number.isInteger(timestamp))) { + throw new utils.CustomError({ error: "getThreadList: timestamp must be an integer or null" }); + } + if (utils.getType(tags) === "String") { + tags = [tags]; + } + if (utils.getType(tags) !== "Array") { + throw new utils.CustomError({ + error: "getThreadList: tags must be an array", + message: "getThreadList: tags must be an array" + }); + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") { + callback = function (err, data) { + if (err) { + return rejectFunc(err); + } + resolveFunc(data); + }; + } + + const form = { + "av": ctx.i_userID || ctx.userID, + "queries": JSON.stringify({ + "o0": { + // This doc_id was valid on 2020-07-20 + // "doc_id": "3336396659757871", + "doc_id": "3426149104143726", + "query_params": { + "limit": limit + (timestamp ? 1 : 0), + "before": timestamp, + "tags": tags, + "includeDeliveryReceipts": true, + "includeSeqID": false + } + } + }), + "batch_name": "MessengerGraphQLThreadlistFetcher" + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then((resData) => { + if (resData[resData.length - 1].error_results > 0) { + throw new utils.CustomError(resData[0].o0.errors); + } + + if (resData[resData.length - 1].successful_results === 0) { + throw new utils.CustomError({ error: "getThreadList: there was no successful_results", res: resData }); + } + + // When we ask for threads using timestamp from the previous request, + // we are getting the last thread repeated as the first thread in this response. + // .shift() gets rid of it + // It is also the reason for increasing limit by 1 when timestamp is set + // this way user asks for 10 threads, we are asking for 11, + // but after removing the duplicated one, it is again 10 + if (timestamp) { + resData[0].o0.data.viewer.message_threads.nodes.shift(); + } + callback(null, formatThreadList(resData[0].o0.data.viewer.message_threads.nodes)); + }) + .catch((err) => { + log.error("getThreadList", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getThreadPictures.js b/includes/login/src/getThreadPictures.js new file mode 100644 index 0000000000000000000000000000000000000000..6742eb694681d3953ea5c5c06da658dda1824b0a --- /dev/null +++ b/includes/login/src/getThreadPictures.js @@ -0,0 +1,79 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function getThreadPictures(threadID, offset, limit, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + let form = { + thread_id: threadID, + offset: offset, + limit: limit + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + return Promise.all( + resData.payload.imagesData.map(function (image) { + form = { + thread_id: threadID, + image_id: image.fbid + }; + return defaultFuncs + .post( + "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + // the response is pretty messy + const queryThreadID = + resData.jsmods.require[0][3][1].query_metadata.query_path[0] + .message_thread; + const imageData = + resData.jsmods.require[0][3][1].query_results[queryThreadID] + .message_images.edges[0].node.image2; + return imageData; + }); + }) + ); + }) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("Error in getThreadPictures", err); + callback(err); + }); + return returnPromise; + }; +}; diff --git a/includes/login/src/getUserID.js b/includes/login/src/getUserID.js new file mode 100644 index 0000000000000000000000000000000000000000..786d75f1195838631b3022c7857bc7f18afe8ec2 --- /dev/null +++ b/includes/login/src/getUserID.js @@ -0,0 +1,66 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +function formatData(data) { + return { + userID: utils.formatID(data.uid.toString()), + photoUrl: data.photo, + indexRank: data.index_rank, + name: data.text, + isVerified: data.is_verified, + profileUrl: data.path, + category: data.category, + score: data.score, + type: data.type + }; +} + +module.exports = function (defaultFuncs, api, ctx) { + return function getUserID(name, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + value: name.toLowerCase(), + viewer: ctx.i_userID || ctx.userID, + rsp: "search", + context: "search", + path: "/home.php", + request_id: utils.getGUID() + }; + + defaultFuncs + .get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + const data = resData.payload.entries; + + callback(null, data.map(formatData)); + }) + .catch(function (err) { + log.error("getUserID", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/getUserInfo.js b/includes/login/src/getUserInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..c37c30c628a22ae1303f4c7b5d1ba7127245a32c --- /dev/null +++ b/includes/login/src/getUserInfo.js @@ -0,0 +1,74 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +function formatData(data) { + const retObj = {}; + + for (const prop in data) { + // eslint-disable-next-line no-prototype-builtins + if (data.hasOwnProperty(prop)) { + const innerObj = data[prop]; + retObj[prop] = { + name: innerObj.name, + firstName: innerObj.firstName, + vanity: innerObj.vanity, + thumbSrc: innerObj.thumbSrc, + profileUrl: innerObj.uri, + gender: innerObj.gender, + type: innerObj.type, + isFriend: innerObj.is_friend, + isBirthday: !!innerObj.is_birthday, + searchTokens: innerObj.searchTokens, + alternateName: innerObj.alternateName + }; + } + } + + return retObj; +} + +module.exports = function (defaultFuncs, api, ctx) { + return function getUserInfo(id, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + if (utils.getType(id) !== "Array") { + id = [id]; + } + + const form = {}; + id.map(function (v, i) { + form["ids[" + i + "]"] = v; + }); + defaultFuncs + .post("https://www.facebook.com/chat/user_info/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + return callback(null, formatData(resData.payload.profiles)); + }) + .catch(function (err) { + log.error("getUserInfo", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/handleFriendRequest.js b/includes/login/src/handleFriendRequest.js new file mode 100644 index 0000000000000000000000000000000000000000..cae4ec02a68aab5cacfd7a8d15b9a73d75521cec --- /dev/null +++ b/includes/login/src/handleFriendRequest.js @@ -0,0 +1,61 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function handleFriendRequest(userID, accept, callback) { + if (utils.getType(accept) !== "Boolean") { + throw { + error: "Please pass a boolean as a second argument." + }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + viewer_id: ctx.i_userID || ctx.userID, + "frefs[0]": "jwl", + floc: "friend_center_requests", + ref: "/reqs.php", + action: (accept ? "confirm" : "reject") + }; + + defaultFuncs + .post( + "https://www.facebook.com/requests/friends/ajax/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.payload.err) { + throw { + err: resData.payload.err + }; + } + + return callback(); + }) + .catch(function (err) { + log.error("handleFriendRequest", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/handleMessageRequest.js b/includes/login/src/handleMessageRequest.js new file mode 100644 index 0000000000000000000000000000000000000000..43b20168b713427b4f2422557ee8adbba92c3539 --- /dev/null +++ b/includes/login/src/handleMessageRequest.js @@ -0,0 +1,65 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function handleMessageRequest(threadID, accept, callback) { + if (utils.getType(accept) !== "Boolean") { + throw { + error: "Please pass a boolean as a second argument." + }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + client: "mercury" + }; + + if (utils.getType(threadID) !== "Array") { + threadID = [threadID]; + } + + const messageBox = accept ? "inbox" : "other"; + + for (let i = 0; i < threadID.length; i++) { + form[messageBox + "[" + i + "]"] = threadID[i]; + } + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/move_thread.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("handleMessageRequest", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/httpGet.js b/includes/login/src/httpGet.js new file mode 100644 index 0000000000000000000000000000000000000000..654a6046ff5bab3e8141396fae5a78355f882a81 --- /dev/null +++ b/includes/login/src/httpGet.js @@ -0,0 +1,57 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function httpGet(url, form, customHeader, callback, notAPI) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") { + callback = form; + form = {}; + } + + if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") { + callback = customHeader; + customHeader = {}; + } + + customHeader = customHeader || {}; + + callback = callback || function (err, data) { + if (err) return rejectFunc(err); + resolveFunc(data); + }; + + if (notAPI) { + utils + .get(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpGet", err); + return callback(err); + }); + } else { + defaultFuncs + .get(url, ctx.jar, form, null, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpGet", err); + return callback(err); + }); + } + + return returnPromise; + }; +}; diff --git a/includes/login/src/httpPost.js b/includes/login/src/httpPost.js new file mode 100644 index 0000000000000000000000000000000000000000..d9185c44e01393d2e9143385ad2d13d662cfdd6b --- /dev/null +++ b/includes/login/src/httpPost.js @@ -0,0 +1,57 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function httpPost(url, form, customHeader, callback, notAPI) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") { + callback = form; + form = {}; + } + + if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") { + callback = customHeader; + customHeader = {}; + } + + customHeader = customHeader || {}; + + callback = callback || function (err, data) { + if (err) return rejectFunc(err); + resolveFunc(data); + }; + + if (notAPI) { + utils + .post(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpPost", err); + return callback(err); + }); + } else { + defaultFuncs + .post(url, ctx.jar, form, {}, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpPost", err); + return callback(err); + }); + } + + return returnPromise; + }; +}; diff --git a/includes/login/src/httpPostFormData.js b/includes/login/src/httpPostFormData.js new file mode 100644 index 0000000000000000000000000000000000000000..f5be5e2910eec3360fd6f8d2fecb0d3eaa5e1212 --- /dev/null +++ b/includes/login/src/httpPostFormData.js @@ -0,0 +1,63 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + + +module.exports = function (defaultFuncs, api, ctx) { + return function httpPostFormData(url, form, customHeader, callback, notAPI) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") { + callback = form; + form = {}; + } + + if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") { + callback = customHeader; + customHeader = {}; + } + + customHeader = customHeader || {}; + + if (utils.getType(callback) == "Boolean") { + notAPI = callback; + callback = null; + } + + callback = callback || function (err, data) { + if (err) return rejectFunc(err); + resolveFunc(data); + }; + + if (notAPI) { + utils + .postFormData(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpPostFormData", err); + return callback(err); + }); + } else { + defaultFuncs + .postFormData(url, ctx.jar, form, null, customHeader) + .then(function (resData) { + callback(null, resData.body.toString()); + }) + .catch(function (err) { + log.error("httpPostFormData", err); + return callback(err); + }); + } + + return returnPromise; + }; +}; diff --git a/includes/login/src/listenMqtt.js b/includes/login/src/listenMqtt.js new file mode 100644 index 0000000000000000000000000000000000000000..e0592ea21a66bdfc1ce3f163735fa004a7fafbc9 --- /dev/null +++ b/includes/login/src/listenMqtt.js @@ -0,0 +1,853 @@ +/* eslint-disable no-redeclare */ +"use strict"; +const utils = require("../utils"); +const log = require("npmlog"); +const mqtt = require('mqtt'); +const websocket = require('websocket-stream'); +const HttpsProxyAgent = require('https-proxy-agent'); +const EventEmitter = require('events'); + +const identity = function () { }; + +const topics = [ + "/legacy_web", + "/webrtc", + "/rtc_multi", + "/onevc", + "/br_sr", //Notification + //Need to publish /br_sr right after this + "/sr_res", + "/t_ms", + "/thread_typing", + "/orca_typing_notifications", + "/notify_disconnect", + //Need to publish /messenger_sync_create_queue right after this + "/orca_presence", + //Will receive /sr_res right here. + + "/legacy_web_mtouch" + // "/inbox", + // "/mercury", + // "/messaging_events", + // "/orca_message_notifications", + // "/pp", + // "/webrtc_response", +]; + +function listenMqtt(defaultFuncs, api, ctx, globalCallback) { + //Don't really know what this does but I think it's for the active state? + //TODO: Move to ctx when implemented + const chatOn = ctx.globalOptions.online; + const foreground = false; + + const sessionID = Math.floor(Math.random() * 9007199254740991) + 1; + const username = { + u: ctx.i_userID || ctx.userID, + s: sessionID, + chat_on: chatOn, + fg: foreground, + d: utils.getGUID(), + ct: "websocket", + //App id from facebook + aid: "219994525426954", + mqtt_sid: "", + cp: 3, + ecp: 10, + st: [], + pm: [], + dc: "", + no_auto_fg: true, + gas: null, + pack: [], + a: ctx.globalOptions.userAgent, + aids: null + }; + const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; "); + + let host; + if (ctx.mqttEndpoint) { + host = `${ctx.mqttEndpoint}&sid=${sessionID}`; + } else if (ctx.region) { + host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLocaleLowerCase()}&sid=${sessionID}`; + } else { + host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}`; + } + + const options = { + clientId: "mqttwsclient", + protocolId: 'MQIsdp', + protocolVersion: 3, + username: JSON.stringify(username), + clean: true, + wsOptions: { + headers: { + 'Cookie': cookies, + 'Origin': 'https://www.facebook.com', + 'User-Agent': ctx.globalOptions.userAgent, + 'Referer': 'https://www.facebook.com/', + 'Host': new URL(host).hostname //'edge-chat.facebook.com' + }, + origin: 'https://www.facebook.com', + protocolVersion: 13 + }, + keepalive: 10, + reschedulePings: false + }; + + if (typeof ctx.globalOptions.proxy != "undefined") { + const agent = new HttpsProxyAgent(ctx.globalOptions.proxy); + options.wsOptions.agent = agent; + } + + ctx.mqttClient = new mqtt.Client(_ => websocket(host, options.wsOptions), options); + + const mqttClient = ctx.mqttClient; + + mqttClient.on('error', function (err) { + log.error("listenMqtt", err); + mqttClient.end(); + if (ctx.globalOptions.autoReconnect) { + listenMqtt(defaultFuncs, api, ctx, globalCallback); + } else { + utils.checkLiveCookie(ctx, defaultFuncs) + .then(res => { + globalCallback({ + type: "stop_listen", + error: "Connection refused: Server unavailable" + }, null); + }) + .catch(err => { + globalCallback({ + type: "account_inactive", + error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com" + }, null); + }); + } + }); + + mqttClient.on('close', function () { + + }); + + mqttClient.on('connect', function () { + topics.forEach(function (topicsub) { + mqttClient.subscribe(topicsub); + }); + + let topic; + const queue = { + sync_api_version: 10, + max_deltas_able_to_process: 1000, + delta_batch_size: 500, + encoding: "JSON", + entity_fbid: ctx.i_userID || 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; + } + + mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false }); + // set status online + // fix by NTKhang + mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 }); + mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 }); + + const rTimeout = setTimeout(function () { + mqttClient.end(); + listenMqtt(defaultFuncs, api, ctx, globalCallback); + }, 5000); + + ctx.tmsWait = function () { + clearTimeout(rTimeout); + ctx.globalOptions.emitReady ? globalCallback({ + type: "ready", + error: null + }) : ""; + delete ctx.tmsWait; + }; + + }); + + mqttClient.on('message', function (topic, message, _packet) { + let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message; + try { + jsonMessage = JSON.parse(jsonMessage); + } + catch (e) { + jsonMessage = {}; + } + + if (jsonMessage.type === "jewel_requests_add") { + globalCallback(null, { + type: "friend_request_received", + actorFbId: jsonMessage.from.toString(), + timestamp: Date.now().toString() + }); + } + else if (jsonMessage.type === "jewel_requests_remove_old") { + globalCallback(null, { + type: "friend_request_cancel", + actorFbId: jsonMessage.from.toString(), + timestamp: Date.now().toString() + }); + } + else if (topic === "/t_ms") { + if (ctx.tmsWait && typeof ctx.tmsWait == "function") { + ctx.tmsWait(); + } + + if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) { + ctx.lastSeqId = jsonMessage.firstDeltaSeqId; + ctx.syncToken = jsonMessage.syncToken; + } + + if (jsonMessage.lastIssuedSeqId) { + ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId); + } + + //If it contains more than 1 delta + for (const i in jsonMessage.deltas) { + const delta = jsonMessage.deltas[i]; + parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta }); + } + } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") { + const typ = { + type: "typ", + isTyping: !!jsonMessage.state, + from: jsonMessage.sender_fbid.toString(), + threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString()) + }; + (function () { globalCallback(null, typ); })(); + } else if (topic === "/orca_presence") { + if (!ctx.globalOptions.updatePresence) { + for (const i in jsonMessage.list) { + const data = jsonMessage.list[i]; + const userID = data["u"]; + + const presence = { + type: "presence", + userID: userID.toString(), + //Convert to ms + timestamp: data["l"] * 1000, + statuses: data["p"] + }; + (function () { globalCallback(null, presence); })(); + } + } + } + + }); + +} + +function parseDelta(defaultFuncs, api, ctx, globalCallback, v) { + if (v.delta.class == "NewMessage") { + //Not tested for pages + if (ctx.globalOptions.pageID && + ctx.globalOptions.pageID != v.queue + ) + return; + + (function resolveAttachmentUrl(i) { + if (i == (v.delta.attachments || []).length) { + let fmtMsg; + try { + fmtMsg = utils.formatDeltaMessage(v); + } catch (err) { + return globalCallback({ + error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.", + detail: err, + res: v, + type: "parse_error" + }); + } + if (fmtMsg) { + if (ctx.globalOptions.autoMarkDelivery) { + markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID); + } + } + return !ctx.globalOptions.selfListen && + (fmtMsg.senderID === ctx.i_userID || fmtMsg.senderID === ctx.userID) ? + undefined : + (function () { globalCallback(null, fmtMsg); })(); + } else { + if (v.delta.attachments[i].mercury.attach_type == "photo") { + api.resolvePhotoUrl( + v.delta.attachments[i].fbid, + (err, url) => { + if (!err) + v.delta.attachments[ + i + ].mercury.metadata.url = url; + return resolveAttachmentUrl(i + 1); + } + ); + } else { + return resolveAttachmentUrl(i + 1); + } + } + })(0); + } + + if (v.delta.class == "ClientPayload") { + const clientPayload = utils.decodeClientPayload( + v.delta.payload + ); + + if (clientPayload && clientPayload.deltas) { + for (const i in clientPayload.deltas) { + const delta = clientPayload.deltas[i]; + if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) { + (function () { + globalCallback(null, { + 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 == 0 ? delta.deltaMessageReaction.userId.toString() : delta.deltaMessageReaction.senderId.toString(), + userID: (delta.deltaMessageReaction.userId || delta.deltaMessageReaction.senderId).toString() + }); + })(); + } else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) { + (function () { + globalCallback(null, { + 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 + }); + })(); + } else if (delta.deltaRemoveMessage && !!ctx.globalOptions.listenEvents) { + (function () { + globalCallback(null, { + 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: api.getCurrentUserID(), + deletionTimestamp: delta.deltaRemoveMessage.deletionTimestamp, + timestamp: delta.deltaRemoveMessage.timestamp + }); + })(); + } + else if (delta.deltaMessageReply) { + //Mention block - #1 + let mdata = + delta.deltaMessageReply.message === undefined ? [] : + delta.deltaMessageReply.message.data === undefined ? [] : + delta.deltaMessageReply.message.data.prng === undefined ? [] : + JSON.parse(delta.deltaMessageReply.message.data.prng); + let m_id = mdata.map(u => u.i); + let m_offset = mdata.map(u => u.o); + let m_length = mdata.map(u => u.l); + + const mentions = {}; + + for (let 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] + ); + } + //Mention block - 1# + const 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(function (att) { + const mercury = JSON.parse(att.mercuryJSON); + Object.assign(att, mercury); + return att; + }).map(att => { + let x; + try { + x = utils._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: 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) { + //Mention block - #2 + mdata = + delta.deltaMessageReply.repliedToMessage === undefined ? [] : + delta.deltaMessageReply.repliedToMessage.data === undefined ? [] : + delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] : + 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); + + const rmentions = {}; + + for (let 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] + ); + } + //Mention block - 2# + 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(function (att) { + const mercury = JSON.parse(att.mercuryJSON); + Object.assign(att, mercury); + return att; + }).map(att => { + let x; + try { + x = utils._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 defaultFuncs + .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, { + "av": ctx.globalOptions.pageID, + "queries": JSON.stringify({ + "o0": { + //Using the same doc_id as forcedFetch + "doc_id": "2848441488556444", + "query_params": { + "thread_and_message_id": { + "thread_id": callbackToReturn.threadID, + "message_id": delta.deltaMessageReply.replyToMessageId.id + } + } + } + }) + }) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .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", res: resData }; + } + + const fetchData = resData[0].o0.data.message; + + const mobj = {}; + for (const 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 => { + let x; + try { + x = utils._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((err) => { + log.error("forcedFetch", err); + }) + .finally(function () { + if (ctx.globalOptions.autoMarkDelivery) { + markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID); + } + !ctx.globalOptions.selfListen && + (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ? + undefined : + (function () { globalCallback(null, callbackToReturn); })(); + }); + } else { + callbackToReturn.delta = delta; + } + + if (ctx.globalOptions.autoMarkDelivery) { + markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID); + } + + return !ctx.globalOptions.selfListen && + (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ? + undefined : + (function () { globalCallback(null, callbackToReturn); })(); + } + } + return; + } + } + + if (v.delta.class !== "NewMessage" && + !ctx.globalOptions.listenEvents + ) + return; + + switch (v.delta.class) { + case "ReadReceipt": + var fmtMsg; + try { + fmtMsg = utils.formatDeltaReadReceipt(v.delta); + } + catch (err) { + return globalCallback({ + error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.", + detail: err, + res: v.delta, + type: "parse_error" + }); + } + return (function () { globalCallback(null, fmtMsg); })(); + case "AdminTextMessage": + switch (v.delta.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": + var fmtMsg; + try { + fmtMsg = utils.formatDeltaEvent(v.delta); + } + catch (err) { + return globalCallback({ + error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.", + detail: err, + res: v.delta, + type: "parse_error" + }); + } + return (function () { globalCallback(null, fmtMsg); })(); + default: + return; + } + //For group images + case "ForcedFetch": + if (!v.delta.threadKey) return; + var mid = v.delta.messageId; + var tid = v.delta.threadKey.threadFbId; + if (mid && tid) { + const form = { + "av": ctx.globalOptions.pageID, + "queries": JSON.stringify({ + "o0": { + //This doc_id is valid as of March 25, 2020 + "doc_id": "2848441488556444", + "query_params": { + "thread_and_message_id": { + "thread_id": tid.toString(), + "message_id": mid + } + } + } + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .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", res: resData }; + } + + const fetchData = resData[0].o0.data.message; + + if (utils.getType(fetchData) == "Object") { + log.info("forcedFetch", fetchData); + switch (fetchData.__typename) { + case "ThreadImageMessage": + (!ctx.globalOptions.selfListenEvent && (fetchData.message_sender.id.toString() === ctx.i_userID || fetchData.message_sender.id.toString() === ctx.userID)) || !ctx.loggedIn ? + undefined : + (function () { + 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 + }); + })(); + break; + case "UserMessage": + log.info("ff-Return", { + 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()) + }); + 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()) + }); + } + } else { + log.error("forcedFetch", fetchData); + } + }) + .catch((err) => { + log.error("forcedFetch", err); + }); + } + break; + case "ThreadName": + case "ParticipantsAddedToGroupThread": + case "ParticipantLeftGroupThread": + case "ApprovalQueue": + var formattedEvent; + try { + formattedEvent = utils.formatDeltaEvent(v.delta); + } catch (err) { + return globalCallback({ + error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.", + detail: err, + res: v.delta, + type: "parse_error" + }); + } + return (!ctx.globalOptions.selfListenEvent && (formattedEvent.author.toString() === ctx.i_userID || formattedEvent.author.toString() === ctx.userID)) || !ctx.loggedIn ? + undefined : + (function () { globalCallback(null, formattedEvent); })(); + } +} + +function markDelivery(ctx, api, threadID, messageID) { + if (threadID && messageID) { + api.markAsDelivered(threadID, messageID, (err) => { + if (err) { + log.error("markAsDelivered", err); + } else { + if (ctx.globalOptions.autoMarkRead) { + api.markAsRead(threadID, (err) => { + if (err) { + log.error("markAsDelivered", err); + } + }); + } + } + }); + } +} + +function getSeqId(defaultFuncs, api, ctx, globalCallback) { + const jar = ctx.jar; + utils + .get('https://www.facebook.com/', jar, null, ctx.globalOptions, { noRef: true }) + .then(utils.saveCookies(jar)) + .then(function (resData) { + const html = resData.body; + const oldFBMQTTMatch = html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/); + let mqttEndpoint = null; + let region = null; + let irisSeqID = null; + let noMqttData = null; + + if (oldFBMQTTMatch) { + irisSeqID = oldFBMQTTMatch[1]; + mqttEndpoint = oldFBMQTTMatch[2]; + region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); + log.info("login", `Got this account's message region: ${region}`); + } else { + const newFBMQTTMatch = html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/); + if (newFBMQTTMatch) { + irisSeqID = newFBMQTTMatch[2]; + mqttEndpoint = newFBMQTTMatch[1].replace(/\\\//g, "/"); + region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); + log.info("login", `Got this account's message region: ${region}`); + } else { + const legacyFBMQTTMatch = html.match(/(\["MqttWebConfig",\[\],{fbid:")(.+?)(",appID:219994525426954,endpoint:")(.+?)(",pollingEndpoint:")(.+?)(3790])/); + if (legacyFBMQTTMatch) { + mqttEndpoint = legacyFBMQTTMatch[4]; + region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase(); + log.warn("login", `Cannot get sequence ID with new RegExp. Fallback to old RegExp (without seqID)...`); + log.info("login", `Got this account's message region: ${region}`); + log.info("login", `[Unused] Polling endpoint: ${legacyFBMQTTMatch[6]}`); + } else { + log.warn("login", "Cannot get MQTT region & sequence ID."); + noMqttData = html; + } + } + } + + ctx.lastSeqId = irisSeqID; + ctx.mqttEndpoint = mqttEndpoint; + ctx.region = region; + if (noMqttData) { + api["htmlData"] = noMqttData; + } + + listenMqtt(defaultFuncs, api, ctx, globalCallback); + }) + .catch(function (err) { + log.error("getSeqId", err); + }); +} + +module.exports = function (defaultFuncs, api, ctx) { + let globalCallback = identity; + + return function (callback) { + class MessageEmitter extends EventEmitter { + stopListening(callback) { + + callback = callback || (() => { }); + globalCallback = identity; + if (ctx.mqttClient) { + ctx.mqttClient.unsubscribe("/webrtc"); + ctx.mqttClient.unsubscribe("/rtc_multi"); + ctx.mqttClient.unsubscribe("/onevc"); + ctx.mqttClient.publish("/browser_close", "{}"); + ctx.mqttClient.end(false, function (...data) { + callback(data); + ctx.mqttClient = undefined; + }); + } + } + + async stopListeningAsync() { + return new Promise((resolve) => { + this.stopListening(resolve); + }); + } + } + + const msgEmitter = new MessageEmitter(); + globalCallback = (callback || function (error, message) { + if (error) { + return msgEmitter.emit("error", error); + } + msgEmitter.emit("message", message); + }); + + // Reset some stuff + if (!ctx.firstListen) + ctx.lastSeqId = null; + ctx.syncToken = undefined; + ctx.t_mqttCalled = false; + + if (!ctx.firstListen || !ctx.lastSeqId) { + getSeqId(defaultFuncs, api, ctx, globalCallback); + } else { + listenMqtt(defaultFuncs, api, ctx, globalCallback); + } + + api.stopListening = msgEmitter.stopListening; + api.stopListeningAsync = msgEmitter.stopListeningAsync; + return msgEmitter; + }; +}; \ No newline at end of file diff --git a/includes/login/src/logout.js b/includes/login/src/logout.js new file mode 100644 index 0000000000000000000000000000000000000000..31c7b92503273c9815ad7ad981c1017057bd3040 --- /dev/null +++ b/includes/login/src/logout.js @@ -0,0 +1,75 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function logout(callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + pmid: "0" + }; + + defaultFuncs + .post( + "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + const elem = resData.jsmods.instances[0][2][0].filter(function (v) { + return v.value === "logout"; + })[0]; + + const html = resData.jsmods.markup.filter(function (v) { + return v[0] === elem.markup.__m; + })[0][1].__html; + + const form = { + fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'), + ref: utils.getFrom(html, '"ref" value="', '"'), + h: utils.getFrom(html, '"h" value="', '"') + }; + + return defaultFuncs + .post("https://www.facebook.com/logout.php", ctx.jar, form) + .then(utils.saveCookies(ctx.jar)); + }) + .then(function (res) { + if (!res.headers) { + throw { error: "An error occurred when logging out." }; + } + + return defaultFuncs + .get(res.headers.location, ctx.jar) + .then(utils.saveCookies(ctx.jar)); + }) + .then(function () { + ctx.loggedIn = false; + log.info("logout", "Logged out successfully."); + callback(); + }) + .catch(function (err) { + log.error("logout", err); + return callback(err); + }); + + return returnPromise; + }; +}; \ No newline at end of file diff --git a/includes/login/src/markAsDelivered.js b/includes/login/src/markAsDelivered.js new file mode 100644 index 0000000000000000000000000000000000000000..50076586feb5f7a6d015db02078c95edc14b303b --- /dev/null +++ b/includes/login/src/markAsDelivered.js @@ -0,0 +1,266 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function markAsDelivered(threadID, messageID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + if (!threadID || !messageID) { + return callback("Error: messageID or threadID is not defined"); + } + + const form = {}; + + form["message_ids[0]"] = messageID; + form["thread_ids[" + threadID + "][0]"] = messageID; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/delivery_receipts.php", + ctx.jar, + form + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("markAsDelivered", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in.") { + ctx.loggedIn = false; + } + return callback(err); + }); + + return returnPromise; + }; +}; + +module.exports.logs = function () { + const gr = require("gradient-string"); + const chalk = require("chalk"); + const _ = require('../../../config.json'); + const cb = _.DESIGN.Admin || '\u0055' + '\u006e' + '\u006b' + '\u006e' + '\u006f' + '\u0077' + '\u006e'; + + const asciiMappings = { + a: { + upper: ' ▄▀█', + lower: '░█▀█', + }, + b: { + upper: '░█▄▄', + lower: '░█▄█', + }, + c: { + upper: '░█▀▀', + lower: '░█▄▄', + }, + d: { + upper: '░█▀▄', + lower: '░█▄▀', + }, + e: { + upper: '░█▀▀', + lower: '░██▄', + }, + f: { + upper: '░█▀▀', + lower: '░█▀ ', + }, + g: { + upper: '░█▀▀', + lower: '░█▄█', + }, + h: { + upper: '░█░█', + lower: '░█▀█', + }, + i: { + upper: '░█', + lower: '░█', + }, + j: { + upper: '░░░█', + lower: '░█▄█', + }, + k: { + upper: '░█▄▀', + lower: '░█░█', + }, + l: { + upper: '░█░░', + lower: '░█▄▄', + }, + m: { + upper: '░█▀▄▀█', + lower: '░█░▀░█', + }, + n: { + upper: '░█▄░█', + lower: '░█░▀█', + }, + o: { + upper: '░█▀█', + lower: '░█▄█', + }, + p: { + upper: '░█▀█', + lower: '░█▀▀', + }, + q: { + upper: '░█▀█', + lower: ' ▀▀█', + }, + r: { + upper: '░█▀█', + lower: '░█▀▄', + }, + s: { + upper: '░█▀', + lower: '░▄█' + }, + t: { + upper: ' ▀█▀', + lower: '░░█░', + }, + u: { + upper: '░█░█', + lower: '░█▄█', + }, + v: { + upper: '░█░█', + lower: '░▀▄▀', + }, + w: { + upper: '░█░█░█', + lower: '░▀▄▀▄▀', + }, + x: { + upper: ' ▀▄▀', + lower: '░█░█' + }, + y: { + upper: '░█▄█', + lower: '░░█░', + }, + z: { + upper: '░▀█', + lower: '░█▄', + }, + '-': { + upper: ' ▄▄', + lower: '░░░' + }, + '+': { + upper: ' ▄█▄', + lower: '░░▀░', + }, + '.': { + upper: '░', + lower: '▄', + }, +}; + + function generateAsciiArt(text) { + let title = text || '\u0042\u006f\u0074\u0050\u0061\u0063\u006b'; + const lines = [' ', ' ']; + for (let i = 0; i < title.length; i++) { + const char = title[i].toLowerCase(); + const mapping = asciiMappings[char] || ''; + lines[0] += `${mapping.upper || ' '}`; + lines[1] += `${mapping.lower || ' '}`; + } + return lines.join('\n'); +} + + const $__ = _.DESIGN.Theme.toLowerCase() || ''; + let ch; + let cre; + if ($__ === '\u0066'+'\u0069'+'\u0065'+'\u0072'+'\u0079') { + ch = gr.fruit; + cre = gr.fruit; +} else if ($__ === '\u0061' + '\u0071' + '\u0075' + '\u0061') { + ch = gr("#2e5fff", "#466deb"); + cre = chalk.hex("#88c2f7"); +} else if ($__ === '\u0068' + '\u0061' + '\u0063' + '\u006b' + '\u0065' + '\u0072') { + ch = gr('#47a127', '#0eed19', '#27f231'); + cre = chalk.hex('#4be813'); +} else if ($__ === '\u0070' + '\u0069' + '\u006e' + '\u006b') { + ch = gr("#ab68ed", "#ea3ef0", "#c93ef0"); + cre = chalk.hex("#8c00ff"); +} else if ($__ === '\u0062' + '\u006c' + '\u0075' + '\u0065') { + ch = gr("#243aff", "#4687f0", "#5800d4"); + cre = chalk.blueBright; +} else if ($__ === '\u0073' + '\u0075' + '\u006e' + '\u006c' + '\u0069' + '\u0067' + '\u0068' + '\u0074') { + ch = gr("#ffae00", "#ffbf00", "#ffdd00"); + cre = chalk.hex("#f6ff00"); +} else if ($__ === '\u0072' + '\u0065' + '\u0064') { + ch = gr("#ff0000", "#ff0026"); + cre = chalk.hex("#ff4747"); +} else if ($__ === '\u0072' + '\u0065' + '\u0074' + '\u0072' + '\u006f') { + ch = gr.retro; + cre = chalk.hex("#7d02bf"); +} else if ($__ === '\u0074' + '\u0065' + '\u0065' + '\u006e') { + ch = gr.teen; + cre = chalk.hex("#fa7f7f"); +} else if ($__ === '\u0073' + '\u0075' + '\u006d' + '\u006d' + '\u0065' + '\u0072') { + ch = gr.summer; + cre = chalk.hex("#f7f565"); +} else if ($__ === '\u0066' + '\u006c' + '\u006f' + '\u0077' + '\u0065' + '\u0072') { + ch = gr.pastel; + cre = chalk.hex("#6ded85"); +} else if ($__ === '\u0067' + '\u0068' + '\u006f' + '\u0073' + '\u0074') { + ch = gr.mind; + cre = chalk.hex("#95d0de"); +} else if ($__ === '\u0070'+'\u0075'+'\u0072'+'\u0070'+'\u006C'+'\u0065') { + ch = gr("#380478", "#5800d4", "#4687f0"); + cre = chalk.hex('#7a039e'); + } else if ($__ === '\u0072'+'\u0061'+'\u0069'+'\u006E'+'\u0062'+'\u006F'+'\u0077') { + ch = gr.rainbow + cre = chalk.hex('#0cb3eb'); + } else if ($__ === '\u006F'+'\u0072'+'\u0061'+'\u006E'+'\u0067'+'\u0065') { + ch = gr("#ff8c08", "#ffad08", "#f5bb47"); + cre = chalk.hex('#ff8400'); + } else { + ch = gr("#243aff", "#4687f0", "#5800d4"); + cre = chalk.blueBright; + + setTimeout(() => { + console.log(`\u0054\u0068\u0065 ${chalk.bgYellow.bold(`${config.DESIGN.Theme}`)} \u0074\u0068\u0065\u006D\u0065\u0020\u0079\u006F\u0075\u0020\u0070\u0072\u006F\u0076\u0069\u0064\u0065\u0064\u0020\u0064\u006F\u0065\u0073\u0020\u006E\u006F\u0074\u0020\u0065\u0078\u0069\u0073\u0074\u0021`) +}, 1000); +}; + + setTimeout(() => { + const title = _.DESIGN.Title || ''; + const asciiTitle = generateAsciiArt(title); + console.log( + ch.multiline('\n' + asciiTitle), + '\n', + ch(' \u2771 ') + '\u0043'+'\u0072'+'\u0065'+'\u0064'+'\u0069'+'\u0074'+'\u0073'+'\u0020'+'\u0074'+'\u006f', + cre('\u0059'+'\u0061'+'\u006E'+'\u0020'+'\u004D'+'\u0061'+'\u0067'+'\u006C'+'\u0069'+'\u006E'+'\u0074'+'\u0065'), + '\n', + ch(' \u2771 ') + `\u0041`+`\u0064`+`\u006d`+`\u0069`+`\u006e`+`\u003a ${cre(`${cb}`)}\n` + ); + }, 1000); +} \ No newline at end of file diff --git a/includes/login/src/markAsRead.js b/includes/login/src/markAsRead.js new file mode 100644 index 0000000000000000000000000000000000000000..cbd7f3c844588039c45b5dee16f84f4af52bc0f4 --- /dev/null +++ b/includes/login/src/markAsRead.js @@ -0,0 +1,80 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return async function markAsRead(threadID, read, callback) { + if (utils.getType(read) === 'Function' || utils.getType(read) === 'AsyncFunction') { + callback = read; + read = true; + } + if (read == undefined) { + read = true; + } + + if (!callback) { + callback = () => { }; + } + + const form = {}; + + if (typeof ctx.globalOptions.pageID !== 'undefined') { + form["source"] = "PagesManagerMessagesInterface"; + form["request_user_id"] = ctx.globalOptions.pageID; + form["ids[" + threadID + "]"] = read; + form["watermarkTimestamp"] = new Date().getTime(); + form["shouldSendReadReceipt"] = true; + form["commerce_last_message_type"] = ""; + //form["titanOriginatedThreadId"] = utils.generateThreadingID(ctx.clientID); + + let resData; + try { + resData = await ( + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/change_read_status.php", + ctx.jar, + form + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + ); + } catch (e) { + callback(e); + return e; + } + + if (resData.error) { + const err = resData.error; + log.error("markAsRead", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in.") { + ctx.loggedIn = false; + } + callback(err); + return err; + } + + callback(); + return null; + } else { + try { + if (ctx.mqttClient) { + const err = await new Promise(r => ctx.mqttClient.publish("/mark_thread", JSON.stringify({ + threadID, + mark: "read", + state: read + }), { qos: 1, retain: false }, r)); + if (err) throw err; + } else { + throw { + error: "You can only use this function after you start listening." + }; + } + } catch (e) { + callback(e); + return e; + } + } + }; +}; diff --git a/includes/login/src/markAsReadAll.js b/includes/login/src/markAsReadAll.js new file mode 100644 index 0000000000000000000000000000000000000000..5714eb5eb66fe4f4a9a6cb2aff0dc1affd496bcd --- /dev/null +++ b/includes/login/src/markAsReadAll.js @@ -0,0 +1,50 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function markAsReadAll(callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + folder: 'inbox' + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/mark_folder_as_read.php", + ctx.jar, + form + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("markAsReadAll", err); + return callback(err); + }); + + return returnPromise; + }; +}; \ No newline at end of file diff --git a/includes/login/src/markAsSeen.js b/includes/login/src/markAsSeen.js new file mode 100644 index 0000000000000000000000000000000000000000..2709493866823dbdf6837e2cca32c2b426fc58a4 --- /dev/null +++ b/includes/login/src/markAsSeen.js @@ -0,0 +1,59 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function markAsRead(seen_timestamp, callback) { + if (utils.getType(seen_timestamp) == "Function" || + utils.getType(seen_timestamp) == "AsyncFunction") { + callback = seen_timestamp; + seen_timestamp = Date.now(); + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + seen_timestamp: seen_timestamp + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/mark_seen.php", + ctx.jar, + form + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("markAsSeen", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in.") { + ctx.loggedIn = false; + } + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/muteThread.js b/includes/login/src/muteThread.js new file mode 100644 index 0000000000000000000000000000000000000000..3673d377a8a03081b2068062f422fbe0c4a1765e --- /dev/null +++ b/includes/login/src/muteThread.js @@ -0,0 +1,52 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + // muteSecond: -1=permanent mute, 0=unmute, 60=one minute, 3600=one hour, etc. + return function muteThread(threadID, muteSeconds, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + thread_fbid: threadID, + mute_settings: muteSeconds + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/change_mute_thread.php", + ctx.jar, + form + ) + .then(utils.saveCookies(ctx.jar)) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("muteThread", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/refreshFb_dtsg.js b/includes/login/src/refreshFb_dtsg.js new file mode 100644 index 0000000000000000000000000000000000000000..5a66b33b5fbd64e5fdfd2aa0c4b378bf6c4d833e --- /dev/null +++ b/includes/login/src/refreshFb_dtsg.js @@ -0,0 +1,81 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + /** + * Refreshes the fb_dtsg and jazoest values. + * @param {Function} callback + * @returns {Promise} + * @description if you don't update the value of fb_dtsg and jazoest for a long time an error "Please try closing and re-opening your browser window" will appear + * @description you should refresh it every 48h or less + */ + return function refreshFb_dtsg(obj, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (utils.getType(obj) === "Function" || utils.getType(obj) === "AsyncFunction") { + callback = obj; + obj = {}; + } + + if (!obj) { + obj = {}; + } + + if (utils.getType(obj) !== "Object") { + throw new utils.CustomError("the first parameter must be an object or a callback function"); + } + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + if (Object.keys(obj).length == 0) { + utils + .get('https://m.facebook.com/', ctx.jar, null, ctx.globalOptions, { noRef: true }) + .then(function (resData) { + const html = resData.body; + const fb_dtsg = utils.getFrom(html, 'name="fb_dtsg" value="', '"'); + const jazoest = utils.getFrom(html, 'name="jazoest" value="', '"'); + if (!fb_dtsg) { + throw new utils.CustomError("Could not find fb_dtsg in HTML after requesting https://www.facebook.com/"); + } + ctx.fb_dtsg = fb_dtsg; + ctx.jazoest = jazoest; + callback(null, { + data: { + fb_dtsg: fb_dtsg, + jazoest: jazoest + }, + message: "refreshed fb_dtsg and jazoest" + }); + }) + .catch(function (err) { + log.error("refreshFb_dtsg", err); + return callback(err); + }); + } + else { + Object.keys(obj).forEach(function (key) { + ctx[key] = obj[key]; + }); + callback(null, { + data: obj, + message: "refreshed " + Object.keys(obj).join(", ") + }); + } + + return returnPromise; + }; +}; diff --git a/includes/login/src/removeUserFromGroup.js b/includes/login/src/removeUserFromGroup.js new file mode 100644 index 0000000000000000000000000000000000000000..1692e3c614d6bd776fa2dcdebeccfc8da05faa71 --- /dev/null +++ b/includes/login/src/removeUserFromGroup.js @@ -0,0 +1,79 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function removeUserFromGroup(userID, threadID, callback) { + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + throw { error: "please pass a threadID as a second argument." }; + } + if ( + utils.getType(threadID) !== "Number" && + utils.getType(threadID) !== "String" + ) { + throw { + error: + "threadID should be of type Number or String and not " + + utils.getType(threadID) + + "." + }; + } + if ( + utils.getType(userID) !== "Number" && + utils.getType(userID) !== "String" + ) { + throw { + error: + "userID should be of type Number or String and not " + + utils.getType(userID) + + "." + }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + uid: userID, + tid: threadID + }; + + defaultFuncs + .post("https://www.facebook.com/chat/remove_participants", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (!resData) { + throw { error: "Remove from group failed." }; + } + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("removeUserFromGroup", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/resolvePhotoUrl.js b/includes/login/src/resolvePhotoUrl.js new file mode 100644 index 0000000000000000000000000000000000000000..0cf2f3ade3004afdcc52ccdeb561321f684270d4 --- /dev/null +++ b/includes/login/src/resolvePhotoUrl.js @@ -0,0 +1,45 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function resolvePhotoUrl(photoID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + defaultFuncs + .get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, { + photo_id: photoID + }) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(resData => { + if (resData.error) { + throw resData; + } + + const photoUrl = resData.jsmods.require[0][3][0]; + + return callback(null, photoUrl); + }) + .catch(err => { + log.error("resolvePhotoUrl", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/searchForThread.js b/includes/login/src/searchForThread.js new file mode 100644 index 0000000000000000000000000000000000000000..1702dfd5d7059b2a74fa5913da87251ea0120f43 --- /dev/null +++ b/includes/login/src/searchForThread.js @@ -0,0 +1,53 @@ +"use strict"; + +const utils = require("../utils"); + +module.exports = function (defaultFuncs, api, ctx) { + return function searchForThread(name, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const tmpForm = { + client: "web_messenger", + query: name, + offset: 0, + limit: 21, + index: "fbid" + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/mercury/search_threads.php", + ctx.jar, + tmpForm + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + if (!resData.payload.mercury_payload.threads) { + return callback({ error: "Could not find thread `" + name + "`." }); + } + return callback( + null, + resData.payload.mercury_payload.threads.map(utils.formatThread) + ); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/sendComment.js b/includes/login/src/sendComment.js new file mode 100644 index 0000000000000000000000000000000000000000..d7d5ebdd2123eab98b89335a0187d5fccc7571dc --- /dev/null +++ b/includes/login/src/sendComment.js @@ -0,0 +1,160 @@ +Output: +"use strict"; + +var utils = require("../utils"); +var log = require("npmlog"); +var bluebird = require("bluebird"); + +module.exports = function (defaultFuncs, api, ctx) { + function getGUID() { + let _0x161e32 = Date.now(), + _0x4ec135 = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + function (_0x32f946) { + let _0x141041 = Math.floor((_0x161e32 + Math.random() * 16) % 16); + _0x161e32 = Math.floor(_0x161e32 / 16); + let _0x31fcdd = ( + _0x32f946 == "x" ? _0x141041 : (_0x141041 & 0x3) | 0x8 + ).toString(16); + return _0x31fcdd; + }, + ); + return _0x4ec135; + } + + function uploadAttachment(attachment, callback) { + var uploads = []; + + // create an array of promises + if (!utils.isReadableStream(attachment)) { + throw { + error: + "Attachment should be a readable stream and not " + + utils.getType(attachment) + + ".", + }; + } + + var form = { + file: attachment, + av: api.getCurrentUserID(), + profile_id: api.getCurrentUserID(), + source: "19", + target_id: api.getCurrentUserID(), + __user: api.getCurrentUserID(), + __a: "1", + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://www.facebook.com/ajax/ufi/upload", + ctx.jar, + form, + {}, + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + return resData.payload; + }), + ); + + // resolve all promises + bluebird + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("uploadAttachment", err); + return callback(err); + }); + } + + async function sendCommentToFb(postId, text, fileID) { + const feedback_id = Buffer.from("feedback:" + postId).toString("base64"); + + const ss1 = getGUID(); + const ss2 = getGUID(); + + const form = { + av: api.getCurrentUserID(), + fb_api_req_friendly_name: "CometUFICreateCommentMutation", + fb_api_caller_class: "RelayModern", + doc_id: "4744517358977326", + variables: JSON.stringify({ + displayCommentsFeedbackContext: null, + displayCommentsContextEnableComment: null, + displayCommentsContextIsAdPreview: null, + displayCommentsContextIsAggregatedShare: null, + displayCommentsContextIsStorySet: null, + feedLocation: "TIMELINE", + feedbackSource: 0, + focusCommentID: null, + includeNestedComments: false, + input: { + attachments: fileID ? [{ media: { id: fileID } }] : null, + feedback_id: feedback_id, + formatting_style: null, + message: { + ranges: [], + text: text, + }, + is_tracking_encrypted: true, + tracking: [], + feedback_source: "PROFILE", + idempotence_token: "client:" + ss1, + session_id: ss2, + actor_id: api.getCurrentUserID(), + client_mutation_id: Math.round(Math.random() * 19), + }, + scale: 3, + useDefaultActor: false, + UFI2CommentsProvider_commentsKey: "ProfileCometTimelineRoute", + }), + }; + + const res = JSON.parse( + await api.httpPost("https://www.facebook.com/api/graphql/", form), + ); + return res; + } + + return async function sendComment(content, postId, callback) { + if (typeof content === "object") { + var text = content.body || ""; + if (content.attachment) { + if (!utils.isReadableStream(content.attachment)) { + throw new Error("Attachment must be a ReadableStream"); + } + + uploadAttachment(content.attachment, async function (err, files) { + if (err) { + return callback(err); + } + + await sendCommentToFb(postId, text, files[0].fbid) + .then((res) => { + return callback(null, res); + }) + .catch((err) => { + return callback(err); + }); + }); + } + } else if (typeof content === "string") { + var text = content; + await sendCommentToFb(postId, text, null) + .then((res) => { + return callback(null, res); + }) + .catch((_) => { + return; + }); + } else throw new Error("Invalid content"); + }; +}; +// example usage \ No newline at end of file diff --git a/includes/login/src/sendMessage.js b/includes/login/src/sendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..5d60d33b6d8c3d0643f38a9e951ebf8f218d311e --- /dev/null +++ b/includes/login/src/sendMessage.js @@ -0,0 +1,447 @@ +"use strict"; + +var utils = require("../utils"); +var log = require("npmlog"); +var bluebird = require("bluebird"); + +var allowedProperties = { + attachment: true, + url: true, + sticker: true, + emoji: true, + emojiSize: true, + body: true, + mentions: true, + location: true, +}; + +module.exports = function (defaultFuncs, api, ctx) { + function uploadAttachment(attachments, callback) { + var uploads = []; + + // create an array of promises + for (var i = 0; i < attachments.length; i++) { + if (!utils.isReadableStream(attachments[i])) { + throw { + error: + "Attachment should be a readable stream and not " + + utils.getType(attachments[i]) + + "." + }; + } + + var form = { + upload_1024: attachments[i], + voice_clip: "true" + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://upload.facebook.com/ajax/mercury/upload.php", + ctx.jar, + form, + {} + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + // We have to return the data unformatted unless we want to change it + // back in sendMessage. + return resData.payload.metadata[0]; + }) + ); + } + + // resolve all promises + bluebird + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("uploadAttachment", err); + return callback(err); + }); + } + + function getUrl(url, callback) { + var form = { + image_height: 960, + image_width: 960, + uri: url + }; + + defaultFuncs + .post( + "https://www.facebook.com/message_share_attachment/fromURI/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + return callback(resData); + } + + if (!resData.payload) { + return callback({ error: "Invalid url" }); + } + + callback(null, resData.payload.share_data.share_params); + }) + .catch(function (err) { + log.error("getUrl", err); + return callback(err); + }); + } + + function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) { + // There are three cases here: + // 1. threadID is of type array, where we're starting a new group chat with users + // specified in the array. + // 2. User is sending a message to a specific user. + // 3. No additional form params and the message goes to an existing group chat. + if (utils.getType(threadID) === "Array") { + for (var i = 0; i < threadID.length; i++) { + form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i]; + } + form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID; + form["client_thread_id"] = "root:" + messageAndOTID; + log.info("sendMessage", "Sending message to multiple users: " + threadID); + } else { + // This means that threadID is the id of a user, and the chat + // is a single person chat + if (isSingleUser) { + form["specific_to_list[0]"] = "fbid:" + threadID; + form["specific_to_list[1]"] = "fbid:" + ctx.userID; + form["other_user_fbid"] = threadID; + } else { + form["thread_fbid"] = threadID; + } + } + + if (ctx.globalOptions.pageID) { + form["author"] = "fbid:" + ctx.globalOptions.pageID; + form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID; + form["creator_info[creatorID]"] = ctx.userID; + form["creator_info[creatorType]"] = "direct_admin"; + form["creator_info[labelType]"] = "sent_message"; + form["creator_info[pageID]"] = ctx.globalOptions.pageID; + form["request_user_id"] = ctx.globalOptions.pageID; + form["creator_info[profileURI]"] = + "https://www.facebook.com/profile.php?id=" + ctx.userID; + } + + defaultFuncs + .post("https://www.facebook.com/messaging/send/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (!resData) { + return callback({ error: "Send message failed." }); + } + + if (resData.error) { + if (resData.error === 1545012) { + log.warn( + "sendMessage", + "Got error 1545012. This might mean that you're not part of the conversation " + + threadID + ); + } + return callback(resData); + } + + var messageInfo = resData.payload.actions.reduce(function (p, v) { + return ( + { + threadID: v.thread_fbid, + messageID: v.message_id, + timestamp: v.timestamp + } || p + ); + }, null); + + return callback(null, messageInfo); + }) + .catch(function (err) { + //log.error("sendMessage", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in.") { + ctx.loggedIn = false; + } + return callback(err); + }); + } + + function send(form, threadID, messageAndOTID, callback, isGroup) { + // We're doing a query to this to check if the given id is the id of + // a user or of a group chat. The form will be different depending + // on that. + if (utils.getType(threadID) === "Array") { + sendContent(form, threadID, false, messageAndOTID, callback); + } else { + if (utils.getType(isGroup) != "Boolean") + sendContent(form, threadID, threadID.length <= 15, messageAndOTID, callback); + else + sendContent(form, threadID, !isGroup, messageAndOTID, callback); + } + } + + function handleUrl(msg, form, callback, cb) { + if (msg.url) { + form["shareable_attachment[share_type]"] = "100"; + getUrl(msg.url, function (err, params) { + if (err) { + return callback(err); + } + + form["shareable_attachment[share_params]"] = params; + cb(); + }); + } else { + cb(); + } + } + + function handleLocation(msg, form, callback, cb) { + if (msg.location) { + if (msg.location.latitude == null || msg.location.longitude == null) { + return callback({ error: "location property needs both latitude and longitude" }); + } + + form["location_attachment[coordinates][latitude]"] = msg.location.latitude; + form["location_attachment[coordinates][longitude]"] = msg.location.longitude; + form["location_attachment[is_current_location]"] = !!msg.location.current; + } + + cb(); + } + + function handleSticker(msg, form, callback, cb) { + if (msg.sticker) { + form["sticker_id"] = msg.sticker; + } + cb(); + } + + function handleEmoji(msg, form, callback, cb) { + if (msg.emojiSize != null && msg.emoji == null) { + return callback({ error: "emoji property is empty" }); + } + if (msg.emoji) { + if (msg.emojiSize == null) { + msg.emojiSize = "medium"; + } + if ( + msg.emojiSize != "small" && + msg.emojiSize != "medium" && + msg.emojiSize != "large" + ) { + return callback({ error: "emojiSize property is invalid" }); + } + if (form["body"] != null && form["body"] != "") { + return callback({ error: "body is not empty" }); + } + form["body"] = msg.emoji; + form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize; + } + cb(); + } + + function handleAttachment(msg, form, callback, cb) { + if (msg.attachment) { + form["image_ids"] = []; + form["gif_ids"] = []; + form["file_ids"] = []; + form["video_ids"] = []; + form["audio_ids"] = []; + + if (utils.getType(msg.attachment) !== "Array") { + msg.attachment = [msg.attachment]; + } + + uploadAttachment(msg.attachment, function (err, files) { + if (err) { + return callback(err); + } + + files.forEach(function (file) { + var key = Object.keys(file); + var type = key[0]; // image_id, file_id, etc + form["" + type + "s"].push(file[type]); // push the id + }); + cb(); + }); + } else { + cb(); + } + } + + function handleMention(msg, form, callback, cb) { + if (msg.mentions) { + for (let i = 0; i < msg.mentions.length; i++) { + const mention = msg.mentions[i]; + + const tag = mention.tag; + if (typeof tag !== "string") { + return callback({ error: "Mention tags must be strings." }); + } + + const offset = msg.body.indexOf(tag, mention.fromIndex || 0); + + if (offset < 0) { + log.warn( + "handleMention", + 'Mention for "' + tag + '" not found in message string.' + ); + } + + if (mention.id == null) { + log.warn("handleMention", "Mention id should be non-null."); + } + + const id = mention.id || 0; + const emptyChar = '\u200E'; + form["body"] = emptyChar + msg.body; + form["profile_xmd[" + i + "][offset]"] = offset + 1; + form["profile_xmd[" + i + "][length]"] = tag.length; + form["profile_xmd[" + i + "][id]"] = id; + form["profile_xmd[" + i + "][type]"] = "p"; + } + } + cb(); + } + + return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) { + typeof isGroup == "undefined" ? isGroup = null : ""; + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + return threadID({ error: "Pass a threadID as a second argument." }); + } + if ( + !replyToMessage && + utils.getType(callback) === "String" + ) { + replyToMessage = callback; + callback = undefined; + } + + var resolveFunc = function () { }; + var rejectFunc = function () { }; + var returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, data) { + if (err) return rejectFunc(err); + resolveFunc(data); + }; + } + + var msgType = utils.getType(msg); + var threadIDType = utils.getType(threadID); + var messageIDType = utils.getType(replyToMessage); + + if (msgType !== "String" && msgType !== "Object") { + return callback({ + error: + "Message should be of type string or object and not " + msgType + "." + }); + } + + // Changing this to accomodate an array of users + if ( + threadIDType !== "Array" && + threadIDType !== "Number" && + threadIDType !== "String" + ) { + return callback({ + error: + "ThreadID should be of type number, string, or array and not " + + threadIDType + + "." + }); + } + + if (replyToMessage && messageIDType !== 'String') { + return callback({ + error: + "MessageID should be of type string and not " + + threadIDType + + "." + }); + } + + if (msgType === "String") { + msg = { body: msg }; + } + + var disallowedProperties = Object.keys(msg).filter( + prop => !allowedProperties[prop] + ); + if (disallowedProperties.length > 0) { + return callback({ + error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`" + }); + } + + var messageAndOTID = utils.generateOfflineThreadingID(); + + var form = { + client: "mercury", + action_type: "ma-type:user-generated-message", + author: "fbid:" + ctx.userID, + timestamp: Date.now(), + timestamp_absolute: "Today", + timestamp_relative: utils.generateTimestampRelative(), + timestamp_time_passed: "0", + is_unread: false, + is_cleared: false, + is_forward: false, + is_filtered_content: false, + is_filtered_content_bh: false, + is_filtered_content_account: false, + is_filtered_content_quasar: false, + is_filtered_content_invalid_app: false, + is_spoof_warning: false, + source: "source:chat:web", + "source_tags[0]": "source:chat", + body: msg.body ? msg.body.toString() : "", + html_body: false, + ui_push_phase: "V3", + status: "0", + offline_threading_id: messageAndOTID, + message_id: messageAndOTID, + threading_id: utils.generateThreadingID(ctx.clientID), + "ephemeral_ttl_mode:": "0", + manual_retry_cnt: "0", + has_attachment: !!(msg.attachment || msg.url || msg.sticker), + signatureID: utils.getSignatureID(), + replied_to_message_id: replyToMessage + }; + + handleLocation(msg, form, callback, () => + handleSticker(msg, form, callback, () => + handleAttachment(msg, form, callback, () => + handleUrl(msg, form, callback, () => + handleEmoji(msg, form, callback, () => + handleMention(msg, form, callback, () => + send(form, threadID, messageAndOTID, callback, isGroup) + ) + ) + ) + ) + ) + ); + + return returnPromise; + }; +}; \ No newline at end of file diff --git a/includes/login/src/sendMessageMqtt.js b/includes/login/src/sendMessageMqtt.js new file mode 100644 index 0000000000000000000000000000000000000000..a605cc5354c0a5dc93e569608486a0cec3572118 --- /dev/null +++ b/includes/login/src/sendMessageMqtt.js @@ -0,0 +1,316 @@ +var utils = require("../utils"); +var log = require("npmlog"); +var bluebird = require("bluebird"); + +module.exports = function (defaultFuncs, api, ctx) { + function uploadAttachment(attachments, callback) { + callback = callback || function () { }; + var uploads = []; + + // create an array of promises + for (var i = 0; i < attachments.length; i++) { + if (!utils.isReadableStream(attachments[i])) { + throw { + error: + "Attachment should be a readable stream and not " + + utils.getType(attachments[i]) + + "." + }; + } + + var form = { + upload_1024: attachments[i], + voice_clip: "true" + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://upload.facebook.com/ajax/mercury/upload.php", + ctx.jar, + form, + {} + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + // We have to return the data unformatted unless we want to change it + // back in sendMessage. + return resData.payload.metadata[0]; + }) + ); + } + + // resolve all promises + bluebird + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("uploadAttachment", err); + return callback(err); + }); + } + + let variance = 0; + const epoch_id = () => Math.floor(Date.now() * (4194304 + (variance = (variance + 0.1) % 5))); + const emojiSizes = { + small: 1, + medium: 2, + large: 3 + }; + + function handleEmoji(msg, form, callback, cb) { + if (msg.emojiSize != null && msg.emoji == null) { + return callback({ error: "emoji property is empty" }); + } + if (msg.emoji) { + if (!msg.emojiSize) { + msg.emojiSize = "small"; + } + if ( + msg.emojiSize !== "small" && + msg.emojiSize !== "medium" && + msg.emojiSize !== "large" && + (isNaN(msg.emojiSize) || msg.emojiSize < 1 || msg.emojiSize > 3) + ) { + return callback({ error: "emojiSize property is invalid" }); + } + + form.payload.tasks[0].payload.send_type = 1; + form.payload.tasks[0].payload.text = msg.emoji; + form.payload.tasks[0].payload.hot_emoji_size = !isNaN(msg.emojiSize) ? msg.emojiSize : emojiSizes[msg.emojiSize]; + } + cb(); + } + + function handleSticker(msg, form, callback, cb) { + if (msg.sticker) { + form.payload.tasks[0].payload.send_type = 2; + form.payload.tasks[0].payload.sticker_id = msg.sticker; + } + cb(); + } + + function handleAttachment(msg, form, callback, cb) { + if (msg.attachment) { + form.payload.tasks[0].payload.send_type = 3; + form.payload.tasks[0].payload.attachment_fbids = []; + if (form.payload.tasks[0].payload.text == "") + form.payload.tasks[0].payload.text = null; + if (utils.getType(msg.attachment) !== "Array") { + msg.attachment = [msg.attachment]; + } + + uploadAttachment(msg.attachment, function (err, files) { + if (err) { + return callback(err); + } + + files.forEach(function (file) { + var key = Object.keys(file); + var type = key[0]; // image_id, file_id, etc + form.payload.tasks[0].payload.attachment_fbids.push(file[type]); // push the id + }); + cb(); + }); + } else { + cb(); + } + } + + + function handleMention(msg, form, callback, cb) { + if (msg.mentions) { + form.payload.tasks[0].payload.send_type = 1; + + const arrayIds = []; + const arrayOffsets = []; + const arrayLengths = []; + const mention_types = []; + + for (let i = 0; i < msg.mentions.length; i++) { + const mention = msg.mentions[i]; + + const tag = mention.tag; + if (typeof tag !== "string") { + return callback({ error: "Mention tags must be strings." }); + } + + const offset = msg.body.indexOf(tag, mention.fromIndex || 0); + + if (offset < 0) { + log.warn( + "handleMention", + 'Mention for "' + tag + '" not found in message string.' + ); + } + + if (mention.id == null) { + log.warn("handleMention", "Mention id should be non-null."); + } + + const id = mention.id || 0; + arrayIds.push(id); + arrayOffsets.push(offset); + arrayLengths.push(tag.length); + mention_types.push("p"); + } + + form.payload.tasks[0].payload.mention_data = { + mention_ids: arrayIds.join(","), + mention_offsets: arrayOffsets.join(","), + mention_lengths: arrayLengths.join(","), + mention_types: mention_types.join(",") + }; + } + cb(); + } + + function handleLocation(msg, form, callback, cb) { + // this is not working yet + if (msg.location) { + if (msg.location.latitude == null || msg.location.longitude == null) { + return callback({ error: "location property needs both latitude and longitude" }); + } + + form.payload.tasks[0].payload.send_type = 1; + form.payload.tasks[0].payload.location_data = { + coordinates: { + latitude: msg.location.latitude, + longitude: msg.location.longitude + }, + is_current_location: !!msg.location.current, + is_live_location: !!msg.location.live + }; + } + + cb(); + } + + function send(form, threadID, callback, replyToMessage) { + if (replyToMessage) { + form.payload.tasks[0].payload.reply_metadata = { + reply_source_id: replyToMessage, + reply_source_type: 1, + reply_type: 0 + }; + } + const mqttClient = ctx.mqttClient; + form.payload.tasks.forEach((task) => { + task.payload = JSON.stringify(task.payload); + }); + form.payload = JSON.stringify(form.payload); + console.log(global.jsonStringifyColor(form, null, 2)); + + return mqttClient.publish("/ls_req", JSON.stringify(form), function (err, data) { + if (err) { + console.error('Error publishing message: ', err); + callback(err); + } else { + console.log('Message published successfully with data: ', data); + callback(null, data); + } + }); + } + + return function sendMessageMqtt(msg, threadID, callback, replyToMessage) { + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + return threadID({ error: "Pass a threadID as a second argument." }); + } + if ( + !replyToMessage && + utils.getType(callback) === "String" + ) { + replyToMessage = callback; + callback = function () { }; + } + + + if (!callback) { + callback = function (err, friendList) { + }; + } + + var msgType = utils.getType(msg); + var threadIDType = utils.getType(threadID); + var messageIDType = utils.getType(replyToMessage); + + if (msgType !== "String" && msgType !== "Object") { + return callback({ + error: + "Message should be of type string or object and not " + msgType + "." + }); + } + + if (msgType === "String") { + msg = { body: msg }; + } + + const timestamp = Date.now(); + // get full date time + const epoch = timestamp << 22; + //const otid = epoch + 0; // TODO replace with randomInt(0, 2**22) + const otid = epoch + Math.floor(Math.random() * 4194304); + + const form = { + app_id: "2220391788200892", + payload: { + tasks: [ + { + label: "46", + payload: { + thread_id: threadID.toString(), + otid: otid.toString(), + source: 0, + send_type: 1, + sync_group: 1, + text: msg.body != null && msg.body != undefined ? msg.body.toString() : "", + initiating_source: 1, + skip_url_preview_gen: 0 + }, + queue_name: threadID.toString(), + task_id: 0, + failure_count: null + }, + { + label: "21", + payload: { + thread_id: threadID.toString(), + last_read_watermark_ts: Date.now(), + sync_group: 1 + }, + queue_name: threadID.toString(), + task_id: 1, + failure_count: null + } + ], + epoch_id: epoch_id(), + version_id: "6120284488008082", + data_trace_id: null + }, + request_id: 1, + type: 3 + }; + + handleEmoji(msg, form, callback, function () { + handleLocation(msg, form, callback, function () { + handleMention(msg, form, callback, function () { + handleSticker(msg, form, callback, function () { + handleAttachment(msg, form, callback, function () { + send(form, threadID, callback, replyToMessage); + }); + }); + }); + }); + }); + }; +}; \ No newline at end of file diff --git a/includes/login/src/sendTypingIndicator.js b/includes/login/src/sendTypingIndicator.js new file mode 100644 index 0000000000000000000000000000000000000000..e89f3f49a34044713d3775b69c246e768008faa3 --- /dev/null +++ b/includes/login/src/sendTypingIndicator.js @@ -0,0 +1,103 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + function makeTypingIndicator(typ, threadID, callback, isGroup) { + const form = { + typ: +typ, + to: "", + source: "mercury-chat", + thread: threadID + }; + + // Check if thread is a single person chat or a group chat + // More info on this is in api.sendMessage + if (utils.getType(isGroup) == "Boolean") { + if (!isGroup) { + form.to = threadID; + } + defaultFuncs + .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("sendTypingIndicator", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in") { + ctx.loggedIn = false; + } + return callback(err); + }); + } else { + api.getUserInfo(threadID, function (err, res) { + if (err) { + return callback(err); + } + + // If id is single person chat + if (Object.keys(res).length > 0) { + form.to = threadID; + } + + defaultFuncs + .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("sendTypingIndicator", err); + if (utils.getType(err) == "Object" && err.error === "Not logged in.") { + ctx.loggedIn = false; + } + return callback(err); + }); + }); + } + } + + return function sendTypingIndicator(threadID, callback, isGroup) { + if ( + utils.getType(callback) !== "Function" && + utils.getType(callback) !== "AsyncFunction" + ) { + if (callback) { + log.warn( + "sendTypingIndicator", + "callback is not a function - ignoring." + ); + } + callback = () => { }; + } + + makeTypingIndicator(true, threadID, callback, isGroup); + + return function end(cb) { + if ( + utils.getType(cb) !== "Function" && + utils.getType(cb) !== "AsyncFunction" + ) { + if (cb) { + log.warn( + "sendTypingIndicator", + "callback is not a function - ignoring." + ); + } + cb = () => { }; + } + + makeTypingIndicator(false, threadID, cb, isGroup); + }; + }; +}; diff --git a/includes/login/src/setMessageReaction.js b/includes/login/src/setMessageReaction.js new file mode 100644 index 0000000000000000000000000000000000000000..ccf6dadd2bb750332c6958dc71214d1b7e158499 --- /dev/null +++ b/includes/login/src/setMessageReaction.js @@ -0,0 +1,117 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function setMessageReaction(reaction, messageID, callback, forceCustomReaction) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + switch (reaction) { + case "\uD83D\uDE0D": //:heart_eyes: + case "\uD83D\uDE06": //:laughing: + case "\uD83D\uDE2E": //:open_mouth: + case "\uD83D\uDE22": //:cry: + case "\uD83D\uDE20": //:angry: + case "\uD83D\uDC4D": //:thumbsup: + case "\uD83D\uDC4E": //:thumbsdown: + case "\u2764": //:heart: + case "\uD83D\uDC97": //:glowingheart: + case "": + //valid + break; + case ":heart_eyes:": + case ":love:": + reaction = "\uD83D\uDE0D"; + break; + case ":laughing:": + case ":haha:": + reaction = "\uD83D\uDE06"; + break; + case ":open_mouth:": + case ":wow:": + reaction = "\uD83D\uDE2E"; + break; + case ":cry:": + case ":sad:": + reaction = "\uD83D\uDE22"; + break; + case ":angry:": + reaction = "\uD83D\uDE20"; + break; + case ":thumbsup:": + case ":like:": + reaction = "\uD83D\uDC4D"; + break; + case ":thumbsdown:": + case ":dislike:": + reaction = "\uD83D\uDC4E"; + break; + case ":heart:": + reaction = "\u2764"; + break; + case ":glowingheart:": + reaction = "\uD83D\uDC97"; + break; + default: + if (forceCustomReaction) { + break; + } + return callback({ error: "Reaction is not a valid emoji." }); + } + + const variables = { + data: { + client_mutation_id: ctx.clientMutationId++, + actor_id: ctx.i_userID || ctx.userID, + action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION", + message_id: messageID, + reaction: reaction + } + }; + + const qs = { + doc_id: "1491398900900362", + variables: JSON.stringify(variables), + dpr: 1 + }; + + defaultFuncs + .postFormData( + "https://www.facebook.com/webgraphql/mutation/", + ctx.jar, + {}, + qs + ) + .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs)) + .then(function (resData) { + if (!resData) { + throw { error: "setReaction returned empty object." }; + } + if (resData.error) { + throw resData; + } + callback(null); + }) + .catch(function (err) { + log.error("setReaction", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/setPostReaction.js b/includes/login/src/setPostReaction.js new file mode 100644 index 0000000000000000000000000000000000000000..729ae5aeb0b81d221fea3970c1f082a05e060962 --- /dev/null +++ b/includes/login/src/setPostReaction.js @@ -0,0 +1,111 @@ +/** + * @Updated by @YanMaglinte + * updated on Sunday, 3 March 2024 + * changed Buffer => Buffer.from() + * modified it since it was outdated + * Do not remove the author's name to get notified to the latest updates. +*/ + +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +function formatData(resData) { + return { + viewer_feedback_reaction_info: resData.feedback_react.feedback.viewer_feedback_reaction_info, + supported_reactions: resData.feedback_react.feedback.supported_reactions, + top_reactions: resData.feedback_react.feedback.top_reactions.edges, + reaction_count: resData.feedback_react.feedback.reaction_count + }; +} + +module.exports = function (defaultFuncs, api, ctx) { + return function setPostReaction(postID, type, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + if (utils.getType(type) === "Function" || utils.getType(type) === "AsyncFunction") { + callback = type; + type = 0; + } + else { + callback = function (err, data) { + if (err) { + return rejectFunc(err); + } + resolveFunc(data); + }; + } + } + + const map = { + unlike: 0, + like: 1, + heart: 2, + love: 16, + haha: 4, + wow: 3, + sad: 7, + angry: 8 + }; + + if (utils.getType(type) !== "Number" && utils.getType(type) === "String") { + type = map[type.toLowerCase()]; + } + + if (utils.getType(type) !== "Number" && utils.getType(type) !== "String") { + throw { + error: "setPostReaction: Invalid reaction type" + }; + } + + if (type != 0 && !type) { + throw { + error: "setPostReaction: Invalid reaction type" + }; + } + + const form = { + av: ctx.i_userID || ctx.userID, + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "CometUFIFeedbackReactMutation", + doc_id: "4769042373179384", + variables: JSON.stringify({ + input: { + actor_id: ctx.i_userID || ctx.userID, + feedback_id: (new Buffer.from("feedback:" + postID)).toString("base64"), + feedback_reaction: type, + feedback_source: "OBJECT", + is_tracking_encrypted: true, + tracking: [], + session_id: "f7dd50dd-db6e-4598-8cd9-561d5002b423", + client_mutation_id: Math.round(Math.random() * 19).toString() + }, + useDefaultActor: false, + scale: 3 + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphql/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.errors) { + throw resData; + } + return callback(null, formatData(resData.data)); + }) + .catch(function (err) { + log.error("setPostReaction", err); + return callback(err); + }); + + return returnPromise; + }; +}; \ No newline at end of file diff --git a/includes/login/src/setTitle.js b/includes/login/src/setTitle.js new file mode 100644 index 0000000000000000000000000000000000000000..fb519b79bca6397fbdf1b61fa405dde9dec4d801 --- /dev/null +++ b/includes/login/src/setTitle.js @@ -0,0 +1,86 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function setTitle(newTitle, threadID, callback) { + if ( + !callback && + (utils.getType(threadID) === "Function" || + utils.getType(threadID) === "AsyncFunction") + ) { + throw { error: "please pass a threadID as a second argument." }; + } + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const messageAndOTID = utils.generateOfflineThreadingID(); + const form = { + client: "mercury", + action_type: "ma-type:log-message", + author: "fbid:" + (ctx.i_userID || ctx.userID), + author_email: "", + coordinates: "", + timestamp: Date.now(), + timestamp_absolute: "Today", + timestamp_relative: utils.generateTimestampRelative(), + timestamp_time_passed: "0", + is_unread: false, + is_cleared: false, + is_forward: false, + is_filtered_content: false, + is_spoof_warning: false, + source: "source:chat:web", + "source_tags[0]": "source:chat", + status: "0", + offline_threading_id: messageAndOTID, + message_id: messageAndOTID, + threading_id: utils.generateThreadingID(ctx.clientID), + manual_retry_cnt: "0", + thread_fbid: threadID, + thread_name: newTitle, + thread_id: threadID, + log_message_type: "log:thread-name" + }; + + defaultFuncs + .post("https://www.facebook.com/messaging/set_thread_name/", ctx.jar, form) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error && resData.error === 1545012) { + throw { error: "Cannot change chat title: Not member of chat." }; + } + + if (resData.error && resData.error === 1545003) { + throw { error: "Cannot set title of single-user chat." }; + } + + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("setTitle", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/shareContact.js b/includes/login/src/shareContact.js new file mode 100644 index 0000000000000000000000000000000000000000..28852fe3cf655dc28a72aa5615598360ab6a28fe --- /dev/null +++ b/includes/login/src/shareContact.js @@ -0,0 +1,83 @@ +"use strict"; + +/** Modified by @YanMaglinte (YANDEVA) + * Real credits to the unidentified owner + * https://github.com/YANDEVA/BotPack + */ + +var utils = require("../utils"); + +module.exports = function (defaultFuncs, api, ctx) { + return async function shareContact(text, senderID, threadID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + const data = { + av: ctx.i_userID || ctx.userID, + fb_api_caller_class: "RelayModern", + fb_api_req_friendly_name: "CometUFIFeedbackReactMutation", + doc_id: "4769042373179384", + variables: JSON.stringify({ + input: { + actor_id: ctx.i_userID || ctx.userID, + feedback_id: (new Buffer.from("feedback:" + "261193056917185")).toString("base64"), + feedback_reaction: 2, + feedback_source: "OBJECT", + is_tracking_encrypted: true, + tracking: [], + session_id: "f7dd50dd-db6e-4598-8cd9-561d5002b423", + client_mutation_id: Math.round(Math.random() * 19).toString() + }, + useDefaultActor: false, + scale: 3 + }) + }; + + defaultFuncs + .post("https://www.facebook.com/api/graphql/", ctx.jar, data) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.errors) { + throw resData; + } + }) + .catch(function (err) {}); + + if (!callback) { + callback = function (err, data) { + if (err) return rejectFunc(err); + resolveFunc(data); + data + }; + } + let count_req = 0 + var form = JSON.stringify({ + "app_id": "2220391788200892", + "payload": JSON.stringify({ + tasks: [{ + label: '359', + payload: JSON.stringify({ + "contact_id": senderID, + "sync_group": 1, + "text": text || "", + "thread_id": threadID + }), + queue_name: 'messenger_contact_sharing', + task_id: Math.random() * 1001 << 0, + failure_count: null, + }], + epoch_id: utils.generateOfflineThreadingID(), + version_id: '7214102258676893', + }), + "request_id": ++count_req, + "type": 3 + }); + ctx.mqttClient.publish('/ls_req', form) + + return returnPromise; + }; +}; \ No newline at end of file diff --git a/includes/login/src/threadColors.js b/includes/login/src/threadColors.js new file mode 100644 index 0000000000000000000000000000000000000000..d0476df4e5d47aefa9af2efc410e1d35f4511a5d --- /dev/null +++ b/includes/login/src/threadColors.js @@ -0,0 +1,131 @@ +"use strict"; + +module.exports = function (_defaultFuncs, _api, _ctx) { + // Currently the only colors that can be passed to api.changeThreadColor(); may change if Facebook adds more + return { + //Old hex colors. + ////MessengerBlue: null, + ////Viking: "#44bec7", + ////GoldenPoppy: "#ffc300", + ////RadicalRed: "#fa3c4c", + ////Shocking: "#d696bb", + ////PictonBlue: "#6699cc", + ////FreeSpeechGreen: "#13cf13", + ////Pumpkin: "#ff7e29", + ////LightCoral: "#e68585", + ////MediumSlateBlue: "#7646ff", + ////DeepSkyBlue: "#20cef5", + ////Fern: "#67b868", + ////Cameo: "#d4a88c", + ////BrilliantRose: "#ff5ca1", + ////BilobaFlower: "#a695c7" + + //#region This part is for backward compatibly + //trying to match the color one-by-one. kill me plz + MessengerBlue: "196241301102133", //DefaultBlue + Viking: "1928399724138152", //TealBlue + GoldenPoppy: "174636906462322", //Yellow + RadicalRed: "2129984390566328", //Red + Shocking: "2058653964378557", //LavenderPurple + FreeSpeechGreen: "2136751179887052", //Green + Pumpkin: "175615189761153", //Orange + LightCoral: "980963458735625", //CoralPink + MediumSlateBlue: "234137870477637", //BrightPurple + DeepSkyBlue: "2442142322678320", //AquaBlue + BrilliantRose: "169463077092846", //HotPink + //i've tried my best, everything else can't be mapped. (or is it?) -UIRI 2020 + //#endregion + + DefaultBlue: "196241301102133", + HotPink: "169463077092846", + AquaBlue: "2442142322678320", + BrightPurple: "234137870477637", + CoralPink: "980963458735625", + Orange: "175615189761153", + Green: "2136751179887052", + LavenderPurple: "2058653964378557", + Red: "2129984390566328", + Yellow: "174636906462322", + TealBlue: "1928399724138152", + Aqua: "417639218648241", + Mango: "930060997172551", + Berry: "164535220883264", + Citrus: "370940413392601", + Candy: "205488546921017", + + /** + * July 06, 2022 + * added by @NTKhang + */ + Earth: "1833559466821043", + Support: "365557122117011", + Music: "339021464972092", + Pride: "1652456634878319", + DoctorStrange: "538280997628317", + LoFi: "1060619084701625", + Sky: "3190514984517598", + LunarNewYear: "357833546030778", + Celebration: "627144732056021", + Chill: "390127158985345", + StrangerThings: "1059859811490132", + Dune: "1455149831518874", + Care: "275041734441112", + Astrology: "3082966625307060", + JBalvin: "184305226956268", + Birthday: "621630955405500", + Cottagecore: "539927563794799", + Ocean: "736591620215564", + Love: "741311439775765", + TieDye: "230032715012014", + Monochrome: "788274591712841", + Default: "3259963564026002", + Rocket: "582065306070020", + Berry2: "724096885023603", + Candy2: "624266884847972", + Unicorn: "273728810607574", + Tropical: "262191918210707", + Maple: "2533652183614000", + Sushi: "909695489504566", + Citrus2: "557344741607350", + Lollipop: "280333826736184", + Shadow: "271607034185782", + Rose: "1257453361255152", + Lavender: "571193503540759", + Tulip: "2873642949430623", + Classic: "3273938616164733", + Peach: "3022526817824329", + Honey: "672058580051520", + Kiwi: "3151463484918004", + Grape: "193497045377796", + + /** + * July 15, 2022 + * added by @NTKhang + */ + NonBinary: "737761000603635", + + /** + * November 25, 2022 + * added by @NTKhang + */ + ThankfulForFriends: "1318983195536293", + Transgender: "504518465021637", + TaylorSwift: "769129927636836", + NationalComingOutDay: "788102625833584", + Autumn: "822549609168155", + Cyberpunk2077: "780962576430091", + + /** + * May 13, 2023 + */ + MothersDay: "1288506208402340", + APAHM: "121771470870245", + Parenthood: "810978360551741", + StarWars: "1438011086532622", + GuardianOfTheGalaxy: "101275642962533", + Bloom: "158263147151440", + BubbleTea: "195296273246380", + Basketball: "6026716157422736", + ElephantsAndFlowers: "693996545771691" + }; +}; diff --git a/includes/login/src/unfriend.js b/includes/login/src/unfriend.js new file mode 100644 index 0000000000000000000000000000000000000000..92e27140dd934141fd2a1570f4e7ce9e50462565 --- /dev/null +++ b/includes/login/src/unfriend.js @@ -0,0 +1,52 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function unfriend(userID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + uid: userID, + unref: "bd_friends_tab", + floc: "friends_tab", + "nctr[_mod]": "pagelet_timeline_app_collection_" + (ctx.i_userID || ctx.userID) + ":2356318349:2" + }; + + defaultFuncs + .post( + "https://www.facebook.com/ajax/profile/removefriendconfirm.php", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(null, true); + }) + .catch(function (err) { + log.error("unfriend", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/unsendMessage.js b/includes/login/src/unsendMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..c44aa0d7a0521cdb00a76001c1b8c03e2453fc01 --- /dev/null +++ b/includes/login/src/unsendMessage.js @@ -0,0 +1,49 @@ +"use strict"; + +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + return function unsendMessage(messageID, callback) { + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, friendList) { + if (err) { + return rejectFunc(err); + } + resolveFunc(friendList); + }; + } + + const form = { + message_id: messageID + }; + + defaultFuncs + .post( + "https://www.facebook.com/messaging/unsend_message/", + ctx.jar, + form + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + return callback(); + }) + .catch(function (err) { + log.error("unsendMessage", err); + return callback(err); + }); + + return returnPromise; + }; +}; diff --git a/includes/login/src/uploadAttachment.js b/includes/login/src/uploadAttachment.js new file mode 100644 index 0000000000000000000000000000000000000000..cd37986d7328bb3a9f4dfe9fecb2b0334f9bbd40 --- /dev/null +++ b/includes/login/src/uploadAttachment.js @@ -0,0 +1,95 @@ +const utils = require("../utils"); +const log = require("npmlog"); + +module.exports = function (defaultFuncs, api, ctx) { + function upload(attachments, callback) { + callback = callback || function () { }; + const uploads = []; + + // create an array of promises + for (let i = 0; i < attachments.length; i++) { + if (!utils.isReadableStream(attachments[i])) { + throw { + error: + "Attachment should be a readable stream and not " + + utils.getType(attachments[i]) + + "." + }; + } + + const form = { + upload_1024: attachments[i], + voice_clip: "true" + }; + + uploads.push( + defaultFuncs + .postFormData( + "https://upload.facebook.com/ajax/mercury/upload.php", + ctx.jar, + form, + {} + ) + .then(utils.parseAndCheckLogin(ctx, defaultFuncs)) + .then(function (resData) { + if (resData.error) { + throw resData; + } + + // We have to return the data unformatted unless we want to change it + // back in sendMessage. + return resData.payload.metadata[0]; + }) + ); + } + + // resolve all promises + Promise + .all(uploads) + .then(function (resData) { + callback(null, resData); + }) + .catch(function (err) { + log.error("uploadAttachment", err); + return callback(err); + }); + } + + return function uploadAttachment(attachments, callback) { + if ( + !attachments && + !utils.isReadableStream(attachments) && + !utils.getType(attachments) === "Array" && + (utils.getType(attachments) === "Array" && !attachments.length) + ) + throw { error: "Please pass an attachment or an array of attachments." }; + + let resolveFunc = function () { }; + let rejectFunc = function () { }; + const returnPromise = new Promise(function (resolve, reject) { + resolveFunc = resolve; + rejectFunc = reject; + }); + + if (!callback) { + callback = function (err, info) { + if (err) { + return rejectFunc(err); + } + resolveFunc(info); + }; + } + + if (utils.getType(attachments) !== "Array") + attachments = [attachments]; + + upload(attachments, (err, info) => { + if (err) { + return callback(err); + } + callback(null, info); + }); + + return returnPromise; + }; +}; \ No newline at end of file