Jonell01 commited on
Commit
e192d16
·
verified ·
1 Parent(s): 4505648

Upload 55 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. includes/login/src/addExternalModule.js +19 -0
  2. includes/login/src/addUserToGroup.js +113 -0
  3. includes/login/src/changeAdminStatus.js +79 -0
  4. includes/login/src/changeArchivedStatus.js +55 -0
  5. includes/login/src/changeAvatar.js +126 -0
  6. includes/login/src/changeBio.js +77 -0
  7. includes/login/src/changeBlockedStatus.js +47 -0
  8. includes/login/src/changeGroupImage.js +132 -0
  9. includes/login/src/changeNickname.js +59 -0
  10. includes/login/src/changeThreadColor.js +65 -0
  11. includes/login/src/changeThreadEmoji.js +55 -0
  12. includes/login/src/createNewGroup.js +86 -0
  13. includes/login/src/createPoll.js +71 -0
  14. includes/login/src/deleteMessage.js +56 -0
  15. includes/login/src/deleteThread.js +56 -0
  16. includes/login/src/editMessage.js +59 -0
  17. includes/login/src/forwardAttachment.js +60 -0
  18. includes/login/src/getCurrentUserID.js +7 -0
  19. includes/login/src/getEmojiUrl.js +29 -0
  20. includes/login/src/getFriendsList.js +83 -0
  21. includes/login/src/getMessage.js +796 -0
  22. includes/login/src/getThreadHistory.js +666 -0
  23. includes/login/src/getThreadInfo.js +232 -0
  24. includes/login/src/getThreadList.js +241 -0
  25. includes/login/src/getThreadPictures.js +79 -0
  26. includes/login/src/getUserID.js +66 -0
  27. includes/login/src/getUserInfo.js +74 -0
  28. includes/login/src/handleFriendRequest.js +61 -0
  29. includes/login/src/handleMessageRequest.js +65 -0
  30. includes/login/src/httpGet.js +57 -0
  31. includes/login/src/httpPost.js +57 -0
  32. includes/login/src/httpPostFormData.js +63 -0
  33. includes/login/src/listenMqtt.js +853 -0
  34. includes/login/src/logout.js +75 -0
  35. includes/login/src/markAsDelivered.js +266 -0
  36. includes/login/src/markAsRead.js +80 -0
  37. includes/login/src/markAsReadAll.js +50 -0
  38. includes/login/src/markAsSeen.js +59 -0
  39. includes/login/src/muteThread.js +52 -0
  40. includes/login/src/refreshFb_dtsg.js +81 -0
  41. includes/login/src/removeUserFromGroup.js +79 -0
  42. includes/login/src/resolvePhotoUrl.js +45 -0
  43. includes/login/src/searchForThread.js +53 -0
  44. includes/login/src/sendComment.js +160 -0
  45. includes/login/src/sendMessage.js +447 -0
  46. includes/login/src/sendMessageMqtt.js +316 -0
  47. includes/login/src/sendTypingIndicator.js +103 -0
  48. includes/login/src/setMessageReaction.js +117 -0
  49. includes/login/src/setPostReaction.js +111 -0
  50. includes/login/src/setTitle.js +86 -0
includes/login/src/addExternalModule.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ return function addExternalModule(moduleObj) {
7
+ if (utils.getType(moduleObj) == "Object") {
8
+ for (const apiName in moduleObj) {
9
+ if (utils.getType(moduleObj[apiName]) == "Function") {
10
+ api[apiName] = moduleObj[apiName](defaultFuncs, api, ctx);
11
+ } else {
12
+ throw new Error(`Item "${apiName}" in moduleObj must be a function, not ${utils.getType(moduleObj[apiName])}!`);
13
+ }
14
+ }
15
+ } else {
16
+ throw new Error(`moduleObj must be an object, not ${utils.getType(moduleObj)}!`);
17
+ }
18
+ }
19
+ };
includes/login/src/addUserToGroup.js ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function addUserToGroup(userID, threadID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (
16
+ !callback &&
17
+ (utils.getType(threadID) === "Function" ||
18
+ utils.getType(threadID) === "AsyncFunction")
19
+ ) {
20
+ throw new utils.CustomError({ error: "please pass a threadID as a second argument." });
21
+ }
22
+
23
+ if (!callback) {
24
+ callback = function (err) {
25
+ if (err) {
26
+ return rejectFunc(err);
27
+ }
28
+ resolveFunc();
29
+ };
30
+ }
31
+
32
+ if (
33
+ utils.getType(threadID) !== "Number" &&
34
+ utils.getType(threadID) !== "String"
35
+ ) {
36
+ throw new utils.CustomError({
37
+ error:
38
+ "ThreadID should be of type Number or String and not " +
39
+ utils.getType(threadID) +
40
+ "."
41
+ });
42
+ }
43
+
44
+ if (utils.getType(userID) !== "Array") {
45
+ userID = [userID];
46
+ }
47
+
48
+ const messageAndOTID = utils.generateOfflineThreadingID();
49
+ const form = {
50
+ client: "mercury",
51
+ action_type: "ma-type:log-message",
52
+ author: "fbid:" + (ctx.i_userID || ctx.userID),
53
+ thread_id: "",
54
+ timestamp: Date.now(),
55
+ timestamp_absolute: "Today",
56
+ timestamp_relative: utils.generateTimestampRelative(),
57
+ timestamp_time_passed: "0",
58
+ is_unread: false,
59
+ is_cleared: false,
60
+ is_forward: false,
61
+ is_filtered_content: false,
62
+ is_filtered_content_bh: false,
63
+ is_filtered_content_account: false,
64
+ is_spoof_warning: false,
65
+ source: "source:chat:web",
66
+ "source_tags[0]": "source:chat",
67
+ log_message_type: "log:subscribe",
68
+ status: "0",
69
+ offline_threading_id: messageAndOTID,
70
+ message_id: messageAndOTID,
71
+ threading_id: utils.generateThreadingID(ctx.clientID),
72
+ manual_retry_cnt: "0",
73
+ thread_fbid: threadID
74
+ };
75
+
76
+ for (let i = 0; i < userID.length; i++) {
77
+ if (
78
+ utils.getType(userID[i]) !== "Number" &&
79
+ utils.getType(userID[i]) !== "String"
80
+ ) {
81
+ throw new utils.CustomError({
82
+ error:
83
+ "Elements of userID should be of type Number or String and not " +
84
+ utils.getType(userID[i]) +
85
+ "."
86
+ });
87
+ }
88
+
89
+ form["log_message_data[added_participants][" + i + "]"] =
90
+ "fbid:" + userID[i];
91
+ }
92
+
93
+ defaultFuncs
94
+ .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
95
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
96
+ .then(function (resData) {
97
+ if (!resData) {
98
+ throw new utils.CustomError({ error: "Add to group failed." });
99
+ }
100
+ if (resData.error) {
101
+ throw new utils.CustomError(resData);
102
+ }
103
+
104
+ return callback();
105
+ })
106
+ .catch(function (err) {
107
+ log.error("addUserToGroup", err);
108
+ return callback(err);
109
+ });
110
+
111
+ return returnPromise;
112
+ };
113
+ };
includes/login/src/changeAdminStatus.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeAdminStatus(threadID, adminIDs, adminStatus, callback) {
8
+ if (utils.getType(threadID) !== "String") {
9
+ throw new utils.CustomError({ error: "changeAdminStatus: threadID must be a string" });
10
+ }
11
+
12
+ if (utils.getType(adminIDs) === "String") {
13
+ adminIDs = [adminIDs];
14
+ }
15
+
16
+ if (utils.getType(adminIDs) !== "Array") {
17
+ throw new utils.CustomError({ error: "changeAdminStatus: adminIDs must be an array or string" });
18
+ }
19
+
20
+ if (utils.getType(adminStatus) !== "Boolean") {
21
+ throw new utils.CustomError({ error: "changeAdminStatus: adminStatus must be a string" });
22
+ }
23
+
24
+ let resolveFunc = function () { };
25
+ let rejectFunc = function () { };
26
+ const returnPromise = new Promise(function (resolve, reject) {
27
+ resolveFunc = resolve;
28
+ rejectFunc = reject;
29
+ });
30
+
31
+ if (!callback) {
32
+ callback = function (err) {
33
+ if (err) {
34
+ return rejectFunc(err);
35
+ }
36
+ resolveFunc();
37
+ };
38
+ }
39
+
40
+ if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
41
+ throw new utils.CustomError({ error: "changeAdminStatus: callback is not a function" });
42
+ }
43
+
44
+ const form = {
45
+ "thread_fbid": threadID
46
+ };
47
+
48
+ let i = 0;
49
+ for (const u of adminIDs) {
50
+ form[`admin_ids[${i++}]`] = u;
51
+ }
52
+ form["add"] = adminStatus;
53
+
54
+ defaultFuncs
55
+ .post("https://www.facebook.com/messaging/save_admins/?dpr=1", ctx.jar, form)
56
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
57
+ .then(function (resData) {
58
+ if (resData.error) {
59
+ switch (resData.error) {
60
+ case 1976004:
61
+ throw new utils.CustomError({ error: "Cannot alter admin status: you are not an admin.", rawResponse: resData });
62
+ case 1357031:
63
+ throw new utils.CustomError({ error: "Cannot alter admin status: this thread is not a group chat.", rawResponse: resData });
64
+ default:
65
+ throw new utils.CustomError({ error: "Cannot alter admin status: unknown error.", rawResponse: resData });
66
+ }
67
+ }
68
+
69
+ callback();
70
+ })
71
+ .catch(function (err) {
72
+ log.error("changeAdminStatus", err);
73
+ return callback(err);
74
+ });
75
+
76
+ return returnPromise;
77
+ };
78
+ };
79
+
includes/login/src/changeArchivedStatus.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeArchivedStatus(threadOrThreads, archive, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc();
21
+ };
22
+ }
23
+
24
+ const form = {};
25
+
26
+ if (utils.getType(threadOrThreads) === "Array") {
27
+ for (let i = 0; i < threadOrThreads.length; i++) {
28
+ form["ids[" + threadOrThreads[i] + "]"] = archive;
29
+ }
30
+ } else {
31
+ form["ids[" + threadOrThreads + "]"] = archive;
32
+ }
33
+
34
+ defaultFuncs
35
+ .post(
36
+ "https://www.facebook.com/ajax/mercury/change_archived_status.php",
37
+ ctx.jar,
38
+ form
39
+ )
40
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
41
+ .then(function (resData) {
42
+ if (resData.error) {
43
+ throw resData;
44
+ }
45
+
46
+ return callback();
47
+ })
48
+ .catch(function (err) {
49
+ log.error("changeArchivedStatus", err);
50
+ return callback(err);
51
+ });
52
+
53
+ return returnPromise;
54
+ };
55
+ };
includes/login/src/changeAvatar.js ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ function handleUpload(image, callback) {
8
+ const uploads = [];
9
+
10
+ const form = {
11
+ profile_id: ctx.i_userID || ctx.userID,
12
+ photo_source: 57,
13
+ av: ctx.i_userID || ctx.userID,
14
+ file: image
15
+ };
16
+
17
+ uploads.push(
18
+ defaultFuncs
19
+ .postFormData(
20
+ "https://www.facebook.com/profile/picture/upload/",
21
+ ctx.jar,
22
+ form,
23
+ {}
24
+ )
25
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
26
+ .then(function (resData) {
27
+ if (resData.error) {
28
+ throw resData;
29
+ }
30
+ return resData;
31
+ })
32
+ );
33
+
34
+ // resolve all promises
35
+ Promise
36
+ .all(uploads)
37
+ .then(function (resData) {
38
+ callback(null, resData);
39
+ })
40
+ .catch(function (err) {
41
+ log.error("handleUpload", err);
42
+ return callback(err);
43
+ });
44
+ }
45
+
46
+ return function changeAvatar(image, caption = "", timestamp = null, callback) {
47
+ let resolveFunc = function () { };
48
+ let rejectFunc = function () { };
49
+ const returnPromise = new Promise(function (resolve, reject) {
50
+ resolveFunc = resolve;
51
+ rejectFunc = reject;
52
+ });
53
+
54
+ if (!timestamp && utils.getType(caption) === "Number") {
55
+ timestamp = caption;
56
+ caption = "";
57
+ }
58
+
59
+ if (!timestamp && !callback && (utils.getType(caption) == "Function" || utils.getType(caption) == "AsyncFunction")) {
60
+ callback = caption;
61
+ caption = "";
62
+ timestamp = null;
63
+ }
64
+
65
+ if (!callback) callback = function (err, data) {
66
+ if (err) {
67
+ return rejectFunc(err);
68
+ }
69
+ resolveFunc(data);
70
+ };
71
+
72
+ if (!utils.isReadableStream(image))
73
+ return callback("Image is not a readable stream");
74
+
75
+ handleUpload(image, function (err, payload) {
76
+ if (err) {
77
+ return callback(err);
78
+ }
79
+
80
+ const form = {
81
+ av: ctx.i_userID || ctx.userID,
82
+ fb_api_req_friendly_name: "ProfileCometProfilePictureSetMutation",
83
+ fb_api_caller_class: "RelayModern",
84
+ doc_id: "5066134240065849",
85
+ variables: JSON.stringify({
86
+ input: {
87
+ caption,
88
+ existing_photo_id: payload[0].payload.fbid,
89
+ expiration_time: timestamp,
90
+ profile_id: ctx.i_userID || ctx.userID,
91
+ profile_pic_method: "EXISTING",
92
+ profile_pic_source: "TIMELINE",
93
+ scaled_crop_rect: {
94
+ height: 1,
95
+ width: 1,
96
+ x: 0,
97
+ y: 0
98
+ },
99
+ skip_cropping: true,
100
+ actor_id: ctx.i_userID || ctx.userID,
101
+ client_mutation_id: Math.round(Math.random() * 19).toString()
102
+ },
103
+ isPage: false,
104
+ isProfile: true,
105
+ scale: 3
106
+ })
107
+ };
108
+
109
+ defaultFuncs
110
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
111
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
112
+ .then(function (resData) {
113
+ if (resData.errors) {
114
+ throw resData;
115
+ }
116
+ return callback(null, resData[0].data.profile_picture_set);
117
+ })
118
+ .catch(function (err) {
119
+ log.error("changeAvatar", err);
120
+ return callback(err);
121
+ });
122
+ });
123
+
124
+ return returnPromise;
125
+ };
126
+ };
includes/login/src/changeBio.js ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeBio(bio, publish, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ if (utils.getType(publish) == "Function" || utils.getType(publish) == "AsyncFunction") {
17
+ callback = publish;
18
+ } else {
19
+ callback = function (err) {
20
+ if (err) {
21
+ return rejectFunc(err);
22
+ }
23
+ resolveFunc();
24
+ };
25
+ }
26
+ }
27
+
28
+ if (utils.getType(publish) != "Boolean") {
29
+ publish = false;
30
+ }
31
+
32
+ if (utils.getType(bio) != "String") {
33
+ bio = "";
34
+ publish = false;
35
+ }
36
+
37
+ const form = {
38
+ fb_api_caller_class: "RelayModern",
39
+ fb_api_req_friendly_name: "ProfileCometSetBioMutation",
40
+ // This doc_is is valid as of May 23, 2020
41
+ doc_id: "2725043627607610",
42
+ variables: JSON.stringify({
43
+ input: {
44
+ bio: bio,
45
+ publish_bio_feed_story: publish,
46
+ actor_id: ctx.i_userID || ctx.userID,
47
+ client_mutation_id: Math.round(Math.random() * 1024).toString()
48
+ },
49
+ hasProfileTileViewID: false,
50
+ profileTileViewID: null,
51
+ scale: 1
52
+ }),
53
+ av: ctx.i_userID || ctx.userID
54
+ };
55
+
56
+ defaultFuncs
57
+ .post(
58
+ "https://www.facebook.com/api/graphql/",
59
+ ctx.jar,
60
+ form
61
+ )
62
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
63
+ .then(function (resData) {
64
+ if (resData.errors) {
65
+ throw resData;
66
+ }
67
+
68
+ return callback();
69
+ })
70
+ .catch(function (err) {
71
+ log.error("changeBio", err);
72
+ return callback(err);
73
+ });
74
+
75
+ return returnPromise;
76
+ };
77
+ };
includes/login/src/changeBlockedStatus.js ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeBlockedStatus(userID, block, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc();
21
+ };
22
+ }
23
+
24
+ defaultFuncs
25
+ .post(
26
+ `https://www.facebook.com/messaging/${block ? "" : "un"}block_messages/`,
27
+ ctx.jar,
28
+ {
29
+ fbid: userID
30
+ }
31
+ )
32
+ .then(utils.saveCookies(ctx.jar))
33
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
34
+ .then(function (resData) {
35
+ if (resData.error) {
36
+ throw resData;
37
+ }
38
+
39
+ return callback();
40
+ })
41
+ .catch(function (err) {
42
+ log.error("changeBlockedStatus", err);
43
+ return callback(err);
44
+ });
45
+ return returnPromise;
46
+ };
47
+ };
includes/login/src/changeGroupImage.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ function handleUpload(image, callback) {
8
+ const uploads = [];
9
+
10
+ const form = {
11
+ images_only: "true",
12
+ "attachment[]": image
13
+ };
14
+
15
+ uploads.push(
16
+ defaultFuncs
17
+ .postFormData(
18
+ "https://upload.facebook.com/ajax/mercury/upload.php",
19
+ ctx.jar,
20
+ form,
21
+ {}
22
+ )
23
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
24
+ .then(function (resData) {
25
+ if (resData.error) {
26
+ throw resData;
27
+ }
28
+
29
+ return resData.payload.metadata[0];
30
+ })
31
+ );
32
+
33
+ // resolve all promises
34
+ Promise
35
+ .all(uploads)
36
+ .then(function (resData) {
37
+ callback(null, resData);
38
+ })
39
+ .catch(function (err) {
40
+ log.error("handleUpload", err);
41
+ return callback(err);
42
+ });
43
+ }
44
+
45
+ return function changeGroupImage(image, threadID, callback) {
46
+ if (
47
+ !callback &&
48
+ (utils.getType(threadID) === "Function" ||
49
+ utils.getType(threadID) === "AsyncFunction")
50
+ ) {
51
+ throw { error: "please pass a threadID as a second argument." };
52
+ }
53
+
54
+ if (!utils.isReadableStream(image)) {
55
+ throw { error: "please pass a readable stream as a first argument." };
56
+ }
57
+
58
+ let resolveFunc = function () { };
59
+ let rejectFunc = function () { };
60
+ const returnPromise = new Promise(function (resolve, reject) {
61
+ resolveFunc = resolve;
62
+ rejectFunc = reject;
63
+ });
64
+
65
+ if (!callback) {
66
+ callback = function (err) {
67
+ if (err) {
68
+ return rejectFunc(err);
69
+ }
70
+ resolveFunc();
71
+ };
72
+ }
73
+
74
+ const messageAndOTID = utils.generateOfflineThreadingID();
75
+ const form = {
76
+ client: "mercury",
77
+ action_type: "ma-type:log-message",
78
+ author: "fbid:" + (ctx.i_userID || ctx.userID),
79
+ author_email: "",
80
+ ephemeral_ttl_mode: "0",
81
+ is_filtered_content: false,
82
+ is_filtered_content_account: false,
83
+ is_filtered_content_bh: false,
84
+ is_filtered_content_invalid_app: false,
85
+ is_filtered_content_quasar: false,
86
+ is_forward: false,
87
+ is_spoof_warning: false,
88
+ is_unread: false,
89
+ log_message_type: "log:thread-image",
90
+ manual_retry_cnt: "0",
91
+ message_id: messageAndOTID,
92
+ offline_threading_id: messageAndOTID,
93
+ source: "source:chat:web",
94
+ "source_tags[0]": "source:chat",
95
+ status: "0",
96
+ thread_fbid: threadID,
97
+ thread_id: "",
98
+ timestamp: Date.now(),
99
+ timestamp_absolute: "Today",
100
+ timestamp_relative: utils.generateTimestampRelative(),
101
+ timestamp_time_passed: "0"
102
+ };
103
+
104
+ handleUpload(image, function (err, payload) {
105
+ if (err) {
106
+ return callback(err);
107
+ }
108
+
109
+ form["thread_image_id"] = payload[0]["image_id"];
110
+ form["thread_id"] = threadID;
111
+
112
+ defaultFuncs
113
+ .post("https://www.facebook.com/messaging/set_thread_image/", ctx.jar, form)
114
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
115
+ .then(function (resData) {
116
+ // check for errors here
117
+
118
+ if (resData.error) {
119
+ throw resData;
120
+ }
121
+
122
+ return callback();
123
+ })
124
+ .catch(function (err) {
125
+ log.error("changeGroupImage", err);
126
+ return callback(err);
127
+ });
128
+ });
129
+
130
+ return returnPromise;
131
+ };
132
+ };
includes/login/src/changeNickname.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeNickname(nickname, threadID, participantID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+ if (!callback) {
15
+ callback = function (err) {
16
+ if (err) {
17
+ return rejectFunc(err);
18
+ }
19
+ resolveFunc();
20
+ };
21
+ }
22
+
23
+ const form = {
24
+ nickname: nickname,
25
+ participant_id: participantID,
26
+ thread_or_other_fbid: threadID
27
+ };
28
+
29
+ defaultFuncs
30
+ .post(
31
+ "https://www.facebook.com/messaging/save_thread_nickname/?source=thread_settings&dpr=1",
32
+ ctx.jar,
33
+ form
34
+ )
35
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
36
+ .then(function (resData) {
37
+ if (resData.error === 1545014) {
38
+ throw { error: "Trying to change nickname of user isn't in thread" };
39
+ }
40
+ if (resData.error === 1357031) {
41
+ throw {
42
+ error:
43
+ "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."
44
+ };
45
+ }
46
+ if (resData.error) {
47
+ throw resData;
48
+ }
49
+
50
+ return callback();
51
+ })
52
+ .catch(function (err) {
53
+ log.error("changeNickname", err);
54
+ return callback(err);
55
+ });
56
+
57
+ return returnPromise;
58
+ };
59
+ };
includes/login/src/changeThreadColor.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeThreadColor(color, threadID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(err);
21
+ };
22
+ }
23
+
24
+ if (!isNaN(color)) {
25
+ color = color.toString();
26
+ }
27
+ const validatedColor = color !== null ? color.toLowerCase() : color; // API only accepts lowercase letters in hex string
28
+
29
+ const form = {
30
+ dpr: 1,
31
+ queries: JSON.stringify({
32
+ o0: {
33
+ //This doc_id is valid as of January 31, 2020
34
+ doc_id: "1727493033983591",
35
+ query_params: {
36
+ data: {
37
+ actor_id: ctx.i_userID || ctx.userID,
38
+ client_mutation_id: "0",
39
+ source: "SETTINGS",
40
+ theme_id: validatedColor,
41
+ thread_id: threadID
42
+ }
43
+ }
44
+ }
45
+ })
46
+ };
47
+
48
+ defaultFuncs
49
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
50
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
51
+ .then(function (resData) {
52
+ if (resData[resData.length - 1].error_results > 0) {
53
+ throw new utils.CustomError(resData[0].o0.errors);
54
+ }
55
+
56
+ return callback();
57
+ })
58
+ .catch(function (err) {
59
+ log.error("changeThreadColor", err);
60
+ return callback(err);
61
+ });
62
+
63
+ return returnPromise;
64
+ };
65
+ };
includes/login/src/changeThreadEmoji.js ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function changeThreadEmoji(emoji, threadID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc();
21
+ };
22
+ }
23
+ const form = {
24
+ emoji_choice: emoji,
25
+ thread_or_other_fbid: threadID
26
+ };
27
+
28
+ defaultFuncs
29
+ .post(
30
+ "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
31
+ ctx.jar,
32
+ form
33
+ )
34
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35
+ .then(function (resData) {
36
+ if (resData.error === 1357031) {
37
+ throw {
38
+ error:
39
+ "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."
40
+ };
41
+ }
42
+ if (resData.error) {
43
+ throw resData;
44
+ }
45
+
46
+ return callback();
47
+ })
48
+ .catch(function (err) {
49
+ log.error("changeThreadEmoji", err);
50
+ return callback(err);
51
+ });
52
+
53
+ return returnPromise;
54
+ };
55
+ };
includes/login/src/createNewGroup.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function createNewGroup(participantIDs, groupTitle, callback) {
8
+ if (utils.getType(groupTitle) == "Function") {
9
+ callback = groupTitle;
10
+ groupTitle = null;
11
+ }
12
+
13
+ if (utils.getType(participantIDs) !== "Array") {
14
+ throw { error: "createNewGroup: participantIDs should be an array." };
15
+ }
16
+
17
+ if (participantIDs.length < 2) {
18
+ throw { error: "createNewGroup: participantIDs should have at least 2 IDs." };
19
+ }
20
+
21
+ let resolveFunc = function () { };
22
+ let rejectFunc = function () { };
23
+ const returnPromise = new Promise(function (resolve, reject) {
24
+ resolveFunc = resolve;
25
+ rejectFunc = reject;
26
+ });
27
+
28
+ if (!callback) {
29
+ callback = function (err, threadID) {
30
+ if (err) {
31
+ return rejectFunc(err);
32
+ }
33
+ resolveFunc(threadID);
34
+ };
35
+ }
36
+
37
+ const pids = [];
38
+ for (const n in participantIDs) {
39
+ pids.push({
40
+ fbid: participantIDs[n]
41
+ });
42
+ }
43
+ pids.push({ fbid: ctx.i_userID || ctx.userID });
44
+
45
+ const form = {
46
+ fb_api_caller_class: "RelayModern",
47
+ fb_api_req_friendly_name: "MessengerGroupCreateMutation",
48
+ av: ctx.i_userID || ctx.userID,
49
+ //This doc_id is valid as of January 11th, 2020
50
+ doc_id: "577041672419534",
51
+ variables: JSON.stringify({
52
+ input: {
53
+ entry_point: "jewel_new_group",
54
+ actor_id: ctx.i_userID || ctx.userID,
55
+ participants: pids,
56
+ client_mutation_id: Math.round(Math.random() * 1024).toString(),
57
+ thread_settings: {
58
+ name: groupTitle,
59
+ joinable_mode: "PRIVATE",
60
+ thread_image_fbid: null
61
+ }
62
+ }
63
+ })
64
+ };
65
+
66
+ defaultFuncs
67
+ .post(
68
+ "https://www.facebook.com/api/graphql/",
69
+ ctx.jar,
70
+ form
71
+ )
72
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
73
+ .then(function (resData) {
74
+ if (resData.errors) {
75
+ throw resData;
76
+ }
77
+ return callback(null, resData.data.messenger_group_thread_create.thread.thread_key.thread_fbid);
78
+ })
79
+ .catch(function (err) {
80
+ log.error("createNewGroup", err);
81
+ return callback(err);
82
+ });
83
+
84
+ return returnPromise;
85
+ };
86
+ };
includes/login/src/createPoll.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function createPoll(title, threadID, options, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ if (utils.getType(options) == "Function") {
17
+ callback = options;
18
+ options = null;
19
+ } else {
20
+ callback = function (err) {
21
+ if (err) {
22
+ return rejectFunc(err);
23
+ }
24
+ resolveFunc();
25
+ };
26
+ }
27
+ }
28
+ if (!options) {
29
+ options = {}; // Initial poll options are optional
30
+ }
31
+
32
+ const form = {
33
+ target_id: threadID,
34
+ question_text: title
35
+ };
36
+
37
+ // Set fields for options (and whether they are selected initially by the posting user)
38
+ let ind = 0;
39
+ for (const opt in options) {
40
+ // eslint-disable-next-line no-prototype-builtins
41
+ if (options.hasOwnProperty(opt)) {
42
+ form["option_text_array[" + ind + "]"] = opt;
43
+ form["option_is_selected_array[" + ind + "]"] = options[opt]
44
+ ? "1"
45
+ : "0";
46
+ ind++;
47
+ }
48
+ }
49
+
50
+ defaultFuncs
51
+ .post(
52
+ "https://www.facebook.com/messaging/group_polling/create_poll/?dpr=1",
53
+ ctx.jar,
54
+ form
55
+ )
56
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
57
+ .then(function (resData) {
58
+ if (resData.payload.status != "success") {
59
+ throw resData;
60
+ }
61
+
62
+ return callback();
63
+ })
64
+ .catch(function (err) {
65
+ log.error("createPoll", err);
66
+ return callback(err);
67
+ });
68
+
69
+ return returnPromise;
70
+ };
71
+ };
includes/login/src/deleteMessage.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function deleteMessage(messageOrMessages, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+ if (!callback) {
15
+ callback = function (err) {
16
+ if (err) {
17
+ return rejectFunc(err);
18
+ }
19
+ resolveFunc();
20
+ };
21
+ }
22
+
23
+ const form = {
24
+ client: "mercury"
25
+ };
26
+
27
+ if (utils.getType(messageOrMessages) !== "Array") {
28
+ messageOrMessages = [messageOrMessages];
29
+ }
30
+
31
+ for (let i = 0; i < messageOrMessages.length; i++) {
32
+ form["message_ids[" + i + "]"] = messageOrMessages[i];
33
+ }
34
+
35
+ defaultFuncs
36
+ .post(
37
+ "https://www.facebook.com/ajax/mercury/delete_messages.php",
38
+ ctx.jar,
39
+ form
40
+ )
41
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42
+ .then(function (resData) {
43
+ if (resData.error) {
44
+ throw resData;
45
+ }
46
+
47
+ return callback();
48
+ })
49
+ .catch(function (err) {
50
+ log.error("deleteMessage", err);
51
+ return callback(err);
52
+ });
53
+
54
+ return returnPromise;
55
+ };
56
+ };
includes/login/src/deleteThread.js ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function deleteThread(threadOrThreads, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+ if (!callback) {
15
+ callback = function (err) {
16
+ if (err) {
17
+ return rejectFunc(err);
18
+ }
19
+ resolveFunc();
20
+ };
21
+ }
22
+
23
+ const form = {
24
+ client: "mercury"
25
+ };
26
+
27
+ if (utils.getType(threadOrThreads) !== "Array") {
28
+ threadOrThreads = [threadOrThreads];
29
+ }
30
+
31
+ for (let i = 0; i < threadOrThreads.length; i++) {
32
+ form["ids[" + i + "]"] = threadOrThreads[i];
33
+ }
34
+
35
+ defaultFuncs
36
+ .post(
37
+ "https://www.facebook.com/ajax/mercury/delete_thread.php",
38
+ ctx.jar,
39
+ form
40
+ )
41
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42
+ .then(function (resData) {
43
+ if (resData.error) {
44
+ throw resData;
45
+ }
46
+
47
+ return callback();
48
+ })
49
+ .catch(function (err) {
50
+ log.error("deleteThread", err);
51
+ return callback(err);
52
+ });
53
+
54
+ return returnPromise;
55
+ };
56
+ };
includes/login/src/editMessage.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use_strict";
2
+
3
+ const { generateOfflineThreadingID } = require('../utils');
4
+
5
+ function isCallable(func) {
6
+ try {
7
+ Reflect.apply(func, null, []);
8
+ return true;
9
+ } catch (error) {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ module.exports = function (defaultFuncs, api, ctx) {
15
+
16
+ return function editMessage(text, messageID, callback) {
17
+ if (!ctx.mqttClient) {
18
+ throw new Error('Not connected to MQTT');
19
+ }
20
+
21
+
22
+ ctx.wsReqNumber += 1;
23
+ ctx.wsTaskNumber += 1;
24
+
25
+ const taskPayload = {
26
+ message_id: messageID,
27
+ text: text,
28
+ };
29
+
30
+ const task = {
31
+ failure_count: null,
32
+ label: '742',
33
+ payload: JSON.stringify(taskPayload),
34
+ queue_name: 'edit_message',
35
+ task_id: ctx.wsTaskNumber,
36
+ };
37
+
38
+ const content = {
39
+ app_id: '2220391788200892',
40
+ payload: {
41
+ data_trace_id: null,
42
+ epoch_id: parseInt(generateOfflineThreadingID()),
43
+ tasks: [],
44
+ version_id: '6903494529735864',
45
+ },
46
+ request_id: ctx.wsReqNumber,
47
+ type: 3,
48
+ };
49
+
50
+ content.payload.tasks.push(task);
51
+ content.payload = JSON.stringify(content.payload);
52
+
53
+ if (isCallable(callback)) {
54
+ ctx.reqCallbacks[ctx.wsReqNumber] = callback;
55
+ }
56
+
57
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false });
58
+ };
59
+ }
includes/login/src/forwardAttachment.js ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function forwardAttachment(attachmentID, userOrUsers, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+ if (!callback) {
15
+ callback = function (err) {
16
+ if (err) {
17
+ return rejectFunc(err);
18
+ }
19
+ resolveFunc();
20
+ };
21
+ }
22
+
23
+ const form = {
24
+ attachment_id: attachmentID
25
+ };
26
+
27
+ if (utils.getType(userOrUsers) !== "Array") {
28
+ userOrUsers = [userOrUsers];
29
+ }
30
+
31
+ const timestamp = Math.floor(Date.now() / 1000);
32
+
33
+ for (let i = 0; i < userOrUsers.length; i++) {
34
+ //That's good, the key of the array is really timestmap in seconds + index
35
+ //Probably time when the attachment will be sent?
36
+ form["recipient_map[" + (timestamp + i) + "]"] = userOrUsers[i];
37
+ }
38
+
39
+ defaultFuncs
40
+ .post(
41
+ "https://www.facebook.com/mercury/attachments/forward/",
42
+ ctx.jar,
43
+ form
44
+ )
45
+ .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
46
+ .then(function (resData) {
47
+ if (resData.error) {
48
+ throw resData;
49
+ }
50
+
51
+ return callback();
52
+ })
53
+ .catch(function (err) {
54
+ log.error("forwardAttachment", err);
55
+ return callback(err);
56
+ });
57
+
58
+ return returnPromise;
59
+ };
60
+ };
includes/login/src/getCurrentUserID.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ module.exports = function (defaultFuncs, api, ctx) {
4
+ return function getCurrentUserID() {
5
+ return ctx.i_userID || ctx.userID;
6
+ };
7
+ };
includes/login/src/getEmojiUrl.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const util = require("util");
4
+
5
+ module.exports = function () {
6
+ return function getEmojiUrl(c, size, pixelRatio) {
7
+ /*
8
+ Resolves Facebook Messenger emoji image asset URL for an emoji character.
9
+ Supported sizes are 32, 64, and 128.
10
+ Supported pixel ratios are '1.0' and '1.5' (possibly more; haven't tested)
11
+ */
12
+ const baseUrl = "https://static.xx.fbcdn.net/images/emoji.php/v8/z%s/%s";
13
+ pixelRatio = pixelRatio || "1.0";
14
+
15
+ const ending = util.format(
16
+ "%s/%s/%s.png",
17
+ pixelRatio,
18
+ size,
19
+ c.codePointAt(0).toString(16)
20
+ );
21
+ let base = 317426846;
22
+ for (let i = 0; i < ending.length; i++) {
23
+ base = (base << 5) - base + ending.charCodeAt(i);
24
+ }
25
+
26
+ const hashed = (base & 255).toString(16);
27
+ return util.format(baseUrl, hashed, ending);
28
+ };
29
+ };
includes/login/src/getFriendsList.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ // [almost] copy pasted from one of FB's minified file (GenderConst)
7
+ const GENDERS = {
8
+ 0: "unknown",
9
+ 1: "female_singular",
10
+ 2: "male_singular",
11
+ 3: "female_singular_guess",
12
+ 4: "male_singular_guess",
13
+ 5: "mixed",
14
+ 6: "neuter_singular",
15
+ 7: "unknown_singular",
16
+ 8: "female_plural",
17
+ 9: "male_plural",
18
+ 10: "neuter_plural",
19
+ 11: "unknown_plural"
20
+ };
21
+
22
+ function formatData(obj) {
23
+ return Object.keys(obj).map(function (key) {
24
+ const user = obj[key];
25
+ return {
26
+ alternateName: user.alternateName,
27
+ firstName: user.firstName,
28
+ gender: GENDERS[user.gender],
29
+ userID: utils.formatID(user.id.toString()),
30
+ isFriend: user.is_friend != null && user.is_friend ? true : false,
31
+ fullName: user.name,
32
+ profilePicture: user.thumbSrc,
33
+ type: user.type,
34
+ profileUrl: user.uri,
35
+ vanity: user.vanity,
36
+ isBirthday: !!user.is_birthday
37
+ };
38
+ });
39
+ }
40
+
41
+ module.exports = function (defaultFuncs, api, ctx) {
42
+ return function getFriendsList(callback) {
43
+ let resolveFunc = function () { };
44
+ let rejectFunc = function () { };
45
+ const returnPromise = new Promise(function (resolve, reject) {
46
+ resolveFunc = resolve;
47
+ rejectFunc = reject;
48
+ });
49
+
50
+ if (!callback) {
51
+ callback = function (err, friendList) {
52
+ if (err) {
53
+ return rejectFunc(err);
54
+ }
55
+ resolveFunc(friendList);
56
+ };
57
+ }
58
+
59
+ defaultFuncs
60
+ .postFormData(
61
+ "https://www.facebook.com/chat/user_info_all",
62
+ ctx.jar,
63
+ {},
64
+ { viewer: ctx.i_userID || ctx.userID }
65
+ )
66
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
67
+ .then(function (resData) {
68
+ if (!resData) {
69
+ throw { error: "getFriendsList returned empty object." };
70
+ }
71
+ if (resData.error) {
72
+ throw resData;
73
+ }
74
+ callback(null, formatData(resData.payload));
75
+ })
76
+ .catch(function (err) {
77
+ log.error("getFriendsList", err);
78
+ return callback(err);
79
+ });
80
+
81
+ return returnPromise;
82
+ };
83
+ };
includes/login/src/getMessage.js ADDED
@@ -0,0 +1,796 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ // Original
4
+ /**
5
+ * @author https://github.com/Schmavery/facebook-chat-api/pull/865
6
+ */
7
+
8
+ const utils = require("../utils");
9
+ const log = require("npmlog");
10
+
11
+
12
+ function formatMessage(threadID, data) {
13
+ switch (data.__typename) {
14
+ case "ThreadNameMessage":
15
+ return {
16
+ type: "event",
17
+ threadID: threadID,
18
+ messageID: data.message_id,
19
+ logMessageType: "log:thread-name",
20
+ logMessageData: {
21
+ name: data.thread_name
22
+ },
23
+ logMessageBody: data.snippet,
24
+ timestamp: data.timestamp_precise,
25
+ author: data.message_sender.id
26
+ };
27
+ case "ThreadImageMessage":
28
+ const metadata = data.image_with_metadata;
29
+ return {
30
+ type: "event",
31
+ threadID: threadID,
32
+ messageID: data.message_id,
33
+ logMessageType: "log:thread-image",
34
+ logMessageData: metadata ? {
35
+ attachmentID: metadata.legacy_attachment_id,
36
+ width: metadata.original_dimensions.x,
37
+ height: metadata.original_dimensions.y,
38
+ url: metadata.preview.uri
39
+ } :
40
+ {
41
+ attachmentID: null,
42
+ width: null,
43
+ height: null,
44
+ url: null
45
+ },
46
+ logMessageBody: data.snippet,
47
+ timestamp: data.timestamp_precise,
48
+ author: data.message_sender.id
49
+ };
50
+ case "GenericAdminTextMessage":
51
+ switch (data.extensible_message_admin_text_type) {
52
+ case "CHANGE_THREAD_THEME":
53
+ return {
54
+ type: "event",
55
+ threadID: threadID,
56
+ messageID: data.message_id,
57
+ logMessageType: "log:thread-color",
58
+ logMessageData: colors.find(color => color.theme_color === data.extensible_message_admin_text.theme_color) || {
59
+ theme_color: data.extensible_message_admin_text.theme_color,
60
+ theme_id: null,
61
+ theme_emoji: null,
62
+ gradient: null,
63
+ should_show_icon: null,
64
+ theme_name_with_subtitle: null
65
+ },
66
+ logMessageBody: data.snippet,
67
+ timestamp: data.timestamp_precise,
68
+ author: data.message_sender.id
69
+ };
70
+ case "CHANGE_THREAD_ICON":
71
+ const thread_icon = data.extensible_message_admin_text.thread_icon;
72
+ return {
73
+ type: "event",
74
+ threadID: threadID,
75
+ messageID: data.message_id,
76
+ logMessageType: "log:thread-icon",
77
+ logMessageData: {
78
+ thread_icon_url: `https://static.xx.fbcdn.net/images/emoji.php/v9/t3c/1/16/${thread_icon.codePointAt(0).toString(16)}.png`,
79
+ thread_icon: thread_icon
80
+ },
81
+ logMessageBody: data.snippet,
82
+ timestamp: data.timestamp_precise,
83
+ author: data.message_sender.id
84
+ };
85
+ case "CHANGE_THREAD_NICKNAME":
86
+ return {
87
+ type: "event",
88
+ threadID: threadID,
89
+ messageID: data.message_id,
90
+ logMessageType: "log:user-nickname",
91
+ logMessageData: {
92
+ nickname: data.extensible_message_admin_text.nickname,
93
+ participant_id: data.extensible_message_admin_text.participant_id
94
+ },
95
+ logMessageBody: data.snippet,
96
+ timestamp: data.timestamp_precise,
97
+ author: data.message_sender.id
98
+ };
99
+ case "GROUP_POLL":
100
+ const question = data.extensible_message_admin_text.question;
101
+ return {
102
+ type: "event",
103
+ threadID: threadID,
104
+ messageID: data.message_id,
105
+ logMessageType: "log:thread-poll",
106
+ logMessageData: {
107
+ question_json: JSON.stringify({
108
+ id: question.id,
109
+ text: question.text,
110
+ total_count: data.extensible_message_admin_text.total_count,
111
+ viewer_has_voted: question.viewer_has_voted,
112
+ question_type: "",
113
+ creator_id: data.message_sender.id,
114
+ options: question.options.nodes.map(option => ({
115
+ id: option.id,
116
+ text: option.text,
117
+ total_count: option.voters.nodes.length,
118
+ viewer_has_voted: option.viewer_has_voted,
119
+ voters: option.voters.nodes.map(voter => voter.id)
120
+ }))
121
+ }),
122
+ event_type: data.extensible_message_admin_text.event_type.toLowerCase(),
123
+ question_id: question.id
124
+ },
125
+ logMessageBody: data.snippet,
126
+ timestamp: data.timestamp_precise,
127
+ author: data.message_sender.id
128
+ };
129
+ default:
130
+ 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.`);
131
+ }
132
+ case "UserMessage":
133
+ return {
134
+ senderID: data.message_sender.id,
135
+ body: data.message.text,
136
+ threadID: threadID,
137
+ messageID: data.message_id,
138
+ reactions: data.message_reactions.map(r => ({
139
+ [r.user.id]: r.reaction
140
+ })),
141
+ attachments: data.blob_attachments && data.blob_attachments.length > 0 ?
142
+ data.blob_attachments.length.map(att => {
143
+ let x;
144
+ try {
145
+ x = utils._formatAttachment(att);
146
+ } catch (ex) {
147
+ x = att;
148
+ x.error = ex;
149
+ x.type = "unknown";
150
+ }
151
+ return x;
152
+ }) :
153
+ data.extensible_attachment && Object.keys(data.extensible_attachment).length > 0 ?
154
+ [{
155
+ type: "share",
156
+ ID: data.extensible_attachment.legacy_attachment_id,
157
+ url: data.extensible_attachment.story_attachment.url,
158
+
159
+ title: data.extensible_attachment.story_attachment.title_with_entities.text,
160
+ description: data.extensible_attachment.story_attachment.description.text,
161
+ source: data.extensible_attachment.story_attachment.source,
162
+
163
+ image: ((data.extensible_attachment.story_attachment.media || {}).image || {}).uri,
164
+ width: ((data.extensible_attachment.story_attachment.media || {}).image || {}).width,
165
+ height: ((data.extensible_attachment.story_attachment.media || {}).image || {}).height,
166
+ playable: (data.extensible_attachment.story_attachment.media || {}).is_playable || false,
167
+ duration: (data.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
168
+
169
+ subattachments: data.extensible_attachment.subattachments,
170
+ properties: data.extensible_attachment.story_attachment.properties
171
+ }] :
172
+ [],
173
+ mentions: data.message.ranges.map(mention => ({
174
+ [mention.entity.id]: data.message.text.substring(mention.offset, mention.offset + mention.length)
175
+ })),
176
+ timestamp: data.timestamp_precise
177
+ };
178
+ default:
179
+ 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.`);
180
+ // If this happens to you let me know when it happens
181
+ // Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.
182
+ // return Object.assign({ type: "unknown", data });
183
+ }
184
+ }
185
+
186
+
187
+ function parseDelta(threadID, delta) {
188
+ if (delta.replied_to_message) {
189
+ return Object.assign({
190
+ type: "message_reply"
191
+ }, formatMessage(threadID, delta), {
192
+ messageReply: formatMessage(threadID, delta.replied_to_message.message)
193
+ });
194
+ }
195
+ else {
196
+ return formatMessage(threadID, delta);
197
+ }
198
+ }
199
+
200
+
201
+ module.exports = function (defaultFuncs, api, ctx) {
202
+ return function getMessage(threadID, messageID, callback) {
203
+ let resolveFunc = function () { };
204
+ let rejectFunc = function () { };
205
+ const returnPromise = new Promise(function (resolve, reject) {
206
+ resolveFunc = resolve;
207
+ rejectFunc = reject;
208
+ });
209
+
210
+ if (!callback) {
211
+ callback = function (err, info) {
212
+ if (err)
213
+ return rejectFunc(err);
214
+ resolveFunc(info);
215
+ };
216
+ }
217
+
218
+ if (!threadID || !messageID) {
219
+ return callback({ error: "getMessage: need threadID and messageID" });
220
+ }
221
+
222
+ const form = {
223
+ "av": ctx.globalOptions.pageID,
224
+ "queries": JSON.stringify({
225
+ "o0": {
226
+ //This doc_id is valid as of ? (prob January 18, 2020)
227
+ "doc_id": "1768656253222505",
228
+ "query_params": {
229
+ "thread_and_message_id": {
230
+ "thread_id": threadID,
231
+ "message_id": messageID
232
+ }
233
+ }
234
+ }
235
+ })
236
+ };
237
+
238
+ defaultFuncs
239
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
240
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
241
+ .then((resData) => {
242
+ if (resData[resData.length - 1].error_results > 0) {
243
+ throw resData[0].o0.errors;
244
+ }
245
+
246
+ if (resData[resData.length - 1].successful_results === 0) {
247
+ throw { error: "getMessage: there was no successful_results", res: resData };
248
+ }
249
+
250
+ const fetchData = resData[0].o0.data.message;
251
+ if (fetchData) {
252
+ callback(null, parseDelta(threadID, fetchData));
253
+ }
254
+ else {
255
+ throw fetchData;
256
+ }
257
+ })
258
+ .catch((err) => {
259
+ log.error("getMessage", err);
260
+ callback(err);
261
+ });
262
+
263
+ return returnPromise;
264
+ };
265
+ };
266
+
267
+ const colors = [
268
+ {
269
+ theme_color: 'FF000000',
270
+ theme_id: '788274591712841',
271
+ theme_emoji: '🖤',
272
+ gradient: '["FFF0F0F0"]',
273
+ should_show_icon: '',
274
+ theme_name_with_subtitle: 'Monochrome'
275
+ },
276
+ {
277
+ theme_color: 'FFFF5CA1',
278
+ theme_id: '169463077092846',
279
+ theme_emoji: null,
280
+ gradient: null,
281
+ should_show_icon: '1',
282
+ theme_name_with_subtitle: 'Hot Pink'
283
+ },
284
+ {
285
+ theme_color: 'FF2825B5',
286
+ theme_id: '271607034185782',
287
+ theme_emoji: null,
288
+ gradient: '["FF5E007E","FF331290","FF2825B5"]',
289
+ should_show_icon: '1',
290
+ theme_name_with_subtitle: 'Shadow'
291
+ },
292
+ {
293
+ theme_color: 'FFD9A900',
294
+ theme_id: '2533652183614000',
295
+ theme_emoji: null,
296
+ gradient: '["FF550029","FFAA3232","FFD9A900"]',
297
+ should_show_icon: '1',
298
+ theme_name_with_subtitle: 'Maple'
299
+ },
300
+ {
301
+ theme_color: 'FFFB45DE',
302
+ theme_id: '2873642949430623',
303
+ theme_emoji: null,
304
+ gradient: null,
305
+ should_show_icon: '1',
306
+ theme_name_with_subtitle: 'Tulip'
307
+ },
308
+ {
309
+ theme_color: 'FF5E007E',
310
+ theme_id: '193497045377796',
311
+ theme_emoji: null,
312
+ gradient: null,
313
+ should_show_icon: '1',
314
+ theme_name_with_subtitle: 'Grape'
315
+ },
316
+ {
317
+ theme_color: 'FF7AA286',
318
+ theme_id: '1455149831518874',
319
+ theme_emoji: '🌑',
320
+ gradient: '["FF25C0E1","FFCE832A"]',
321
+ should_show_icon: '',
322
+ theme_name_with_subtitle: 'Dune'
323
+ },
324
+ {
325
+ theme_color: 'FFFAAF00',
326
+ theme_id: '672058580051520',
327
+ theme_emoji: null,
328
+ gradient: null,
329
+ should_show_icon: '1',
330
+ theme_name_with_subtitle: 'Honey'
331
+ },
332
+ {
333
+ theme_color: 'FF0084FF',
334
+ theme_id: '196241301102133',
335
+ theme_emoji: null,
336
+ gradient: null,
337
+ should_show_icon: '1',
338
+ theme_name_with_subtitle: 'Default Blue'
339
+ },
340
+ {
341
+ theme_color: 'FFFFC300',
342
+ theme_id: '174636906462322',
343
+ theme_emoji: null,
344
+ gradient: null,
345
+ should_show_icon: '1',
346
+ theme_name_with_subtitle: 'Yellow'
347
+ },
348
+ {
349
+ theme_color: 'FF44BEC7',
350
+ theme_id: '1928399724138152',
351
+ theme_emoji: null,
352
+ gradient: null,
353
+ should_show_icon: '1',
354
+ theme_name_with_subtitle: 'Teal Blue'
355
+ },
356
+ {
357
+ theme_color: 'FF7646FF',
358
+ theme_id: '234137870477637',
359
+ theme_emoji: null,
360
+ gradient: null,
361
+ should_show_icon: '1',
362
+ theme_name_with_subtitle: 'Bright Purple'
363
+ },
364
+ {
365
+ theme_color: 'FFF25C54',
366
+ theme_id: '3022526817824329',
367
+ theme_emoji: null,
368
+ gradient: null,
369
+ should_show_icon: '1',
370
+ theme_name_with_subtitle: 'Peach'
371
+ },
372
+ {
373
+ theme_color: 'FFF01D6A',
374
+ theme_id: '724096885023603',
375
+ theme_emoji: null,
376
+ gradient: '["FF005FFF","FF9200FF","FFFF2E19"]',
377
+ should_show_icon: '1',
378
+ theme_name_with_subtitle: 'Berry'
379
+ },
380
+ {
381
+ theme_color: 'FFFF7CA8',
382
+ theme_id: '624266884847972',
383
+ theme_emoji: null,
384
+ gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]',
385
+ should_show_icon: '1',
386
+ theme_name_with_subtitle: 'Candy'
387
+ },
388
+ {
389
+ theme_color: 'FF6E5B04',
390
+ theme_id: '365557122117011',
391
+ theme_emoji: '💛',
392
+ gradient: '["FFED9F9A","FFED9F9A","FFED9F9A"]',
393
+ should_show_icon: '',
394
+ theme_name_with_subtitle: 'Support'
395
+ },
396
+ {
397
+ theme_color: 'FF0052CD',
398
+ theme_id: '230032715012014',
399
+ theme_emoji: '✌️',
400
+ gradient: '["FF0052CD","FF00A1E6","FF0052CD"]',
401
+ should_show_icon: '',
402
+ theme_name_with_subtitle: 'Tie-Dye'
403
+ },
404
+ {
405
+ theme_color: 'FF601DDD',
406
+ theme_id: '1060619084701625',
407
+ theme_emoji: '☁️',
408
+ gradient: '["FFCA34FF","FF302CFF","FFBA009C"]',
409
+ should_show_icon: '',
410
+ theme_name_with_subtitle: 'Lo-Fi'
411
+ },
412
+ {
413
+ theme_color: 'FF0099FF',
414
+ theme_id: '3273938616164733',
415
+ theme_emoji: null,
416
+ gradient: null,
417
+ should_show_icon: '1',
418
+ theme_name_with_subtitle: 'Classic'
419
+ },
420
+ {
421
+ theme_color: 'FF1ADB5B',
422
+ theme_id: '370940413392601',
423
+ theme_emoji: null,
424
+ gradient: '["FFFFD200","FF6EDF00","FF00DFBB"]',
425
+ should_show_icon: '1',
426
+ theme_name_with_subtitle: 'Citrus'
427
+ },
428
+ {
429
+ theme_color: 'FFD696BB',
430
+ theme_id: '2058653964378557',
431
+ theme_emoji: null,
432
+ gradient: null,
433
+ should_show_icon: '1',
434
+ theme_name_with_subtitle: 'Lavender Purple'
435
+ },
436
+ {
437
+ theme_color: 'FFC03232',
438
+ theme_id: '1059859811490132',
439
+ theme_emoji: '🙃',
440
+ gradient: '["FFDB4040","FFA32424"]',
441
+ should_show_icon: '',
442
+ theme_name_with_subtitle: 'Stranger Things'
443
+ },
444
+ {
445
+ theme_color: 'FFFA3C4C',
446
+ theme_id: '2129984390566328',
447
+ theme_emoji: null,
448
+ gradient: null,
449
+ should_show_icon: '1',
450
+ theme_name_with_subtitle: 'Red'
451
+ },
452
+ {
453
+ theme_color: 'FF13CF13',
454
+ theme_id: '2136751179887052',
455
+ theme_emoji: null,
456
+ gradient: null,
457
+ should_show_icon: '1',
458
+ theme_name_with_subtitle: 'Green'
459
+ },
460
+ {
461
+ theme_color: 'FFFF7E29',
462
+ theme_id: '175615189761153',
463
+ theme_emoji: null,
464
+ gradient: null,
465
+ should_show_icon: '1',
466
+ theme_name_with_subtitle: 'Orange'
467
+ },
468
+ {
469
+ theme_color: 'FFE68585',
470
+ theme_id: '980963458735625',
471
+ theme_emoji: null,
472
+ gradient: null,
473
+ should_show_icon: '1',
474
+ theme_name_with_subtitle: 'Coral Pink'
475
+ },
476
+ {
477
+ theme_color: 'FF20CEF5',
478
+ theme_id: '2442142322678320',
479
+ theme_emoji: null,
480
+ gradient: null,
481
+ should_show_icon: '1',
482
+ theme_name_with_subtitle: 'Aqua Blue'
483
+ },
484
+ {
485
+ theme_color: 'FF0EDCDE',
486
+ theme_id: '417639218648241',
487
+ theme_emoji: null,
488
+ gradient: '["FF19C9FF","FF00E6D2","FF0EE6B7"]',
489
+ should_show_icon: '1',
490
+ theme_name_with_subtitle: 'Aqua'
491
+ },
492
+ {
493
+ theme_color: 'FFFF9C19',
494
+ theme_id: '930060997172551',
495
+ theme_emoji: null,
496
+ gradient: '["FFFFDC2D","FFFF9616","FFFF4F00"]',
497
+ should_show_icon: '1',
498
+ theme_name_with_subtitle: 'Mango'
499
+ },
500
+ {
501
+ theme_color: 'FFF01D6A',
502
+ theme_id: '164535220883264',
503
+ theme_emoji: null,
504
+ gradient: '["FF005FFF","FF9200FF","FFFF2E19"]',
505
+ should_show_icon: '1',
506
+ theme_name_with_subtitle: 'Berry'
507
+ },
508
+ {
509
+ theme_color: 'FFFF7CA8',
510
+ theme_id: '205488546921017',
511
+ theme_emoji: null,
512
+ gradient: '["FFFF8FB2","FFA797FF","FF00E5FF"]',
513
+ should_show_icon: '1',
514
+ theme_name_with_subtitle: 'Candy'
515
+ },
516
+ {
517
+ theme_color: 'FFFF6F07',
518
+ theme_id: '1833559466821043',
519
+ theme_emoji: '🌎',
520
+ gradient: '["FFFF6F07"]',
521
+ should_show_icon: '',
522
+ theme_name_with_subtitle: 'Earth'
523
+ },
524
+ {
525
+ theme_color: 'FF0B0085',
526
+ theme_id: '339021464972092',
527
+ theme_emoji: '🔈',
528
+ gradient: '["FF2FA9E4","FF648FEB","FF9B73F2"]',
529
+ should_show_icon: '',
530
+ theme_name_with_subtitle: 'Music'
531
+ },
532
+ {
533
+ theme_color: 'FF8A39EF',
534
+ theme_id: '1652456634878319',
535
+ theme_emoji: '🏳️‍🌈',
536
+ gradient: '["FFFF0018","FFFF0417","FFFF310E","FFFF5D06","FFFF7A01","FFFF8701","FFFFB001","FFD9C507","FF79C718","FF01C92D","FF01BE69","FF01B3AA","FF0BA1DF","FF3F77E6","FF724CEC","FF8A39EF","FF8A39EF"]',
537
+ should_show_icon: '',
538
+ theme_name_with_subtitle: 'Pride'
539
+ },
540
+ {
541
+ theme_color: 'FF004D7C',
542
+ theme_id: '538280997628317',
543
+ theme_emoji: '🌀',
544
+ gradient: '["FF931410","FF931410","FF931410"]',
545
+ should_show_icon: '',
546
+ theme_name_with_subtitle: 'Doctor Strange'
547
+ },
548
+ {
549
+ theme_color: 'FF4F4DFF',
550
+ theme_id: '3190514984517598',
551
+ theme_emoji: '🌤',
552
+ gradient: '["FF0080FF","FF9F1AFF"]',
553
+ should_show_icon: '',
554
+ theme_name_with_subtitle: 'Sky'
555
+ },
556
+ {
557
+ theme_color: 'FFE84B28',
558
+ theme_id: '357833546030778',
559
+ theme_emoji: '🐯',
560
+ gradient: '["FFF69500","FFDA0050"]',
561
+ should_show_icon: '1',
562
+ theme_name_with_subtitle: 'Lunar New Year'
563
+ },
564
+ {
565
+ theme_color: 'FFB24B77',
566
+ theme_id: '627144732056021',
567
+ theme_emoji: '🥳',
568
+ gradient: '["FFF1614E","FF660F84"]',
569
+ should_show_icon: '',
570
+ theme_name_with_subtitle: 'Celebration!'
571
+ },
572
+ {
573
+ theme_color: 'FF66A9FF',
574
+ theme_id: '390127158985345',
575
+ theme_emoji: '🥶',
576
+ gradient: '["FF8CB3FF","FF409FFF"]',
577
+ should_show_icon: '',
578
+ theme_name_with_subtitle: 'Chill'
579
+ },
580
+ {
581
+ theme_color: 'FF5797FC',
582
+ theme_id: '275041734441112',
583
+ theme_emoji: '😌',
584
+ gradient: '["FF4AC9E4","FF5890FF","FF8C91FF"]',
585
+ should_show_icon: '',
586
+ theme_name_with_subtitle: 'Care'
587
+ },
588
+ {
589
+ theme_color: 'FFFF595C',
590
+ theme_id: '3082966625307060',
591
+ theme_emoji: '💫',
592
+ gradient: '["FFFF239A","FFFF8C21"]',
593
+ should_show_icon: '',
594
+ theme_name_with_subtitle: 'Astrology'
595
+ },
596
+ {
597
+ theme_color: 'FF0171FF',
598
+ theme_id: '184305226956268',
599
+ theme_emoji: '☁️',
600
+ gradient: '["FF0026ee","FF00b2ff"]',
601
+ should_show_icon: '',
602
+ theme_name_with_subtitle: 'J Balvin'
603
+ },
604
+ {
605
+ theme_color: 'FFA033FF',
606
+ theme_id: '621630955405500',
607
+ theme_emoji: '🎉',
608
+ gradient: '["FFFF7061","FFFF5280","FFA033FF","FF0099FF"]',
609
+ should_show_icon: '',
610
+ theme_name_with_subtitle: 'Birthday'
611
+ },
612
+ {
613
+ theme_color: 'FF006528',
614
+ theme_id: '539927563794799',
615
+ theme_emoji: '🍄',
616
+ gradient: '["FF00d52f","FF006528"]',
617
+ should_show_icon: '',
618
+ theme_name_with_subtitle: 'Cottagecore'
619
+ },
620
+ {
621
+ theme_color: 'FF4D3EC2',
622
+ theme_id: '736591620215564',
623
+ theme_emoji: null,
624
+ gradient: null,
625
+ should_show_icon: '1',
626
+ theme_name_with_subtitle: 'Ocean'
627
+ },
628
+ {
629
+ theme_color: 'FF4e4bf5',
630
+ theme_id: '3259963564026002',
631
+ theme_emoji: null,
632
+ gradient: '["FFAA00FF","FF0080FF"]',
633
+ should_show_icon: '1',
634
+ theme_name_with_subtitle: 'Default'
635
+ },
636
+ {
637
+ theme_color: 'FF3A12FF',
638
+ theme_id: '582065306070020',
639
+ theme_emoji: null,
640
+ gradient: '["FFFAAF00","FFFF2E2E","FF3A12FF"]',
641
+ should_show_icon: '1',
642
+ theme_name_with_subtitle: 'Rocket'
643
+ },
644
+ {
645
+ theme_color: 'FF3A1D8A',
646
+ theme_id: '273728810607574',
647
+ theme_emoji: null,
648
+ gradient: '["FFFB45DE","FF841DD5","FF3A1D8A"]',
649
+ should_show_icon: '1',
650
+ theme_name_with_subtitle: 'Unicorn'
651
+ },
652
+ {
653
+ theme_color: 'FF9FD52D',
654
+ theme_id: '262191918210707',
655
+ theme_emoji: null,
656
+ gradient: '["FF2A7FE3","FF00BF91","FF9FD52D"]',
657
+ should_show_icon: '1',
658
+ theme_name_with_subtitle: 'Tropical'
659
+ },
660
+ {
661
+ theme_color: 'FFF7B267',
662
+ theme_id: '909695489504566',
663
+ theme_emoji: null,
664
+ gradient: '["FFF25C54","FFF4845F","FFF7B267"]',
665
+ should_show_icon: '1',
666
+ theme_name_with_subtitle: 'Sushi'
667
+ },
668
+ {
669
+ theme_color: 'FF1ADB5B',
670
+ theme_id: '557344741607350',
671
+ theme_emoji: null,
672
+ gradient: '["FFFFD200","FF6EDF00","FF00DFBB"]',
673
+ should_show_icon: '1',
674
+ theme_name_with_subtitle: 'Citrus'
675
+ },
676
+ {
677
+ theme_color: 'FF4D3EC2',
678
+ theme_id: '280333826736184',
679
+ theme_emoji: null,
680
+ gradient: '["FFFF625B","FFC532AD","FF4D3EC2"]',
681
+ should_show_icon: '1',
682
+ theme_name_with_subtitle: 'Lollipop'
683
+ },
684
+ {
685
+ theme_color: 'FFFF311E',
686
+ theme_id: '1257453361255152',
687
+ theme_emoji: null,
688
+ gradient: null,
689
+ should_show_icon: '1',
690
+ theme_name_with_subtitle: 'Rose'
691
+ },
692
+ {
693
+ theme_color: 'FFA797FF',
694
+ theme_id: '571193503540759',
695
+ theme_emoji: null,
696
+ gradient: null,
697
+ should_show_icon: '1',
698
+ theme_name_with_subtitle: 'Lavender'
699
+ },
700
+ {
701
+ theme_color: 'FF6EDF00',
702
+ theme_id: '3151463484918004',
703
+ theme_emoji: null,
704
+ gradient: null,
705
+ should_show_icon: '1',
706
+ theme_name_with_subtitle: 'Kiwi'
707
+ },
708
+ {
709
+ theme_color: 'FF9D59D2',
710
+ theme_id: '737761000603635',
711
+ theme_emoji: '💛',
712
+ gradient: '["FFFFD600","FFFCB37B","FF9D59D2","FF282828"]',
713
+ should_show_icon: '',
714
+ theme_name_with_subtitle: 'Non-Binary'
715
+ },
716
+ {
717
+ theme_color: 'FF57C39C',
718
+ theme_id: '1288506208402340',
719
+ theme_emoji: '💐',
720
+ gradient: '["FF57C39C","FF57C39C","FF57C39C"]',
721
+ should_show_icon: '',
722
+ theme_name_with_subtitle: 'Mother\'s Day'
723
+ },
724
+ {
725
+ theme_color: 'FFF65900',
726
+ theme_id: '121771470870245',
727
+ theme_emoji: '💥',
728
+ gradient: '["FFFF8328","FFFF7014","FFFF5C00"]',
729
+ should_show_icon: '',
730
+ theme_name_with_subtitle: 'APAHM'
731
+ },
732
+ {
733
+ theme_color: 'FF978E21',
734
+ theme_id: '810978360551741',
735
+ theme_emoji: '🪺',
736
+ gradient: '["FF978E21","FF978E21","FF978E21"]',
737
+ should_show_icon: '',
738
+ theme_name_with_subtitle: 'Parenthood'
739
+ },
740
+ {
741
+ theme_color: 'FFDD8800',
742
+ theme_id: '1438011086532622',
743
+ theme_emoji: '✨',
744
+ gradient: '["FFEDAB00","FFCD6300"]',
745
+ should_show_icon: '',
746
+ theme_name_with_subtitle: 'Star Wars'
747
+ },
748
+ {
749
+ theme_color: 'FF7D09B9',
750
+ theme_id: '101275642962533',
751
+ theme_emoji: '🚀',
752
+ gradient: '["FF5C22D6","FF8F1EB5","FFC31B92"]',
753
+ should_show_icon: '',
754
+ theme_name_with_subtitle: 'Guardians of the Galaxy'
755
+ },
756
+ {
757
+ theme_color: 'FF61C500',
758
+ theme_id: '158263147151440',
759
+ theme_emoji: '🐝',
760
+ gradient: '["FF61C500","FF61C500","FF61C500"]',
761
+ should_show_icon: '',
762
+ theme_name_with_subtitle: 'Bloom'
763
+ },
764
+ {
765
+ theme_color: 'FF826044',
766
+ theme_id: '195296273246380',
767
+ theme_emoji: '🧋',
768
+ gradient: '["FF886546","FFA27F57","FFC6A26E"]',
769
+ should_show_icon: '',
770
+ theme_name_with_subtitle: 'Bubble Tea'
771
+ },
772
+ {
773
+ theme_color: 'FFB02501',
774
+ theme_id: '6026716157422736',
775
+ theme_emoji: '⛹️',
776
+ gradient: '["FFAC2503","FFB02501","FFB42400"]',
777
+ should_show_icon: '',
778
+ theme_name_with_subtitle: 'Basketball'
779
+ },
780
+ {
781
+ theme_color: 'FF574DC1',
782
+ theme_id: '693996545771691',
783
+ theme_emoji: '🖤',
784
+ gradient: '["FF4533FF","FF574AE0","FF6C64BE"]',
785
+ should_show_icon: '',
786
+ theme_name_with_subtitle: 'Elephants&Flowers'
787
+ },
788
+ {
789
+ theme_color: 'FFA8B8DA',
790
+ theme_id: '504518465021637',
791
+ theme_emoji: '🏳️‍⚧️',
792
+ gradient: '["FF55D0FF","FF7597D7","FFFF9FB3","FFFF9FB3"]',
793
+ should_show_icon: '',
794
+ theme_name_with_subtitle: 'Transgender'
795
+ }
796
+ ];
includes/login/src/getThreadHistory.js ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+
7
+ function getExtension(original_extension, filename = "") {
8
+ if (original_extension) {
9
+ return original_extension;
10
+ }
11
+ else {
12
+ const extension = filename.split(".").pop();
13
+ if (extension === filename) {
14
+ return "";
15
+ }
16
+ else {
17
+ return extension;
18
+ }
19
+ }
20
+ }
21
+
22
+ function formatAttachmentsGraphQLResponse(attachment) {
23
+ switch (attachment.__typename) {
24
+ case "MessageImage":
25
+ return {
26
+ type: "photo",
27
+ ID: attachment.legacy_attachment_id,
28
+ filename: attachment.filename,
29
+ original_extension: getExtension(attachment.original_extension, attachment.filename),
30
+ thumbnailUrl: attachment.thumbnail.uri,
31
+
32
+ previewUrl: attachment.preview.uri,
33
+ previewWidth: attachment.preview.width,
34
+ previewHeight: attachment.preview.height,
35
+
36
+ largePreviewUrl: attachment.large_preview.uri,
37
+ largePreviewHeight: attachment.large_preview.height,
38
+ largePreviewWidth: attachment.large_preview.width,
39
+
40
+ // You have to query for the real image. See below.
41
+ url: attachment.large_preview.uri, // @Legacy
42
+ width: attachment.large_preview.width, // @Legacy
43
+ height: attachment.large_preview.height, // @Legacy
44
+ name: attachment.filename, // @Legacy
45
+
46
+ // @Undocumented
47
+ attributionApp: attachment.attribution_app
48
+ ? {
49
+ attributionAppID: attachment.attribution_app.id,
50
+ name: attachment.attribution_app.name,
51
+ logo: attachment.attribution_app.square_logo
52
+ }
53
+ : null
54
+
55
+ // @TODO No idea what this is, should we expose it?
56
+ // Ben - July 15th 2017
57
+ // renderAsSticker: attachment.render_as_sticker,
58
+
59
+ // This is _not_ the real URI, this is still just a large preview.
60
+ // To get the URL we'll need to support a POST query to
61
+ //
62
+ // https://www.facebook.com/webgraphql/query/
63
+ //
64
+ // With the following query params:
65
+ //
66
+ // query_id:728987990612546
67
+ // variables:{"id":"100009069356507","photoID":"10213724771692996"}
68
+ // dpr:1
69
+ //
70
+ // No special form though.
71
+ };
72
+ case "MessageAnimatedImage":
73
+ return {
74
+ type: "animated_image",
75
+ ID: attachment.legacy_attachment_id,
76
+ filename: attachment.filename,
77
+ original_extension: getExtension(attachment.original_extension, attachment.filename),
78
+
79
+ previewUrl: attachment.preview_image.uri,
80
+ previewWidth: attachment.preview_image.width,
81
+ previewHeight: attachment.preview_image.height,
82
+
83
+ url: attachment.animated_image.uri,
84
+ width: attachment.animated_image.width,
85
+ height: attachment.animated_image.height,
86
+
87
+ thumbnailUrl: attachment.preview_image.uri, // @Legacy
88
+ name: attachment.filename, // @Legacy
89
+ facebookUrl: attachment.animated_image.uri, // @Legacy
90
+ rawGifImage: attachment.animated_image.uri, // @Legacy
91
+ animatedGifUrl: attachment.animated_image.uri, // @Legacy
92
+ animatedGifPreviewUrl: attachment.preview_image.uri, // @Legacy
93
+ animatedWebpUrl: attachment.animated_image.uri, // @Legacy
94
+ animatedWebpPreviewUrl: attachment.preview_image.uri, // @Legacy
95
+
96
+ // @Undocumented
97
+ attributionApp: attachment.attribution_app
98
+ ? {
99
+ attributionAppID: attachment.attribution_app.id,
100
+ name: attachment.attribution_app.name,
101
+ logo: attachment.attribution_app.square_logo
102
+ }
103
+ : null
104
+ };
105
+ case "MessageVideo":
106
+ return {
107
+ type: "video",
108
+ ID: attachment.legacy_attachment_id,
109
+ filename: attachment.filename,
110
+ original_extension: getExtension(attachment.original_extension, attachment.filename),
111
+ duration: attachment.playable_duration_in_ms,
112
+
113
+ thumbnailUrl: attachment.large_image.uri, // @Legacy
114
+
115
+ previewUrl: attachment.large_image.uri,
116
+ previewWidth: attachment.large_image.width,
117
+ previewHeight: attachment.large_image.height,
118
+
119
+ url: attachment.playable_url,
120
+ width: attachment.original_dimensions.x,
121
+ height: attachment.original_dimensions.y,
122
+
123
+ videoType: attachment.video_type.toLowerCase()
124
+ };
125
+ case "MessageFile":
126
+ return {
127
+ type: "file",
128
+ ID: attachment.message_file_fbid,
129
+ filename: attachment.filename,
130
+ original_extension: getExtension(attachment.original_extension, attachment.filename),
131
+
132
+ url: attachment.url,
133
+ isMalicious: attachment.is_malicious,
134
+ contentType: attachment.content_type,
135
+
136
+ name: attachment.filename, // @Legacy
137
+ mimeType: "", // @Legacy
138
+ fileSize: -1 // @Legacy
139
+ };
140
+ case "MessageAudio":
141
+ return {
142
+ type: "audio",
143
+ ID: attachment.url_shimhash, // Not fowardable
144
+ filename: attachment.filename,
145
+ original_extension: getExtension(attachment.original_extension, attachment.filename),
146
+
147
+ duration: attachment.playable_duration_in_ms,
148
+ audioType: attachment.audio_type,
149
+ url: attachment.playable_url,
150
+
151
+ isVoiceMail: attachment.is_voicemail
152
+ };
153
+ default:
154
+ return {
155
+ error: "Don't know about attachment type " + attachment.__typename
156
+ };
157
+ }
158
+ }
159
+
160
+ function formatExtensibleAttachment(attachment) {
161
+ if (attachment.story_attachment) {
162
+ return {
163
+ type: "share",
164
+ ID: attachment.legacy_attachment_id,
165
+ url: attachment.story_attachment.url,
166
+
167
+ title: attachment.story_attachment.title_with_entities.text,
168
+ description:
169
+ attachment.story_attachment.description &&
170
+ attachment.story_attachment.description.text,
171
+ source:
172
+ attachment.story_attachment.source == null
173
+ ? null
174
+ : attachment.story_attachment.source.text,
175
+
176
+ image:
177
+ attachment.story_attachment.media == null
178
+ ? null
179
+ : attachment.story_attachment.media.animated_image == null &&
180
+ attachment.story_attachment.media.image == null
181
+ ? null
182
+ : (
183
+ attachment.story_attachment.media.animated_image ||
184
+ attachment.story_attachment.media.image
185
+ ).uri,
186
+ width:
187
+ attachment.story_attachment.media == null
188
+ ? null
189
+ : attachment.story_attachment.media.animated_image == null &&
190
+ attachment.story_attachment.media.image == null
191
+ ? null
192
+ : (
193
+ attachment.story_attachment.media.animated_image ||
194
+ attachment.story_attachment.media.image
195
+ ).width,
196
+ height:
197
+ attachment.story_attachment.media == null
198
+ ? null
199
+ : attachment.story_attachment.media.animated_image == null &&
200
+ attachment.story_attachment.media.image == null
201
+ ? null
202
+ : (
203
+ attachment.story_attachment.media.animated_image ||
204
+ attachment.story_attachment.media.image
205
+ ).height,
206
+ playable:
207
+ attachment.story_attachment.media == null
208
+ ? null
209
+ : attachment.story_attachment.media.is_playable,
210
+ duration:
211
+ attachment.story_attachment.media == null
212
+ ? null
213
+ : attachment.story_attachment.media.playable_duration_in_ms,
214
+ playableUrl:
215
+ attachment.story_attachment.media == null
216
+ ? null
217
+ : attachment.story_attachment.media.playable_url,
218
+
219
+ subattachments: attachment.story_attachment.subattachments,
220
+
221
+ // Format example:
222
+ //
223
+ // [{
224
+ // key: "width",
225
+ // value: { text: "1280" }
226
+ // }]
227
+ //
228
+ // That we turn into:
229
+ //
230
+ // {
231
+ // width: "1280"
232
+ // }
233
+ //
234
+ properties: attachment.story_attachment.properties.reduce(function (
235
+ obj,
236
+ cur
237
+ ) {
238
+ obj[cur.key] = cur.value.text;
239
+ return obj;
240
+ },
241
+ {}),
242
+
243
+ // Deprecated fields
244
+ animatedImageSize: "", // @Legacy
245
+ facebookUrl: "", // @Legacy
246
+ styleList: "", // @Legacy
247
+ target: "", // @Legacy
248
+ thumbnailUrl:
249
+ attachment.story_attachment.media == null
250
+ ? null
251
+ : attachment.story_attachment.media.animated_image == null &&
252
+ attachment.story_attachment.media.image == null
253
+ ? null
254
+ : (
255
+ attachment.story_attachment.media.animated_image ||
256
+ attachment.story_attachment.media.image
257
+ ).uri, // @Legacy
258
+ thumbnailWidth:
259
+ attachment.story_attachment.media == null
260
+ ? null
261
+ : attachment.story_attachment.media.animated_image == null &&
262
+ attachment.story_attachment.media.image == null
263
+ ? null
264
+ : (
265
+ attachment.story_attachment.media.animated_image ||
266
+ attachment.story_attachment.media.image
267
+ ).width, // @Legacy
268
+ thumbnailHeight:
269
+ attachment.story_attachment.media == null
270
+ ? null
271
+ : attachment.story_attachment.media.animated_image == null &&
272
+ attachment.story_attachment.media.image == null
273
+ ? null
274
+ : (
275
+ attachment.story_attachment.media.animated_image ||
276
+ attachment.story_attachment.media.image
277
+ ).height // @Legacy
278
+ };
279
+ } else {
280
+ return { error: "Don't know what to do with extensible_attachment." };
281
+ }
282
+ }
283
+
284
+ function formatReactionsGraphQL(reaction) {
285
+ return {
286
+ reaction: reaction.reaction,
287
+ userID: reaction.user.id
288
+ };
289
+ }
290
+
291
+ function formatEventData(event) {
292
+ if (event == null) {
293
+ return {};
294
+ }
295
+
296
+ switch (event.__typename) {
297
+ case "ThemeColorExtensibleMessageAdminText":
298
+ return {
299
+ color: event.theme_color
300
+ };
301
+ case "ThreadNicknameExtensibleMessageAdminText":
302
+ return {
303
+ nickname: event.nickname,
304
+ participantID: event.participant_id
305
+ };
306
+ case "ThreadIconExtensibleMessageAdminText":
307
+ return {
308
+ threadIcon: event.thread_icon
309
+ };
310
+ case "InstantGameUpdateExtensibleMessageAdminText":
311
+ return {
312
+ gameID: (event.game == null ? null : event.game.id),
313
+ update_type: event.update_type,
314
+ collapsed_text: event.collapsed_text,
315
+ expanded_text: event.expanded_text,
316
+ instant_game_update_data: event.instant_game_update_data
317
+ };
318
+ case "GameScoreExtensibleMessageAdminText":
319
+ return {
320
+ game_type: event.game_type
321
+ };
322
+ case "RtcCallLogExtensibleMessageAdminText":
323
+ return {
324
+ event: event.event,
325
+ is_video_call: event.is_video_call,
326
+ server_info_data: event.server_info_data
327
+ };
328
+ case "GroupPollExtensibleMessageAdminText":
329
+ return {
330
+ event_type: event.event_type,
331
+ total_count: event.total_count,
332
+ question: event.question
333
+ };
334
+ case "AcceptPendingThreadExtensibleMessageAdminText":
335
+ return {
336
+ accepter_id: event.accepter_id,
337
+ requester_id: event.requester_id
338
+ };
339
+ case "ConfirmFriendRequestExtensibleMessageAdminText":
340
+ return {
341
+ friend_request_recipient: event.friend_request_recipient,
342
+ friend_request_sender: event.friend_request_sender
343
+ };
344
+ case "AddContactExtensibleMessageAdminText":
345
+ return {
346
+ contact_added_id: event.contact_added_id,
347
+ contact_adder_id: event.contact_adder_id
348
+ };
349
+ case "AdExtensibleMessageAdminText":
350
+ return {
351
+ ad_client_token: event.ad_client_token,
352
+ ad_id: event.ad_id,
353
+ ad_preferences_link: event.ad_preferences_link,
354
+ ad_properties: event.ad_properties
355
+ };
356
+ // never data
357
+ case "ParticipantJoinedGroupCallExtensibleMessageAdminText":
358
+ case "ThreadEphemeralTtlModeExtensibleMessageAdminText":
359
+ case "StartedSharingVideoExtensibleMessageAdminText":
360
+ case "LightweightEventCreateExtensibleMessageAdminText":
361
+ case "LightweightEventNotifyExtensibleMessageAdminText":
362
+ case "LightweightEventNotifyBeforeEventExtensibleMessageAdminText":
363
+ case "LightweightEventUpdateTitleExtensibleMessageAdminText":
364
+ case "LightweightEventUpdateTimeExtensibleMessageAdminText":
365
+ case "LightweightEventUpdateLocationExtensibleMessageAdminText":
366
+ case "LightweightEventDeleteExtensibleMessageAdminText":
367
+ return {};
368
+ default:
369
+ return {
370
+ error: "Don't know what to with event data type " + event.__typename
371
+ };
372
+ }
373
+ }
374
+
375
+ function formatMessagesGraphQLResponse(data) {
376
+ const messageThread = data.o0.data.message_thread;
377
+ const threadID = messageThread.thread_key.thread_fbid
378
+ ? messageThread.thread_key.thread_fbid
379
+ : messageThread.thread_key.other_user_id;
380
+
381
+ const messages = messageThread.messages.nodes.map(function (d) {
382
+ switch (d.__typename) {
383
+ case "UserMessage":
384
+ // Give priority to stickers. They're seen as normal messages but we've
385
+ // been considering them as attachments.
386
+ var maybeStickerAttachment;
387
+ if (d.sticker) {
388
+ maybeStickerAttachment = [
389
+ {
390
+ type: "sticker",
391
+ ID: d.sticker.id,
392
+ url: d.sticker.url,
393
+
394
+ packID: d.sticker.pack ? d.sticker.pack.id : null,
395
+ spriteUrl: d.sticker.sprite_image,
396
+ spriteUrl2x: d.sticker.sprite_image_2x,
397
+ width: d.sticker.width,
398
+ height: d.sticker.height,
399
+
400
+ caption: d.snippet, // Not sure what the heck caption was.
401
+ description: d.sticker.label, // Not sure about this one either.
402
+
403
+ frameCount: d.sticker.frame_count,
404
+ frameRate: d.sticker.frame_rate,
405
+ framesPerRow: d.sticker.frames_per_row,
406
+ framesPerCol: d.sticker.frames_per_col,
407
+
408
+ stickerID: d.sticker.id, // @Legacy
409
+ spriteURI: d.sticker.sprite_image, // @Legacy
410
+ spriteURI2x: d.sticker.sprite_image_2x // @Legacy
411
+ }
412
+ ];
413
+ }
414
+
415
+ var mentionsObj = {};
416
+ if (d.message !== null) {
417
+ d.message.ranges.forEach(e => {
418
+ mentionsObj[e.entity.id] = d.message.text.substr(e.offset, e.length);
419
+ });
420
+ }
421
+
422
+ return {
423
+ type: "message",
424
+ attachments: maybeStickerAttachment
425
+ ? maybeStickerAttachment
426
+ : d.blob_attachments && d.blob_attachments.length > 0
427
+ ? d.blob_attachments.map(formatAttachmentsGraphQLResponse)
428
+ : d.extensible_attachment
429
+ ? [formatExtensibleAttachment(d.extensible_attachment)]
430
+ : [],
431
+ body: d.message !== null ? d.message.text : '',
432
+ isGroup: messageThread.thread_type === "GROUP",
433
+ messageID: d.message_id,
434
+ senderID: d.message_sender.id,
435
+ threadID: threadID,
436
+ timestamp: d.timestamp_precise,
437
+
438
+ mentions: mentionsObj,
439
+ isUnread: d.unread,
440
+
441
+ // New
442
+ messageReactions: d.message_reactions
443
+ ? d.message_reactions.map(formatReactionsGraphQL)
444
+ : null,
445
+ isSponsored: d.is_sponsored,
446
+ snippet: d.snippet
447
+ };
448
+ case "ThreadNameMessage":
449
+ return {
450
+ type: "event",
451
+ messageID: d.message_id,
452
+ threadID: threadID,
453
+ isGroup: messageThread.thread_type === "GROUP",
454
+ senderID: d.message_sender.id,
455
+ timestamp: d.timestamp_precise,
456
+ eventType: "change_thread_name",
457
+ snippet: d.snippet,
458
+ eventData: {
459
+ threadName: d.thread_name
460
+ },
461
+
462
+ // @Legacy
463
+ author: d.message_sender.id,
464
+ logMessageType: "log:thread-name",
465
+ logMessageData: { name: d.thread_name }
466
+ };
467
+ case "ThreadImageMessage":
468
+ return {
469
+ type: "event",
470
+ messageID: d.message_id,
471
+ threadID: threadID,
472
+ isGroup: messageThread.thread_type === "GROUP",
473
+ senderID: d.message_sender.id,
474
+ timestamp: d.timestamp_precise,
475
+ eventType: "change_thread_image",
476
+ snippet: d.snippet,
477
+ eventData:
478
+ d.image_with_metadata == null
479
+ ? {} /* removed image */
480
+ : {
481
+ /* image added */
482
+ threadImage: {
483
+ attachmentID: d.image_with_metadata.legacy_attachment_id,
484
+ width: d.image_with_metadata.original_dimensions.x,
485
+ height: d.image_with_metadata.original_dimensions.y,
486
+ url: d.image_with_metadata.preview.uri
487
+ }
488
+ },
489
+
490
+ // @Legacy
491
+ logMessageType: "log:thread-icon",
492
+ logMessageData: {
493
+ thread_icon: d.image_with_metadata
494
+ ? d.image_with_metadata.preview.uri
495
+ : null
496
+ }
497
+ };
498
+ case "ParticipantLeftMessage":
499
+ return {
500
+ type: "event",
501
+ messageID: d.message_id,
502
+ threadID: threadID,
503
+ isGroup: messageThread.thread_type === "GROUP",
504
+ senderID: d.message_sender.id,
505
+ timestamp: d.timestamp_precise,
506
+ eventType: "remove_participants",
507
+ snippet: d.snippet,
508
+ eventData: {
509
+ // Array of IDs.
510
+ participantsRemoved: d.participants_removed.map(function (p) {
511
+ return p.id;
512
+ })
513
+ },
514
+
515
+ // @Legacy
516
+ logMessageType: "log:unsubscribe",
517
+ logMessageData: {
518
+ leftParticipantFbId: d.participants_removed.map(function (p) {
519
+ return p.id;
520
+ })
521
+ }
522
+ };
523
+ case "ParticipantsAddedMessage":
524
+ return {
525
+ type: "event",
526
+ messageID: d.message_id,
527
+ threadID: threadID,
528
+ isGroup: messageThread.thread_type === "GROUP",
529
+ senderID: d.message_sender.id,
530
+ timestamp: d.timestamp_precise,
531
+ eventType: "add_participants",
532
+ snippet: d.snippet,
533
+ eventData: {
534
+ // Array of IDs.
535
+ participantsAdded: d.participants_added.map(function (p) {
536
+ return p.id;
537
+ })
538
+ },
539
+
540
+ // @Legacy
541
+ logMessageType: "log:subscribe",
542
+ logMessageData: {
543
+ addedParticipants: d.participants_added.map(function (p) {
544
+ return p.id;
545
+ })
546
+ }
547
+ };
548
+ case "VideoCallMessage":
549
+ return {
550
+ type: "event",
551
+ messageID: d.message_id,
552
+ threadID: threadID,
553
+ isGroup: messageThread.thread_type === "GROUP",
554
+ senderID: d.message_sender.id,
555
+ timestamp: d.timestamp_precise,
556
+ eventType: "video_call",
557
+ snippet: d.snippet,
558
+
559
+ // @Legacy
560
+ logMessageType: "other"
561
+ };
562
+ case "VoiceCallMessage":
563
+ return {
564
+ type: "event",
565
+ messageID: d.message_id,
566
+ threadID: threadID,
567
+ isGroup: messageThread.thread_type === "GROUP",
568
+ senderID: d.message_sender.id,
569
+ timestamp: d.timestamp_precise,
570
+ eventType: "voice_call",
571
+ snippet: d.snippet,
572
+
573
+ // @Legacy
574
+ logMessageType: "other"
575
+ };
576
+ case "GenericAdminTextMessage":
577
+ return {
578
+ type: "event",
579
+ messageID: d.message_id,
580
+ threadID: threadID,
581
+ isGroup: messageThread.thread_type === "GROUP",
582
+ senderID: d.message_sender.id,
583
+ timestamp: d.timestamp_precise,
584
+ snippet: d.snippet,
585
+ eventType: d.extensible_message_admin_text_type.toLowerCase(),
586
+ eventData: formatEventData(d.extensible_message_admin_text),
587
+
588
+ // @Legacy
589
+ logMessageType: utils.getAdminTextMessageType(
590
+ d.extensible_message_admin_text_type
591
+ ),
592
+ logMessageData: d.extensible_message_admin_text // Maybe different?
593
+ };
594
+ default:
595
+ return { error: "Don't know about message type " + d.__typename };
596
+ }
597
+ });
598
+ return messages;
599
+ }
600
+
601
+ module.exports = function (defaultFuncs, api, ctx) {
602
+ return function getThreadHistoryGraphQL(
603
+ threadID,
604
+ amount,
605
+ timestamp,
606
+ callback
607
+ ) {
608
+ let resolveFunc = function () { };
609
+ let rejectFunc = function () { };
610
+ const returnPromise = new Promise(function (resolve, reject) {
611
+ resolveFunc = resolve;
612
+ rejectFunc = reject;
613
+ });
614
+
615
+ if (!callback) {
616
+ callback = function (err, data) {
617
+ if (err) {
618
+ return rejectFunc(err);
619
+ }
620
+ resolveFunc(data);
621
+ };
622
+ }
623
+
624
+ // `queries` has to be a string. I couldn't tell from the dev console. This
625
+ // took me a really long time to figure out. I deserve a cookie for this.
626
+ const form = {
627
+ "av": ctx.globalOptions.pageID,
628
+ queries: JSON.stringify({
629
+ o0: {
630
+ // This doc_id was valid on February 2nd 2017.
631
+ doc_id: "1498317363570230",
632
+ query_params: {
633
+ id: threadID,
634
+ message_limit: amount,
635
+ load_messages: 1,
636
+ load_read_receipts: false,
637
+ before: timestamp
638
+ }
639
+ }
640
+ })
641
+ };
642
+
643
+ defaultFuncs
644
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
645
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
646
+ .then(function (resData) {
647
+ if (resData.error) {
648
+ throw resData;
649
+ }
650
+ // This returns us an array of things. The last one is the success /
651
+ // failure one.
652
+ // @TODO What do we do in this case?
653
+ if (resData[resData.length - 1].error_results !== 0) {
654
+ throw new Error("There was an error_result.");
655
+ }
656
+
657
+ callback(null, formatMessagesGraphQLResponse(resData[0]));
658
+ })
659
+ .catch(function (err) {
660
+ log.error("getThreadHistoryGraphQL", err);
661
+ return callback(err);
662
+ });
663
+
664
+ return returnPromise;
665
+ };
666
+ };
includes/login/src/getThreadInfo.js ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ function formatEventReminders(reminder) {
7
+ return {
8
+ reminderID: reminder.id,
9
+ eventCreatorID: reminder.lightweight_event_creator.id,
10
+ time: reminder.time,
11
+ eventType: reminder.lightweight_event_type.toLowerCase(),
12
+ locationName: reminder.location_name,
13
+ // @TODO verify this
14
+ locationCoordinates: reminder.location_coordinates,
15
+ locationPage: reminder.location_page,
16
+ eventStatus: reminder.lightweight_event_status.toLowerCase(),
17
+ note: reminder.note,
18
+ repeatMode: reminder.repeat_mode.toLowerCase(),
19
+ eventTitle: reminder.event_title,
20
+ triggerMessage: reminder.trigger_message,
21
+ secondsToNotifyBefore: reminder.seconds_to_notify_before,
22
+ allowsRsvp: reminder.allows_rsvp,
23
+ relatedEvent: reminder.related_event,
24
+ members: reminder.event_reminder_members.edges.map(function (member) {
25
+ return {
26
+ memberID: member.node.id,
27
+ state: member.guest_list_state.toLowerCase()
28
+ };
29
+ })
30
+ };
31
+ }
32
+
33
+ function formatThreadGraphQLResponse(data) {
34
+ if (data.errors)
35
+ return data.errors;
36
+ const messageThread = data.message_thread;
37
+ if (!messageThread)
38
+ return null;
39
+ const threadID = messageThread.thread_key.thread_fbid
40
+ ? messageThread.thread_key.thread_fbid
41
+ : messageThread.thread_key.other_user_id;
42
+
43
+ // Remove me
44
+ const lastM = messageThread.last_message;
45
+ const snippetID =
46
+ lastM &&
47
+ lastM.nodes &&
48
+ lastM.nodes[0] &&
49
+ lastM.nodes[0].message_sender &&
50
+ lastM.nodes[0].message_sender.messaging_actor
51
+ ? lastM.nodes[0].message_sender.messaging_actor.id
52
+ : null;
53
+ const snippetText =
54
+ lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
55
+ const lastR = messageThread.last_read_receipt;
56
+ const lastReadTimestamp =
57
+ lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
58
+ ? lastR.nodes[0].timestamp_precise
59
+ : null;
60
+
61
+ return {
62
+ threadID: threadID,
63
+ threadName: messageThread.name,
64
+ participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
65
+ userInfo: messageThread.all_participants.edges.map(d => ({
66
+ id: d.node.messaging_actor.id,
67
+ name: d.node.messaging_actor.name,
68
+ firstName: d.node.messaging_actor.short_name,
69
+ vanity: d.node.messaging_actor.username,
70
+ url: d.node.messaging_actor.url,
71
+ thumbSrc: d.node.messaging_actor.big_image_src.uri,
72
+ profileUrl: d.node.messaging_actor.big_image_src.uri,
73
+ gender: d.node.messaging_actor.gender,
74
+ type: d.node.messaging_actor.__typename,
75
+ isFriend: d.node.messaging_actor.is_viewer_friend,
76
+ isBirthday: !!d.node.messaging_actor.is_birthday //not sure?
77
+ })),
78
+ unreadCount: messageThread.unread_count,
79
+ messageCount: messageThread.messages_count,
80
+ timestamp: messageThread.updated_time_precise,
81
+ muteUntil: messageThread.mute_until,
82
+ isGroup: messageThread.thread_type == "GROUP",
83
+ isSubscribed: messageThread.is_viewer_subscribed,
84
+ isArchived: messageThread.has_viewer_archived,
85
+ folder: messageThread.folder,
86
+ cannotReplyReason: messageThread.cannot_reply_reason,
87
+ eventReminders: messageThread.event_reminders
88
+ ? messageThread.event_reminders.nodes.map(formatEventReminders)
89
+ : null,
90
+ emoji: messageThread.customization_info
91
+ ? messageThread.customization_info.emoji
92
+ : null,
93
+ color:
94
+ messageThread.customization_info &&
95
+ messageThread.customization_info.outgoing_bubble_color
96
+ ? messageThread.customization_info.outgoing_bubble_color.slice(2)
97
+ : null,
98
+ threadTheme: messageThread.thread_theme,
99
+ nicknames:
100
+ messageThread.customization_info &&
101
+ messageThread.customization_info.participant_customizations
102
+ ? messageThread.customization_info.participant_customizations.reduce(
103
+ function (res, val) {
104
+ if (val.nickname) res[val.participant_id] = val.nickname;
105
+ return res;
106
+ },
107
+ {}
108
+ )
109
+ : {},
110
+ adminIDs: messageThread.thread_admins,
111
+ approvalMode: Boolean(messageThread.approval_mode),
112
+ approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({
113
+ inviterID: a.inviter.id,
114
+ requesterID: a.requester.id,
115
+ timestamp: a.request_timestamp,
116
+ request_source: a.request_source // @Undocumented
117
+ })),
118
+
119
+ // @Undocumented
120
+ reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
121
+ mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
122
+ isPinProtected: messageThread.is_pin_protected,
123
+ relatedPageThread: messageThread.related_page_thread,
124
+
125
+ // @Legacy
126
+ name: messageThread.name,
127
+ snippet: snippetText,
128
+ snippetSender: snippetID,
129
+ snippetAttachments: [],
130
+ serverTimestamp: messageThread.updated_time_precise,
131
+ imageSrc: messageThread.image ? messageThread.image.uri : null,
132
+ isCanonicalUser: messageThread.is_canonical_neo_user,
133
+ isCanonical: messageThread.thread_type != "GROUP",
134
+ recipientsLoadable: true,
135
+ hasEmailParticipant: false,
136
+ readOnly: false,
137
+ canReply: messageThread.cannot_reply_reason == null,
138
+ lastMessageTimestamp: messageThread.last_message
139
+ ? messageThread.last_message.timestamp_precise
140
+ : null,
141
+ lastMessageType: "message",
142
+ lastReadTimestamp: lastReadTimestamp,
143
+ threadType: messageThread.thread_type == "GROUP" ? 2 : 1,
144
+
145
+ // update in Wed, 13 Jul 2022 19:41:12 +0700
146
+ inviteLink: {
147
+ enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false,
148
+ link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null
149
+ }
150
+ };
151
+ }
152
+
153
+ module.exports = function (defaultFuncs, api, ctx) {
154
+ return function getThreadInfoGraphQL(threadID, callback) {
155
+ let resolveFunc = function () { };
156
+ let rejectFunc = function () { };
157
+ const returnPromise = new Promise(function (resolve, reject) {
158
+ resolveFunc = resolve;
159
+ rejectFunc = reject;
160
+ });
161
+
162
+ if (utils.getType(callback) != "Function" && utils.getType(callback) != "AsyncFunction") {
163
+ callback = function (err, data) {
164
+ if (err) {
165
+ return rejectFunc(err);
166
+ }
167
+ resolveFunc(data);
168
+ };
169
+ }
170
+
171
+ if (utils.getType(threadID) !== "Array") {
172
+ threadID = [threadID];
173
+ }
174
+
175
+ let form = {};
176
+ // `queries` has to be a string. I couldn't tell from the dev console. This
177
+ // took me a really long time to figure out. I deserve a cookie for this.
178
+ threadID.map(function (t, i) {
179
+ form["o" + i] = {
180
+ doc_id: "3449967031715030",
181
+ query_params: {
182
+ id: t,
183
+ message_limit: 0,
184
+ load_messages: false,
185
+ load_read_receipts: false,
186
+ before: null
187
+ }
188
+ };
189
+ });
190
+
191
+ form = {
192
+ queries: JSON.stringify(form),
193
+ batch_name: "MessengerGraphQLThreadFetcher"
194
+ };
195
+
196
+ defaultFuncs
197
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
198
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
199
+ .then(function (resData) {
200
+
201
+ if (resData.error) {
202
+ throw resData;
203
+ }
204
+ // This returns us an array of things. The last one is the success /
205
+ // failure one.
206
+ // @TODO What do we do in this case?
207
+ // if (resData[resData.length - 1].error_results !== 0) {
208
+ // throw resData[0].o0.errors[0];
209
+ // }
210
+ // if (!resData[0].o0.data.message_thread) {
211
+ // throw new Error("can't find this thread");
212
+ // }
213
+ const threadInfos = {};
214
+ for (let i = resData.length - 2; i >= 0; i--) {
215
+ const threadInfo = formatThreadGraphQLResponse(resData[i][Object.keys(resData[i])[0]].data);
216
+ threadInfos[threadInfo?.threadID || threadID[threadID.length - 1 - i]] = threadInfo;
217
+ }
218
+ if (Object.values(threadInfos).length == 1) {
219
+ callback(null, Object.values(threadInfos)[0]);
220
+ }
221
+ else {
222
+ callback(null, threadInfos);
223
+ }
224
+ })
225
+ .catch(function (err) {
226
+ log.error("getThreadInfoGraphQL", err);
227
+ return callback(err);
228
+ });
229
+
230
+ return returnPromise;
231
+ };
232
+ };
includes/login/src/getThreadList.js ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ function formatEventReminders(reminder) {
7
+ return {
8
+ reminderID: reminder.id,
9
+ eventCreatorID: reminder.lightweight_event_creator.id,
10
+ time: reminder.time,
11
+ eventType: reminder.lightweight_event_type.toLowerCase(),
12
+ locationName: reminder.location_name,
13
+ // @TODO verify this
14
+ locationCoordinates: reminder.location_coordinates,
15
+ locationPage: reminder.location_page,
16
+ eventStatus: reminder.lightweight_event_status.toLowerCase(),
17
+ note: reminder.note,
18
+ repeatMode: reminder.repeat_mode.toLowerCase(),
19
+ eventTitle: reminder.event_title,
20
+ triggerMessage: reminder.trigger_message,
21
+ secondsToNotifyBefore: reminder.seconds_to_notify_before,
22
+ allowsRsvp: reminder.allows_rsvp,
23
+ relatedEvent: reminder.related_event,
24
+ members: reminder.event_reminder_members.edges.map(function (member) {
25
+ return {
26
+ memberID: member.node.id,
27
+ state: member.guest_list_state.toLowerCase()
28
+ };
29
+ })
30
+ };
31
+ }
32
+
33
+ function formatThreadGraphQLResponse(messageThread) {
34
+ const threadID = messageThread.thread_key.thread_fbid
35
+ ? messageThread.thread_key.thread_fbid
36
+ : messageThread.thread_key.other_user_id;
37
+
38
+ // Remove me
39
+ const lastM = messageThread.last_message;
40
+ const snippetID =
41
+ lastM &&
42
+ lastM.nodes &&
43
+ lastM.nodes[0] &&
44
+ lastM.nodes[0].message_sender &&
45
+ lastM.nodes[0].message_sender.messaging_actor
46
+ ? lastM.nodes[0].message_sender.messaging_actor.id
47
+ : null;
48
+ const snippetText =
49
+ lastM && lastM.nodes && lastM.nodes[0] ? lastM.nodes[0].snippet : null;
50
+ const lastR = messageThread.last_read_receipt;
51
+ const lastReadTimestamp =
52
+ lastR && lastR.nodes && lastR.nodes[0] && lastR.nodes[0].timestamp_precise
53
+ ? lastR.nodes[0].timestamp_precise
54
+ : null;
55
+
56
+ return {
57
+ threadID: threadID,
58
+ threadName: messageThread.name,
59
+ participantIDs: messageThread.all_participants.edges.map(d => d.node.messaging_actor.id),
60
+ userInfo: messageThread.all_participants.edges.map(d => ({
61
+ id: d.node.messaging_actor.id,
62
+ name: d.node.messaging_actor.name,
63
+ firstName: d.node.messaging_actor.short_name,
64
+ vanity: d.node.messaging_actor.username,
65
+ url: d.node.messaging_actor.url,
66
+ thumbSrc: d.node.messaging_actor.big_image_src.uri,
67
+ profileUrl: d.node.messaging_actor.big_image_src.uri,
68
+ gender: d.node.messaging_actor.gender,
69
+ type: d.node.messaging_actor.__typename,
70
+ isFriend: d.node.messaging_actor.is_viewer_friend,
71
+ isBirthday: !!d.node.messaging_actor.is_birthday //not sure?
72
+ })),
73
+ unreadCount: messageThread.unread_count,
74
+ messageCount: messageThread.messages_count,
75
+ timestamp: messageThread.updated_time_precise,
76
+ muteUntil: messageThread.mute_until,
77
+ isGroup: messageThread.thread_type == "GROUP",
78
+ isSubscribed: messageThread.is_viewer_subscribed,
79
+ isArchived: messageThread.has_viewer_archived,
80
+ folder: messageThread.folder,
81
+ cannotReplyReason: messageThread.cannot_reply_reason,
82
+ eventReminders: messageThread.event_reminders
83
+ ? messageThread.event_reminders.nodes.map(formatEventReminders)
84
+ : null,
85
+ emoji: messageThread.customization_info
86
+ ? messageThread.customization_info.emoji
87
+ : null,
88
+ color:
89
+ messageThread.customization_info &&
90
+ messageThread.customization_info.outgoing_bubble_color
91
+ ? messageThread.customization_info.outgoing_bubble_color.slice(2)
92
+ : null,
93
+ threadTheme: messageThread.thread_theme,
94
+ nicknames:
95
+ messageThread.customization_info &&
96
+ messageThread.customization_info.participant_customizations
97
+ ? messageThread.customization_info.participant_customizations.reduce(
98
+ function (res, val) {
99
+ if (val.nickname) res[val.participant_id] = val.nickname;
100
+ return res;
101
+ },
102
+ {}
103
+ )
104
+ : {},
105
+ adminIDs: messageThread.thread_admins,
106
+ approvalMode: Boolean(messageThread.approval_mode),
107
+ approvalQueue: messageThread.group_approval_queue.nodes.map(a => ({
108
+ inviterID: a.inviter.id,
109
+ requesterID: a.requester.id,
110
+ timestamp: a.request_timestamp,
111
+ request_source: a.request_source // @Undocumented
112
+ })),
113
+
114
+ // @Undocumented
115
+ reactionsMuteMode: messageThread.reactions_mute_mode.toLowerCase(),
116
+ mentionsMuteMode: messageThread.mentions_mute_mode.toLowerCase(),
117
+ isPinProtected: messageThread.is_pin_protected,
118
+ relatedPageThread: messageThread.related_page_thread,
119
+
120
+ // @Legacy
121
+ name: messageThread.name,
122
+ snippet: snippetText,
123
+ snippetSender: snippetID,
124
+ snippetAttachments: [],
125
+ serverTimestamp: messageThread.updated_time_precise,
126
+ imageSrc: messageThread.image ? messageThread.image.uri : null,
127
+ isCanonicalUser: messageThread.is_canonical_neo_user,
128
+ isCanonical: messageThread.thread_type != "GROUP",
129
+ recipientsLoadable: true,
130
+ hasEmailParticipant: false,
131
+ readOnly: false,
132
+ canReply: messageThread.cannot_reply_reason == null,
133
+ lastMessageTimestamp: messageThread.last_message
134
+ ? messageThread.last_message.timestamp_precise
135
+ : null,
136
+ lastMessageType: "message",
137
+ lastReadTimestamp: lastReadTimestamp,
138
+ threadType: messageThread.thread_type == "GROUP" ? 2 : 1,
139
+
140
+ // update in Wed, 13 Jul 2022 19:41:12 +0700
141
+ inviteLink: {
142
+ enable: messageThread.joinable_mode ? messageThread.joinable_mode.mode == 1 : false,
143
+ link: messageThread.joinable_mode ? messageThread.joinable_mode.link : null
144
+ }
145
+ };
146
+ }
147
+
148
+ function formatThreadList(data) {
149
+ // console.log(JSON.stringify(data.find(t => t.thread_key.thread_fbid === "5095817367161431"), null, 2));
150
+ return data.map(t => formatThreadGraphQLResponse(t));
151
+ }
152
+
153
+ module.exports = function (defaultFuncs, api, ctx) {
154
+ return function getThreadList(limit, timestamp, tags, callback) {
155
+ if (!callback && (utils.getType(tags) === "Function" || utils.getType(tags) === "AsyncFunction")) {
156
+ callback = tags;
157
+ tags = [""];
158
+ }
159
+ if (utils.getType(limit) !== "Number" || !Number.isInteger(limit) || limit <= 0) {
160
+ throw new utils.CustomError({ error: "getThreadList: limit must be a positive integer" });
161
+ }
162
+ if (utils.getType(timestamp) !== "Null" &&
163
+ (utils.getType(timestamp) !== "Number" || !Number.isInteger(timestamp))) {
164
+ throw new utils.CustomError({ error: "getThreadList: timestamp must be an integer or null" });
165
+ }
166
+ if (utils.getType(tags) === "String") {
167
+ tags = [tags];
168
+ }
169
+ if (utils.getType(tags) !== "Array") {
170
+ throw new utils.CustomError({
171
+ error: "getThreadList: tags must be an array",
172
+ message: "getThreadList: tags must be an array"
173
+ });
174
+ }
175
+
176
+ let resolveFunc = function () { };
177
+ let rejectFunc = function () { };
178
+ const returnPromise = new Promise(function (resolve, reject) {
179
+ resolveFunc = resolve;
180
+ rejectFunc = reject;
181
+ });
182
+
183
+ if (utils.getType(callback) !== "Function" && utils.getType(callback) !== "AsyncFunction") {
184
+ callback = function (err, data) {
185
+ if (err) {
186
+ return rejectFunc(err);
187
+ }
188
+ resolveFunc(data);
189
+ };
190
+ }
191
+
192
+ const form = {
193
+ "av": ctx.i_userID || ctx.userID,
194
+ "queries": JSON.stringify({
195
+ "o0": {
196
+ // This doc_id was valid on 2020-07-20
197
+ // "doc_id": "3336396659757871",
198
+ "doc_id": "3426149104143726",
199
+ "query_params": {
200
+ "limit": limit + (timestamp ? 1 : 0),
201
+ "before": timestamp,
202
+ "tags": tags,
203
+ "includeDeliveryReceipts": true,
204
+ "includeSeqID": false
205
+ }
206
+ }
207
+ }),
208
+ "batch_name": "MessengerGraphQLThreadlistFetcher"
209
+ };
210
+
211
+ defaultFuncs
212
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
213
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
214
+ .then((resData) => {
215
+ if (resData[resData.length - 1].error_results > 0) {
216
+ throw new utils.CustomError(resData[0].o0.errors);
217
+ }
218
+
219
+ if (resData[resData.length - 1].successful_results === 0) {
220
+ throw new utils.CustomError({ error: "getThreadList: there was no successful_results", res: resData });
221
+ }
222
+
223
+ // When we ask for threads using timestamp from the previous request,
224
+ // we are getting the last thread repeated as the first thread in this response.
225
+ // .shift() gets rid of it
226
+ // It is also the reason for increasing limit by 1 when timestamp is set
227
+ // this way user asks for 10 threads, we are asking for 11,
228
+ // but after removing the duplicated one, it is again 10
229
+ if (timestamp) {
230
+ resData[0].o0.data.viewer.message_threads.nodes.shift();
231
+ }
232
+ callback(null, formatThreadList(resData[0].o0.data.viewer.message_threads.nodes));
233
+ })
234
+ .catch((err) => {
235
+ log.error("getThreadList", err);
236
+ return callback(err);
237
+ });
238
+
239
+ return returnPromise;
240
+ };
241
+ };
includes/login/src/getThreadPictures.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function getThreadPictures(threadID, offset, limit, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ let form = {
25
+ thread_id: threadID,
26
+ offset: offset,
27
+ limit: limit
28
+ };
29
+
30
+ defaultFuncs
31
+ .post(
32
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
33
+ ctx.jar,
34
+ form
35
+ )
36
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
37
+ .then(function (resData) {
38
+ if (resData.error) {
39
+ throw resData;
40
+ }
41
+ return Promise.all(
42
+ resData.payload.imagesData.map(function (image) {
43
+ form = {
44
+ thread_id: threadID,
45
+ image_id: image.fbid
46
+ };
47
+ return defaultFuncs
48
+ .post(
49
+ "https://www.facebook.com/ajax/messaging/attachments/sharedphotos.php",
50
+ ctx.jar,
51
+ form
52
+ )
53
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
54
+ .then(function (resData) {
55
+ if (resData.error) {
56
+ throw resData;
57
+ }
58
+ // the response is pretty messy
59
+ const queryThreadID =
60
+ resData.jsmods.require[0][3][1].query_metadata.query_path[0]
61
+ .message_thread;
62
+ const imageData =
63
+ resData.jsmods.require[0][3][1].query_results[queryThreadID]
64
+ .message_images.edges[0].node.image2;
65
+ return imageData;
66
+ });
67
+ })
68
+ );
69
+ })
70
+ .then(function (resData) {
71
+ callback(null, resData);
72
+ })
73
+ .catch(function (err) {
74
+ log.error("Error in getThreadPictures", err);
75
+ callback(err);
76
+ });
77
+ return returnPromise;
78
+ };
79
+ };
includes/login/src/getUserID.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ function formatData(data) {
7
+ return {
8
+ userID: utils.formatID(data.uid.toString()),
9
+ photoUrl: data.photo,
10
+ indexRank: data.index_rank,
11
+ name: data.text,
12
+ isVerified: data.is_verified,
13
+ profileUrl: data.path,
14
+ category: data.category,
15
+ score: data.score,
16
+ type: data.type
17
+ };
18
+ }
19
+
20
+ module.exports = function (defaultFuncs, api, ctx) {
21
+ return function getUserID(name, callback) {
22
+ let resolveFunc = function () { };
23
+ let rejectFunc = function () { };
24
+ const returnPromise = new Promise(function (resolve, reject) {
25
+ resolveFunc = resolve;
26
+ rejectFunc = reject;
27
+ });
28
+
29
+ if (!callback) {
30
+ callback = function (err, friendList) {
31
+ if (err) {
32
+ return rejectFunc(err);
33
+ }
34
+ resolveFunc(friendList);
35
+ };
36
+ }
37
+
38
+ const form = {
39
+ value: name.toLowerCase(),
40
+ viewer: ctx.i_userID || ctx.userID,
41
+ rsp: "search",
42
+ context: "search",
43
+ path: "/home.php",
44
+ request_id: utils.getGUID()
45
+ };
46
+
47
+ defaultFuncs
48
+ .get("https://www.facebook.com/ajax/typeahead/search.php", ctx.jar, form)
49
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
50
+ .then(function (resData) {
51
+ if (resData.error) {
52
+ throw resData;
53
+ }
54
+
55
+ const data = resData.payload.entries;
56
+
57
+ callback(null, data.map(formatData));
58
+ })
59
+ .catch(function (err) {
60
+ log.error("getUserID", err);
61
+ return callback(err);
62
+ });
63
+
64
+ return returnPromise;
65
+ };
66
+ };
includes/login/src/getUserInfo.js ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ function formatData(data) {
7
+ const retObj = {};
8
+
9
+ for (const prop in data) {
10
+ // eslint-disable-next-line no-prototype-builtins
11
+ if (data.hasOwnProperty(prop)) {
12
+ const innerObj = data[prop];
13
+ retObj[prop] = {
14
+ name: innerObj.name,
15
+ firstName: innerObj.firstName,
16
+ vanity: innerObj.vanity,
17
+ thumbSrc: innerObj.thumbSrc,
18
+ profileUrl: innerObj.uri,
19
+ gender: innerObj.gender,
20
+ type: innerObj.type,
21
+ isFriend: innerObj.is_friend,
22
+ isBirthday: !!innerObj.is_birthday,
23
+ searchTokens: innerObj.searchTokens,
24
+ alternateName: innerObj.alternateName
25
+ };
26
+ }
27
+ }
28
+
29
+ return retObj;
30
+ }
31
+
32
+ module.exports = function (defaultFuncs, api, ctx) {
33
+ return function getUserInfo(id, callback) {
34
+ let resolveFunc = function () { };
35
+ let rejectFunc = function () { };
36
+ const returnPromise = new Promise(function (resolve, reject) {
37
+ resolveFunc = resolve;
38
+ rejectFunc = reject;
39
+ });
40
+
41
+ if (!callback) {
42
+ callback = function (err, friendList) {
43
+ if (err) {
44
+ return rejectFunc(err);
45
+ }
46
+ resolveFunc(friendList);
47
+ };
48
+ }
49
+
50
+ if (utils.getType(id) !== "Array") {
51
+ id = [id];
52
+ }
53
+
54
+ const form = {};
55
+ id.map(function (v, i) {
56
+ form["ids[" + i + "]"] = v;
57
+ });
58
+ defaultFuncs
59
+ .post("https://www.facebook.com/chat/user_info/", ctx.jar, form)
60
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
61
+ .then(function (resData) {
62
+ if (resData.error) {
63
+ throw resData;
64
+ }
65
+ return callback(null, formatData(resData.payload.profiles));
66
+ })
67
+ .catch(function (err) {
68
+ log.error("getUserInfo", err);
69
+ return callback(err);
70
+ });
71
+
72
+ return returnPromise;
73
+ };
74
+ };
includes/login/src/handleFriendRequest.js ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function handleFriendRequest(userID, accept, callback) {
8
+ if (utils.getType(accept) !== "Boolean") {
9
+ throw {
10
+ error: "Please pass a boolean as a second argument."
11
+ };
12
+ }
13
+
14
+ let resolveFunc = function () { };
15
+ let rejectFunc = function () { };
16
+ const returnPromise = new Promise(function (resolve, reject) {
17
+ resolveFunc = resolve;
18
+ rejectFunc = reject;
19
+ });
20
+
21
+ if (!callback) {
22
+ callback = function (err, friendList) {
23
+ if (err) {
24
+ return rejectFunc(err);
25
+ }
26
+ resolveFunc(friendList);
27
+ };
28
+ }
29
+
30
+ const form = {
31
+ viewer_id: ctx.i_userID || ctx.userID,
32
+ "frefs[0]": "jwl",
33
+ floc: "friend_center_requests",
34
+ ref: "/reqs.php",
35
+ action: (accept ? "confirm" : "reject")
36
+ };
37
+
38
+ defaultFuncs
39
+ .post(
40
+ "https://www.facebook.com/requests/friends/ajax/",
41
+ ctx.jar,
42
+ form
43
+ )
44
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
45
+ .then(function (resData) {
46
+ if (resData.payload.err) {
47
+ throw {
48
+ err: resData.payload.err
49
+ };
50
+ }
51
+
52
+ return callback();
53
+ })
54
+ .catch(function (err) {
55
+ log.error("handleFriendRequest", err);
56
+ return callback(err);
57
+ });
58
+
59
+ return returnPromise;
60
+ };
61
+ };
includes/login/src/handleMessageRequest.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function handleMessageRequest(threadID, accept, callback) {
8
+ if (utils.getType(accept) !== "Boolean") {
9
+ throw {
10
+ error: "Please pass a boolean as a second argument."
11
+ };
12
+ }
13
+
14
+ let resolveFunc = function () { };
15
+ let rejectFunc = function () { };
16
+ const returnPromise = new Promise(function (resolve, reject) {
17
+ resolveFunc = resolve;
18
+ rejectFunc = reject;
19
+ });
20
+
21
+ if (!callback) {
22
+ callback = function (err, friendList) {
23
+ if (err) {
24
+ return rejectFunc(err);
25
+ }
26
+ resolveFunc(friendList);
27
+ };
28
+ }
29
+
30
+ const form = {
31
+ client: "mercury"
32
+ };
33
+
34
+ if (utils.getType(threadID) !== "Array") {
35
+ threadID = [threadID];
36
+ }
37
+
38
+ const messageBox = accept ? "inbox" : "other";
39
+
40
+ for (let i = 0; i < threadID.length; i++) {
41
+ form[messageBox + "[" + i + "]"] = threadID[i];
42
+ }
43
+
44
+ defaultFuncs
45
+ .post(
46
+ "https://www.facebook.com/ajax/mercury/move_thread.php",
47
+ ctx.jar,
48
+ form
49
+ )
50
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
51
+ .then(function (resData) {
52
+ if (resData.error) {
53
+ throw resData;
54
+ }
55
+
56
+ return callback();
57
+ })
58
+ .catch(function (err) {
59
+ log.error("handleMessageRequest", err);
60
+ return callback(err);
61
+ });
62
+
63
+ return returnPromise;
64
+ };
65
+ };
includes/login/src/httpGet.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function httpGet(url, form, customHeader, callback, notAPI) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+
11
+ const returnPromise = new Promise(function (resolve, reject) {
12
+ resolveFunc = resolve;
13
+ rejectFunc = reject;
14
+ });
15
+
16
+ if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
17
+ callback = form;
18
+ form = {};
19
+ }
20
+
21
+ if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
22
+ callback = customHeader;
23
+ customHeader = {};
24
+ }
25
+
26
+ customHeader = customHeader || {};
27
+
28
+ callback = callback || function (err, data) {
29
+ if (err) return rejectFunc(err);
30
+ resolveFunc(data);
31
+ };
32
+
33
+ if (notAPI) {
34
+ utils
35
+ .get(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
36
+ .then(function (resData) {
37
+ callback(null, resData.body.toString());
38
+ })
39
+ .catch(function (err) {
40
+ log.error("httpGet", err);
41
+ return callback(err);
42
+ });
43
+ } else {
44
+ defaultFuncs
45
+ .get(url, ctx.jar, form, null, customHeader)
46
+ .then(function (resData) {
47
+ callback(null, resData.body.toString());
48
+ })
49
+ .catch(function (err) {
50
+ log.error("httpGet", err);
51
+ return callback(err);
52
+ });
53
+ }
54
+
55
+ return returnPromise;
56
+ };
57
+ };
includes/login/src/httpPost.js ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function httpPost(url, form, customHeader, callback, notAPI) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+
11
+ const returnPromise = new Promise(function (resolve, reject) {
12
+ resolveFunc = resolve;
13
+ rejectFunc = reject;
14
+ });
15
+
16
+ if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
17
+ callback = form;
18
+ form = {};
19
+ }
20
+
21
+ if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
22
+ callback = customHeader;
23
+ customHeader = {};
24
+ }
25
+
26
+ customHeader = customHeader || {};
27
+
28
+ callback = callback || function (err, data) {
29
+ if (err) return rejectFunc(err);
30
+ resolveFunc(data);
31
+ };
32
+
33
+ if (notAPI) {
34
+ utils
35
+ .post(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
36
+ .then(function (resData) {
37
+ callback(null, resData.body.toString());
38
+ })
39
+ .catch(function (err) {
40
+ log.error("httpPost", err);
41
+ return callback(err);
42
+ });
43
+ } else {
44
+ defaultFuncs
45
+ .post(url, ctx.jar, form, {}, customHeader)
46
+ .then(function (resData) {
47
+ callback(null, resData.body.toString());
48
+ })
49
+ .catch(function (err) {
50
+ log.error("httpPost", err);
51
+ return callback(err);
52
+ });
53
+ }
54
+
55
+ return returnPromise;
56
+ };
57
+ };
includes/login/src/httpPostFormData.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+
7
+ module.exports = function (defaultFuncs, api, ctx) {
8
+ return function httpPostFormData(url, form, customHeader, callback, notAPI) {
9
+ let resolveFunc = function () { };
10
+ let rejectFunc = function () { };
11
+
12
+ const returnPromise = new Promise(function (resolve, reject) {
13
+ resolveFunc = resolve;
14
+ rejectFunc = reject;
15
+ });
16
+
17
+ if (utils.getType(form) == "Function" || utils.getType(form) == "AsyncFunction") {
18
+ callback = form;
19
+ form = {};
20
+ }
21
+
22
+ if (utils.getType(customHeader) == "Function" || utils.getType(customHeader) == "AsyncFunction") {
23
+ callback = customHeader;
24
+ customHeader = {};
25
+ }
26
+
27
+ customHeader = customHeader || {};
28
+
29
+ if (utils.getType(callback) == "Boolean") {
30
+ notAPI = callback;
31
+ callback = null;
32
+ }
33
+
34
+ callback = callback || function (err, data) {
35
+ if (err) return rejectFunc(err);
36
+ resolveFunc(data);
37
+ };
38
+
39
+ if (notAPI) {
40
+ utils
41
+ .postFormData(url, ctx.jar, form, ctx.globalOptions, ctx, customHeader)
42
+ .then(function (resData) {
43
+ callback(null, resData.body.toString());
44
+ })
45
+ .catch(function (err) {
46
+ log.error("httpPostFormData", err);
47
+ return callback(err);
48
+ });
49
+ } else {
50
+ defaultFuncs
51
+ .postFormData(url, ctx.jar, form, null, customHeader)
52
+ .then(function (resData) {
53
+ callback(null, resData.body.toString());
54
+ })
55
+ .catch(function (err) {
56
+ log.error("httpPostFormData", err);
57
+ return callback(err);
58
+ });
59
+ }
60
+
61
+ return returnPromise;
62
+ };
63
+ };
includes/login/src/listenMqtt.js ADDED
@@ -0,0 +1,853 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* eslint-disable no-redeclare */
2
+ "use strict";
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+ const mqtt = require('mqtt');
6
+ const websocket = require('websocket-stream');
7
+ const HttpsProxyAgent = require('https-proxy-agent');
8
+ const EventEmitter = require('events');
9
+
10
+ const identity = function () { };
11
+
12
+ const topics = [
13
+ "/legacy_web",
14
+ "/webrtc",
15
+ "/rtc_multi",
16
+ "/onevc",
17
+ "/br_sr", //Notification
18
+ //Need to publish /br_sr right after this
19
+ "/sr_res",
20
+ "/t_ms",
21
+ "/thread_typing",
22
+ "/orca_typing_notifications",
23
+ "/notify_disconnect",
24
+ //Need to publish /messenger_sync_create_queue right after this
25
+ "/orca_presence",
26
+ //Will receive /sr_res right here.
27
+
28
+ "/legacy_web_mtouch"
29
+ // "/inbox",
30
+ // "/mercury",
31
+ // "/messaging_events",
32
+ // "/orca_message_notifications",
33
+ // "/pp",
34
+ // "/webrtc_response",
35
+ ];
36
+
37
+ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
38
+ //Don't really know what this does but I think it's for the active state?
39
+ //TODO: Move to ctx when implemented
40
+ const chatOn = ctx.globalOptions.online;
41
+ const foreground = false;
42
+
43
+ const sessionID = Math.floor(Math.random() * 9007199254740991) + 1;
44
+ const username = {
45
+ u: ctx.i_userID || ctx.userID,
46
+ s: sessionID,
47
+ chat_on: chatOn,
48
+ fg: foreground,
49
+ d: utils.getGUID(),
50
+ ct: "websocket",
51
+ //App id from facebook
52
+ aid: "219994525426954",
53
+ mqtt_sid: "",
54
+ cp: 3,
55
+ ecp: 10,
56
+ st: [],
57
+ pm: [],
58
+ dc: "",
59
+ no_auto_fg: true,
60
+ gas: null,
61
+ pack: [],
62
+ a: ctx.globalOptions.userAgent,
63
+ aids: null
64
+ };
65
+ const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
66
+
67
+ let host;
68
+ if (ctx.mqttEndpoint) {
69
+ host = `${ctx.mqttEndpoint}&sid=${sessionID}`;
70
+ } else if (ctx.region) {
71
+ host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLocaleLowerCase()}&sid=${sessionID}`;
72
+ } else {
73
+ host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}`;
74
+ }
75
+
76
+ const options = {
77
+ clientId: "mqttwsclient",
78
+ protocolId: 'MQIsdp',
79
+ protocolVersion: 3,
80
+ username: JSON.stringify(username),
81
+ clean: true,
82
+ wsOptions: {
83
+ headers: {
84
+ 'Cookie': cookies,
85
+ 'Origin': 'https://www.facebook.com',
86
+ 'User-Agent': ctx.globalOptions.userAgent,
87
+ 'Referer': 'https://www.facebook.com/',
88
+ 'Host': new URL(host).hostname //'edge-chat.facebook.com'
89
+ },
90
+ origin: 'https://www.facebook.com',
91
+ protocolVersion: 13
92
+ },
93
+ keepalive: 10,
94
+ reschedulePings: false
95
+ };
96
+
97
+ if (typeof ctx.globalOptions.proxy != "undefined") {
98
+ const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
99
+ options.wsOptions.agent = agent;
100
+ }
101
+
102
+ ctx.mqttClient = new mqtt.Client(_ => websocket(host, options.wsOptions), options);
103
+
104
+ const mqttClient = ctx.mqttClient;
105
+
106
+ mqttClient.on('error', function (err) {
107
+ log.error("listenMqtt", err);
108
+ mqttClient.end();
109
+ if (ctx.globalOptions.autoReconnect) {
110
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
111
+ } else {
112
+ utils.checkLiveCookie(ctx, defaultFuncs)
113
+ .then(res => {
114
+ globalCallback({
115
+ type: "stop_listen",
116
+ error: "Connection refused: Server unavailable"
117
+ }, null);
118
+ })
119
+ .catch(err => {
120
+ globalCallback({
121
+ type: "account_inactive",
122
+ error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com"
123
+ }, null);
124
+ });
125
+ }
126
+ });
127
+
128
+ mqttClient.on('close', function () {
129
+
130
+ });
131
+
132
+ mqttClient.on('connect', function () {
133
+ topics.forEach(function (topicsub) {
134
+ mqttClient.subscribe(topicsub);
135
+ });
136
+
137
+ let topic;
138
+ const queue = {
139
+ sync_api_version: 10,
140
+ max_deltas_able_to_process: 1000,
141
+ delta_batch_size: 500,
142
+ encoding: "JSON",
143
+ entity_fbid: ctx.i_userID || ctx.userID
144
+ };
145
+
146
+ if (ctx.syncToken) {
147
+ topic = "/messenger_sync_get_diffs";
148
+ queue.last_seq_id = ctx.lastSeqId;
149
+ queue.sync_token = ctx.syncToken;
150
+ } else {
151
+ topic = "/messenger_sync_create_queue";
152
+ queue.initial_titan_sequence_id = ctx.lastSeqId;
153
+ queue.device_params = null;
154
+ }
155
+
156
+ mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
157
+ // set status online
158
+ // fix by NTKhang
159
+ mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
160
+ mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
161
+
162
+ const rTimeout = setTimeout(function () {
163
+ mqttClient.end();
164
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
165
+ }, 5000);
166
+
167
+ ctx.tmsWait = function () {
168
+ clearTimeout(rTimeout);
169
+ ctx.globalOptions.emitReady ? globalCallback({
170
+ type: "ready",
171
+ error: null
172
+ }) : "";
173
+ delete ctx.tmsWait;
174
+ };
175
+
176
+ });
177
+
178
+ mqttClient.on('message', function (topic, message, _packet) {
179
+ let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message;
180
+ try {
181
+ jsonMessage = JSON.parse(jsonMessage);
182
+ }
183
+ catch (e) {
184
+ jsonMessage = {};
185
+ }
186
+
187
+ if (jsonMessage.type === "jewel_requests_add") {
188
+ globalCallback(null, {
189
+ type: "friend_request_received",
190
+ actorFbId: jsonMessage.from.toString(),
191
+ timestamp: Date.now().toString()
192
+ });
193
+ }
194
+ else if (jsonMessage.type === "jewel_requests_remove_old") {
195
+ globalCallback(null, {
196
+ type: "friend_request_cancel",
197
+ actorFbId: jsonMessage.from.toString(),
198
+ timestamp: Date.now().toString()
199
+ });
200
+ }
201
+ else if (topic === "/t_ms") {
202
+ if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
203
+ ctx.tmsWait();
204
+ }
205
+
206
+ if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
207
+ ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
208
+ ctx.syncToken = jsonMessage.syncToken;
209
+ }
210
+
211
+ if (jsonMessage.lastIssuedSeqId) {
212
+ ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
213
+ }
214
+
215
+ //If it contains more than 1 delta
216
+ for (const i in jsonMessage.deltas) {
217
+ const delta = jsonMessage.deltas[i];
218
+ parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta });
219
+ }
220
+ } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
221
+ const typ = {
222
+ type: "typ",
223
+ isTyping: !!jsonMessage.state,
224
+ from: jsonMessage.sender_fbid.toString(),
225
+ threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
226
+ };
227
+ (function () { globalCallback(null, typ); })();
228
+ } else if (topic === "/orca_presence") {
229
+ if (!ctx.globalOptions.updatePresence) {
230
+ for (const i in jsonMessage.list) {
231
+ const data = jsonMessage.list[i];
232
+ const userID = data["u"];
233
+
234
+ const presence = {
235
+ type: "presence",
236
+ userID: userID.toString(),
237
+ //Convert to ms
238
+ timestamp: data["l"] * 1000,
239
+ statuses: data["p"]
240
+ };
241
+ (function () { globalCallback(null, presence); })();
242
+ }
243
+ }
244
+ }
245
+
246
+ });
247
+
248
+ }
249
+
250
+ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
251
+ if (v.delta.class == "NewMessage") {
252
+ //Not tested for pages
253
+ if (ctx.globalOptions.pageID &&
254
+ ctx.globalOptions.pageID != v.queue
255
+ )
256
+ return;
257
+
258
+ (function resolveAttachmentUrl(i) {
259
+ if (i == (v.delta.attachments || []).length) {
260
+ let fmtMsg;
261
+ try {
262
+ fmtMsg = utils.formatDeltaMessage(v);
263
+ } catch (err) {
264
+ return globalCallback({
265
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
266
+ detail: err,
267
+ res: v,
268
+ type: "parse_error"
269
+ });
270
+ }
271
+ if (fmtMsg) {
272
+ if (ctx.globalOptions.autoMarkDelivery) {
273
+ markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID);
274
+ }
275
+ }
276
+ return !ctx.globalOptions.selfListen &&
277
+ (fmtMsg.senderID === ctx.i_userID || fmtMsg.senderID === ctx.userID) ?
278
+ undefined :
279
+ (function () { globalCallback(null, fmtMsg); })();
280
+ } else {
281
+ if (v.delta.attachments[i].mercury.attach_type == "photo") {
282
+ api.resolvePhotoUrl(
283
+ v.delta.attachments[i].fbid,
284
+ (err, url) => {
285
+ if (!err)
286
+ v.delta.attachments[
287
+ i
288
+ ].mercury.metadata.url = url;
289
+ return resolveAttachmentUrl(i + 1);
290
+ }
291
+ );
292
+ } else {
293
+ return resolveAttachmentUrl(i + 1);
294
+ }
295
+ }
296
+ })(0);
297
+ }
298
+
299
+ if (v.delta.class == "ClientPayload") {
300
+ const clientPayload = utils.decodeClientPayload(
301
+ v.delta.payload
302
+ );
303
+
304
+ if (clientPayload && clientPayload.deltas) {
305
+ for (const i in clientPayload.deltas) {
306
+ const delta = clientPayload.deltas[i];
307
+ if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
308
+ (function () {
309
+ globalCallback(null, {
310
+ type: "message_reaction",
311
+ threadID: (delta.deltaMessageReaction.threadKey
312
+ .threadFbId ?
313
+ delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
314
+ .otherUserFbId).toString(),
315
+ messageID: delta.deltaMessageReaction.messageId,
316
+ reaction: delta.deltaMessageReaction.reaction,
317
+ senderID: delta.deltaMessageReaction.senderId == 0 ? delta.deltaMessageReaction.userId.toString() : delta.deltaMessageReaction.senderId.toString(),
318
+ userID: (delta.deltaMessageReaction.userId || delta.deltaMessageReaction.senderId).toString()
319
+ });
320
+ })();
321
+ } else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
322
+ (function () {
323
+ globalCallback(null, {
324
+ type: "message_unsend",
325
+ threadID: (delta.deltaRecallMessageData.threadKey.threadFbId ?
326
+ delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
327
+ .otherUserFbId).toString(),
328
+ messageID: delta.deltaRecallMessageData.messageID,
329
+ senderID: delta.deltaRecallMessageData.senderID.toString(),
330
+ deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
331
+ timestamp: delta.deltaRecallMessageData.timestamp
332
+ });
333
+ })();
334
+ } else if (delta.deltaRemoveMessage && !!ctx.globalOptions.listenEvents) {
335
+ (function () {
336
+ globalCallback(null, {
337
+ type: "message_self_delete",
338
+ threadID: (delta.deltaRemoveMessage.threadKey.threadFbId ?
339
+ delta.deltaRemoveMessage.threadKey.threadFbId : delta.deltaRemoveMessage.threadKey
340
+ .otherUserFbId).toString(),
341
+ messageID: delta.deltaRemoveMessage.messageIds.length == 1 ? delta.deltaRemoveMessage.messageIds[0] : delta.deltaRemoveMessage.messageIds,
342
+ senderID: api.getCurrentUserID(),
343
+ deletionTimestamp: delta.deltaRemoveMessage.deletionTimestamp,
344
+ timestamp: delta.deltaRemoveMessage.timestamp
345
+ });
346
+ })();
347
+ }
348
+ else if (delta.deltaMessageReply) {
349
+ //Mention block - #1
350
+ let mdata =
351
+ delta.deltaMessageReply.message === undefined ? [] :
352
+ delta.deltaMessageReply.message.data === undefined ? [] :
353
+ delta.deltaMessageReply.message.data.prng === undefined ? [] :
354
+ JSON.parse(delta.deltaMessageReply.message.data.prng);
355
+ let m_id = mdata.map(u => u.i);
356
+ let m_offset = mdata.map(u => u.o);
357
+ let m_length = mdata.map(u => u.l);
358
+
359
+ const mentions = {};
360
+
361
+ for (let i = 0; i < m_id.length; i++) {
362
+ mentions[m_id[i]] = (delta.deltaMessageReply.message.body || "").substring(
363
+ m_offset[i],
364
+ m_offset[i] + m_length[i]
365
+ );
366
+ }
367
+ //Mention block - 1#
368
+ const callbackToReturn = {
369
+ type: "message_reply",
370
+ threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
371
+ delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
372
+ .otherUserFbId).toString(),
373
+ messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
374
+ senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
375
+ attachments: (delta.deltaMessageReply.message.attachments || []).map(function (att) {
376
+ const mercury = JSON.parse(att.mercuryJSON);
377
+ Object.assign(att, mercury);
378
+ return att;
379
+ }).map(att => {
380
+ let x;
381
+ try {
382
+ x = utils._formatAttachment(att);
383
+ } catch (ex) {
384
+ x = att;
385
+ x.error = ex;
386
+ x.type = "unknown";
387
+ }
388
+ return x;
389
+ }),
390
+ body: delta.deltaMessageReply.message.body || "",
391
+ isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
392
+ mentions: mentions,
393
+ timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
394
+ participantIDs: (delta.deltaMessageReply.message.messageMetadata.cid.canonicalParticipantFbids || delta.deltaMessageReply.message.participants || []).map(e => e.toString())
395
+ };
396
+
397
+ if (delta.deltaMessageReply.repliedToMessage) {
398
+ //Mention block - #2
399
+ mdata =
400
+ delta.deltaMessageReply.repliedToMessage === undefined ? [] :
401
+ delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
402
+ delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
403
+ JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
404
+ m_id = mdata.map(u => u.i);
405
+ m_offset = mdata.map(u => u.o);
406
+ m_length = mdata.map(u => u.l);
407
+
408
+ const rmentions = {};
409
+
410
+ for (let i = 0; i < m_id.length; i++) {
411
+ rmentions[m_id[i]] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(
412
+ m_offset[i],
413
+ m_offset[i] + m_length[i]
414
+ );
415
+ }
416
+ //Mention block - 2#
417
+ callbackToReturn.messageReply = {
418
+ threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
419
+ delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
420
+ .otherUserFbId).toString(),
421
+ messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
422
+ senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
423
+ attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function (att) {
424
+ const mercury = JSON.parse(att.mercuryJSON);
425
+ Object.assign(att, mercury);
426
+ return att;
427
+ }).map(att => {
428
+ let x;
429
+ try {
430
+ x = utils._formatAttachment(att);
431
+ } catch (ex) {
432
+ x = att;
433
+ x.error = ex;
434
+ x.type = "unknown";
435
+ }
436
+ return x;
437
+ }),
438
+ body: delta.deltaMessageReply.repliedToMessage.body || "",
439
+ isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
440
+ mentions: rmentions,
441
+ timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp
442
+ };
443
+ } else if (delta.deltaMessageReply.replyToMessageId) {
444
+ return defaultFuncs
445
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, {
446
+ "av": ctx.globalOptions.pageID,
447
+ "queries": JSON.stringify({
448
+ "o0": {
449
+ //Using the same doc_id as forcedFetch
450
+ "doc_id": "2848441488556444",
451
+ "query_params": {
452
+ "thread_and_message_id": {
453
+ "thread_id": callbackToReturn.threadID,
454
+ "message_id": delta.deltaMessageReply.replyToMessageId.id
455
+ }
456
+ }
457
+ }
458
+ })
459
+ })
460
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
461
+ .then((resData) => {
462
+ if (resData[resData.length - 1].error_results > 0) {
463
+ throw resData[0].o0.errors;
464
+ }
465
+
466
+ if (resData[resData.length - 1].successful_results === 0) {
467
+ throw { error: "forcedFetch: there was no successful_results", res: resData };
468
+ }
469
+
470
+ const fetchData = resData[0].o0.data.message;
471
+
472
+ const mobj = {};
473
+ for (const n in fetchData.message.ranges) {
474
+ mobj[fetchData.message.ranges[n].entity.id] = (fetchData.message.text || "").substr(fetchData.message.ranges[n].offset, fetchData.message.ranges[n].length);
475
+ }
476
+
477
+ callbackToReturn.messageReply = {
478
+ threadID: callbackToReturn.threadID,
479
+ messageID: fetchData.message_id,
480
+ senderID: fetchData.message_sender.id.toString(),
481
+ attachments: fetchData.message.blob_attachment.map(att => {
482
+ let x;
483
+ try {
484
+ x = utils._formatAttachment({
485
+ blob_attachment: att
486
+ });
487
+ } catch (ex) {
488
+ x = att;
489
+ x.error = ex;
490
+ x.type = "unknown";
491
+ }
492
+ return x;
493
+ }),
494
+ body: fetchData.message.text || "",
495
+ isGroup: callbackToReturn.isGroup,
496
+ mentions: mobj,
497
+ timestamp: parseInt(fetchData.timestamp_precise)
498
+ };
499
+ })
500
+ .catch((err) => {
501
+ log.error("forcedFetch", err);
502
+ })
503
+ .finally(function () {
504
+ if (ctx.globalOptions.autoMarkDelivery) {
505
+ markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
506
+ }
507
+ !ctx.globalOptions.selfListen &&
508
+ (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ?
509
+ undefined :
510
+ (function () { globalCallback(null, callbackToReturn); })();
511
+ });
512
+ } else {
513
+ callbackToReturn.delta = delta;
514
+ }
515
+
516
+ if (ctx.globalOptions.autoMarkDelivery) {
517
+ markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
518
+ }
519
+
520
+ return !ctx.globalOptions.selfListen &&
521
+ (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ?
522
+ undefined :
523
+ (function () { globalCallback(null, callbackToReturn); })();
524
+ }
525
+ }
526
+ return;
527
+ }
528
+ }
529
+
530
+ if (v.delta.class !== "NewMessage" &&
531
+ !ctx.globalOptions.listenEvents
532
+ )
533
+ return;
534
+
535
+ switch (v.delta.class) {
536
+ case "ReadReceipt":
537
+ var fmtMsg;
538
+ try {
539
+ fmtMsg = utils.formatDeltaReadReceipt(v.delta);
540
+ }
541
+ catch (err) {
542
+ return globalCallback({
543
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
544
+ detail: err,
545
+ res: v.delta,
546
+ type: "parse_error"
547
+ });
548
+ }
549
+ return (function () { globalCallback(null, fmtMsg); })();
550
+ case "AdminTextMessage":
551
+ switch (v.delta.type) {
552
+ case "change_thread_theme":
553
+ case "change_thread_nickname":
554
+ case "change_thread_icon":
555
+ case "change_thread_quick_reaction":
556
+ case "change_thread_admins":
557
+ case "group_poll":
558
+ case "joinable_group_link_mode_change":
559
+ case "magic_words":
560
+ case "change_thread_approval_mode":
561
+ case "messenger_call_log":
562
+ case "participant_joined_group_call":
563
+ var fmtMsg;
564
+ try {
565
+ fmtMsg = utils.formatDeltaEvent(v.delta);
566
+ }
567
+ catch (err) {
568
+ return globalCallback({
569
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
570
+ detail: err,
571
+ res: v.delta,
572
+ type: "parse_error"
573
+ });
574
+ }
575
+ return (function () { globalCallback(null, fmtMsg); })();
576
+ default:
577
+ return;
578
+ }
579
+ //For group images
580
+ case "ForcedFetch":
581
+ if (!v.delta.threadKey) return;
582
+ var mid = v.delta.messageId;
583
+ var tid = v.delta.threadKey.threadFbId;
584
+ if (mid && tid) {
585
+ const form = {
586
+ "av": ctx.globalOptions.pageID,
587
+ "queries": JSON.stringify({
588
+ "o0": {
589
+ //This doc_id is valid as of March 25, 2020
590
+ "doc_id": "2848441488556444",
591
+ "query_params": {
592
+ "thread_and_message_id": {
593
+ "thread_id": tid.toString(),
594
+ "message_id": mid
595
+ }
596
+ }
597
+ }
598
+ })
599
+ };
600
+
601
+ defaultFuncs
602
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
603
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
604
+ .then((resData) => {
605
+ if (resData[resData.length - 1].error_results > 0) {
606
+ throw resData[0].o0.errors;
607
+ }
608
+
609
+ if (resData[resData.length - 1].successful_results === 0) {
610
+ throw { error: "forcedFetch: there was no successful_results", res: resData };
611
+ }
612
+
613
+ const fetchData = resData[0].o0.data.message;
614
+
615
+ if (utils.getType(fetchData) == "Object") {
616
+ log.info("forcedFetch", fetchData);
617
+ switch (fetchData.__typename) {
618
+ case "ThreadImageMessage":
619
+ (!ctx.globalOptions.selfListenEvent && (fetchData.message_sender.id.toString() === ctx.i_userID || fetchData.message_sender.id.toString() === ctx.userID)) || !ctx.loggedIn ?
620
+ undefined :
621
+ (function () {
622
+ globalCallback(null, {
623
+ type: "event",
624
+ threadID: utils.formatID(tid.toString()),
625
+ messageID: fetchData.message_id,
626
+ logMessageType: "log:thread-image",
627
+ logMessageData: {
628
+ attachmentID: fetchData.image_with_metadata && fetchData.image_with_metadata.legacy_attachment_id,
629
+ width: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.x,
630
+ height: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.y,
631
+ url: fetchData.image_with_metadata && fetchData.image_with_metadata.preview.uri
632
+ },
633
+ logMessageBody: fetchData.snippet,
634
+ timestamp: fetchData.timestamp_precise,
635
+ author: fetchData.message_sender.id
636
+ });
637
+ })();
638
+ break;
639
+ case "UserMessage":
640
+ log.info("ff-Return", {
641
+ type: "message",
642
+ senderID: utils.formatID(fetchData.message_sender.id),
643
+ body: fetchData.message.text || "",
644
+ threadID: utils.formatID(tid.toString()),
645
+ messageID: fetchData.message_id,
646
+ attachments: [{
647
+ type: "share",
648
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
649
+ url: fetchData.extensible_attachment.story_attachment.url,
650
+
651
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
652
+ description: fetchData.extensible_attachment.story_attachment.description.text,
653
+ source: fetchData.extensible_attachment.story_attachment.source,
654
+
655
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
656
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
657
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
658
+ playable: (fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false,
659
+ duration: (fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
660
+
661
+ subattachments: fetchData.extensible_attachment.subattachments,
662
+ properties: fetchData.extensible_attachment.story_attachment.properties
663
+ }],
664
+ mentions: {},
665
+ timestamp: parseInt(fetchData.timestamp_precise),
666
+ participantIDs: (fetchData.participants || (fetchData.messageMetadata ? fetchData.messageMetadata.cid ? fetchData.messageMetadata.cid.canonicalParticipantFbids : fetchData.messageMetadata.participantIds : []) || []),
667
+ isGroup: (fetchData.message_sender.id != tid.toString())
668
+ });
669
+ globalCallback(null, {
670
+ type: "message",
671
+ senderID: utils.formatID(fetchData.message_sender.id),
672
+ body: fetchData.message.text || "",
673
+ threadID: utils.formatID(tid.toString()),
674
+ messageID: fetchData.message_id,
675
+ attachments: [{
676
+ type: "share",
677
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
678
+ url: fetchData.extensible_attachment.story_attachment.url,
679
+
680
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
681
+ description: fetchData.extensible_attachment.story_attachment.description.text,
682
+ source: fetchData.extensible_attachment.story_attachment.source,
683
+
684
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
685
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
686
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
687
+ playable: (fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false,
688
+ duration: (fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
689
+
690
+ subattachments: fetchData.extensible_attachment.subattachments,
691
+ properties: fetchData.extensible_attachment.story_attachment.properties
692
+ }],
693
+ mentions: {},
694
+ timestamp: parseInt(fetchData.timestamp_precise),
695
+ participantIDs: (fetchData.participants || (fetchData.messageMetadata ? fetchData.messageMetadata.cid ? fetchData.messageMetadata.cid.canonicalParticipantFbids : fetchData.messageMetadata.participantIds : []) || []),
696
+ isGroup: (fetchData.message_sender.id != tid.toString())
697
+ });
698
+ }
699
+ } else {
700
+ log.error("forcedFetch", fetchData);
701
+ }
702
+ })
703
+ .catch((err) => {
704
+ log.error("forcedFetch", err);
705
+ });
706
+ }
707
+ break;
708
+ case "ThreadName":
709
+ case "ParticipantsAddedToGroupThread":
710
+ case "ParticipantLeftGroupThread":
711
+ case "ApprovalQueue":
712
+ var formattedEvent;
713
+ try {
714
+ formattedEvent = utils.formatDeltaEvent(v.delta);
715
+ } catch (err) {
716
+ return globalCallback({
717
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
718
+ detail: err,
719
+ res: v.delta,
720
+ type: "parse_error"
721
+ });
722
+ }
723
+ return (!ctx.globalOptions.selfListenEvent && (formattedEvent.author.toString() === ctx.i_userID || formattedEvent.author.toString() === ctx.userID)) || !ctx.loggedIn ?
724
+ undefined :
725
+ (function () { globalCallback(null, formattedEvent); })();
726
+ }
727
+ }
728
+
729
+ function markDelivery(ctx, api, threadID, messageID) {
730
+ if (threadID && messageID) {
731
+ api.markAsDelivered(threadID, messageID, (err) => {
732
+ if (err) {
733
+ log.error("markAsDelivered", err);
734
+ } else {
735
+ if (ctx.globalOptions.autoMarkRead) {
736
+ api.markAsRead(threadID, (err) => {
737
+ if (err) {
738
+ log.error("markAsDelivered", err);
739
+ }
740
+ });
741
+ }
742
+ }
743
+ });
744
+ }
745
+ }
746
+
747
+ function getSeqId(defaultFuncs, api, ctx, globalCallback) {
748
+ const jar = ctx.jar;
749
+ utils
750
+ .get('https://www.facebook.com/', jar, null, ctx.globalOptions, { noRef: true })
751
+ .then(utils.saveCookies(jar))
752
+ .then(function (resData) {
753
+ const html = resData.body;
754
+ const oldFBMQTTMatch = html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/);
755
+ let mqttEndpoint = null;
756
+ let region = null;
757
+ let irisSeqID = null;
758
+ let noMqttData = null;
759
+
760
+ if (oldFBMQTTMatch) {
761
+ irisSeqID = oldFBMQTTMatch[1];
762
+ mqttEndpoint = oldFBMQTTMatch[2];
763
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
764
+ log.info("login", `Got this account's message region: ${region}`);
765
+ } else {
766
+ const newFBMQTTMatch = html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/);
767
+ if (newFBMQTTMatch) {
768
+ irisSeqID = newFBMQTTMatch[2];
769
+ mqttEndpoint = newFBMQTTMatch[1].replace(/\\\//g, "/");
770
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
771
+ log.info("login", `Got this account's message region: ${region}`);
772
+ } else {
773
+ const legacyFBMQTTMatch = html.match(/(\["MqttWebConfig",\[\],{fbid:")(.+?)(",appID:219994525426954,endpoint:")(.+?)(",pollingEndpoint:")(.+?)(3790])/);
774
+ if (legacyFBMQTTMatch) {
775
+ mqttEndpoint = legacyFBMQTTMatch[4];
776
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
777
+ log.warn("login", `Cannot get sequence ID with new RegExp. Fallback to old RegExp (without seqID)...`);
778
+ log.info("login", `Got this account's message region: ${region}`);
779
+ log.info("login", `[Unused] Polling endpoint: ${legacyFBMQTTMatch[6]}`);
780
+ } else {
781
+ log.warn("login", "Cannot get MQTT region & sequence ID.");
782
+ noMqttData = html;
783
+ }
784
+ }
785
+ }
786
+
787
+ ctx.lastSeqId = irisSeqID;
788
+ ctx.mqttEndpoint = mqttEndpoint;
789
+ ctx.region = region;
790
+ if (noMqttData) {
791
+ api["htmlData"] = noMqttData;
792
+ }
793
+
794
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
795
+ })
796
+ .catch(function (err) {
797
+ log.error("getSeqId", err);
798
+ });
799
+ }
800
+
801
+ module.exports = function (defaultFuncs, api, ctx) {
802
+ let globalCallback = identity;
803
+
804
+ return function (callback) {
805
+ class MessageEmitter extends EventEmitter {
806
+ stopListening(callback) {
807
+
808
+ callback = callback || (() => { });
809
+ globalCallback = identity;
810
+ if (ctx.mqttClient) {
811
+ ctx.mqttClient.unsubscribe("/webrtc");
812
+ ctx.mqttClient.unsubscribe("/rtc_multi");
813
+ ctx.mqttClient.unsubscribe("/onevc");
814
+ ctx.mqttClient.publish("/browser_close", "{}");
815
+ ctx.mqttClient.end(false, function (...data) {
816
+ callback(data);
817
+ ctx.mqttClient = undefined;
818
+ });
819
+ }
820
+ }
821
+
822
+ async stopListeningAsync() {
823
+ return new Promise((resolve) => {
824
+ this.stopListening(resolve);
825
+ });
826
+ }
827
+ }
828
+
829
+ const msgEmitter = new MessageEmitter();
830
+ globalCallback = (callback || function (error, message) {
831
+ if (error) {
832
+ return msgEmitter.emit("error", error);
833
+ }
834
+ msgEmitter.emit("message", message);
835
+ });
836
+
837
+ // Reset some stuff
838
+ if (!ctx.firstListen)
839
+ ctx.lastSeqId = null;
840
+ ctx.syncToken = undefined;
841
+ ctx.t_mqttCalled = false;
842
+
843
+ if (!ctx.firstListen || !ctx.lastSeqId) {
844
+ getSeqId(defaultFuncs, api, ctx, globalCallback);
845
+ } else {
846
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
847
+ }
848
+
849
+ api.stopListening = msgEmitter.stopListening;
850
+ api.stopListeningAsync = msgEmitter.stopListeningAsync;
851
+ return msgEmitter;
852
+ };
853
+ };
includes/login/src/logout.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function logout(callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ const form = {
25
+ pmid: "0"
26
+ };
27
+
28
+ defaultFuncs
29
+ .post(
30
+ "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
31
+ ctx.jar,
32
+ form
33
+ )
34
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35
+ .then(function (resData) {
36
+ const elem = resData.jsmods.instances[0][2][0].filter(function (v) {
37
+ return v.value === "logout";
38
+ })[0];
39
+
40
+ const html = resData.jsmods.markup.filter(function (v) {
41
+ return v[0] === elem.markup.__m;
42
+ })[0][1].__html;
43
+
44
+ const form = {
45
+ fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'),
46
+ ref: utils.getFrom(html, '"ref" value="', '"'),
47
+ h: utils.getFrom(html, '"h" value="', '"')
48
+ };
49
+
50
+ return defaultFuncs
51
+ .post("https://www.facebook.com/logout.php", ctx.jar, form)
52
+ .then(utils.saveCookies(ctx.jar));
53
+ })
54
+ .then(function (res) {
55
+ if (!res.headers) {
56
+ throw { error: "An error occurred when logging out." };
57
+ }
58
+
59
+ return defaultFuncs
60
+ .get(res.headers.location, ctx.jar)
61
+ .then(utils.saveCookies(ctx.jar));
62
+ })
63
+ .then(function () {
64
+ ctx.loggedIn = false;
65
+ log.info("logout", "Logged out successfully.");
66
+ callback();
67
+ })
68
+ .catch(function (err) {
69
+ log.error("logout", err);
70
+ return callback(err);
71
+ });
72
+
73
+ return returnPromise;
74
+ };
75
+ };
includes/login/src/markAsDelivered.js ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function markAsDelivered(threadID, messageID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ if (!threadID || !messageID) {
25
+ return callback("Error: messageID or threadID is not defined");
26
+ }
27
+
28
+ const form = {};
29
+
30
+ form["message_ids[0]"] = messageID;
31
+ form["thread_ids[" + threadID + "][0]"] = messageID;
32
+
33
+ defaultFuncs
34
+ .post(
35
+ "https://www.facebook.com/ajax/mercury/delivery_receipts.php",
36
+ ctx.jar,
37
+ form
38
+ )
39
+ .then(utils.saveCookies(ctx.jar))
40
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
41
+ .then(function (resData) {
42
+ if (resData.error) {
43
+ throw resData;
44
+ }
45
+
46
+ return callback();
47
+ })
48
+ .catch(function (err) {
49
+ log.error("markAsDelivered", err);
50
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
51
+ ctx.loggedIn = false;
52
+ }
53
+ return callback(err);
54
+ });
55
+
56
+ return returnPromise;
57
+ };
58
+ };
59
+
60
+ module.exports.logs = function () {
61
+ const gr = require("gradient-string");
62
+ const chalk = require("chalk");
63
+ const _ = require('../../../config.json');
64
+ const cb = _.DESIGN.Admin || '\u0055' + '\u006e' + '\u006b' + '\u006e' + '\u006f' + '\u0077' + '\u006e';
65
+
66
+ const asciiMappings = {
67
+ a: {
68
+ upper: ' ▄▀█',
69
+ lower: '░█▀█',
70
+ },
71
+ b: {
72
+ upper: '░█▄▄',
73
+ lower: '░█▄█',
74
+ },
75
+ c: {
76
+ upper: '░█▀▀',
77
+ lower: '░█▄▄',
78
+ },
79
+ d: {
80
+ upper: '░█▀▄',
81
+ lower: '░█▄▀',
82
+ },
83
+ e: {
84
+ upper: '░█▀▀',
85
+ lower: '░██▄',
86
+ },
87
+ f: {
88
+ upper: '░█▀▀',
89
+ lower: '░█▀ ',
90
+ },
91
+ g: {
92
+ upper: '░█▀▀',
93
+ lower: '░█▄█',
94
+ },
95
+ h: {
96
+ upper: '░█░█',
97
+ lower: '░█▀█',
98
+ },
99
+ i: {
100
+ upper: '░█',
101
+ lower: '░█',
102
+ },
103
+ j: {
104
+ upper: '░░░█',
105
+ lower: '░█▄█',
106
+ },
107
+ k: {
108
+ upper: '░█▄▀',
109
+ lower: '░█░█',
110
+ },
111
+ l: {
112
+ upper: '░█░░',
113
+ lower: '░█▄▄',
114
+ },
115
+ m: {
116
+ upper: '░█▀▄▀█',
117
+ lower: '░█░▀░█',
118
+ },
119
+ n: {
120
+ upper: '░█▄░█',
121
+ lower: '░█░▀█',
122
+ },
123
+ o: {
124
+ upper: '░█▀█',
125
+ lower: '░█▄█',
126
+ },
127
+ p: {
128
+ upper: '░█▀█',
129
+ lower: '░█▀▀',
130
+ },
131
+ q: {
132
+ upper: '░█▀█',
133
+ lower: ' ▀▀█',
134
+ },
135
+ r: {
136
+ upper: '░█▀█',
137
+ lower: '░█▀▄',
138
+ },
139
+ s: {
140
+ upper: '░█▀',
141
+ lower: '░▄█'
142
+ },
143
+ t: {
144
+ upper: ' ▀█▀',
145
+ lower: '░░█░',
146
+ },
147
+ u: {
148
+ upper: '░█░█',
149
+ lower: '░█▄█',
150
+ },
151
+ v: {
152
+ upper: '░█░█',
153
+ lower: '░▀▄▀',
154
+ },
155
+ w: {
156
+ upper: '░█░█░█',
157
+ lower: '░▀▄▀▄▀',
158
+ },
159
+ x: {
160
+ upper: ' ▀▄▀',
161
+ lower: '░█░█'
162
+ },
163
+ y: {
164
+ upper: '░█▄█',
165
+ lower: '░░█░',
166
+ },
167
+ z: {
168
+ upper: '░▀█',
169
+ lower: '░█▄',
170
+ },
171
+ '-': {
172
+ upper: ' ▄▄',
173
+ lower: '░░░'
174
+ },
175
+ '+': {
176
+ upper: ' ▄█▄',
177
+ lower: '░░▀░',
178
+ },
179
+ '.': {
180
+ upper: '░',
181
+ lower: '▄',
182
+ },
183
+ };
184
+
185
+ function generateAsciiArt(text) {
186
+ let title = text || '\u0042\u006f\u0074\u0050\u0061\u0063\u006b';
187
+ const lines = [' ', ' '];
188
+ for (let i = 0; i < title.length; i++) {
189
+ const char = title[i].toLowerCase();
190
+ const mapping = asciiMappings[char] || '';
191
+ lines[0] += `${mapping.upper || ' '}`;
192
+ lines[1] += `${mapping.lower || ' '}`;
193
+ }
194
+ return lines.join('\n');
195
+ }
196
+
197
+ const $__ = _.DESIGN.Theme.toLowerCase() || '';
198
+ let ch;
199
+ let cre;
200
+ if ($__ === '\u0066'+'\u0069'+'\u0065'+'\u0072'+'\u0079') {
201
+ ch = gr.fruit;
202
+ cre = gr.fruit;
203
+ } else if ($__ === '\u0061' + '\u0071' + '\u0075' + '\u0061') {
204
+ ch = gr("#2e5fff", "#466deb");
205
+ cre = chalk.hex("#88c2f7");
206
+ } else if ($__ === '\u0068' + '\u0061' + '\u0063' + '\u006b' + '\u0065' + '\u0072') {
207
+ ch = gr('#47a127', '#0eed19', '#27f231');
208
+ cre = chalk.hex('#4be813');
209
+ } else if ($__ === '\u0070' + '\u0069' + '\u006e' + '\u006b') {
210
+ ch = gr("#ab68ed", "#ea3ef0", "#c93ef0");
211
+ cre = chalk.hex("#8c00ff");
212
+ } else if ($__ === '\u0062' + '\u006c' + '\u0075' + '\u0065') {
213
+ ch = gr("#243aff", "#4687f0", "#5800d4");
214
+ cre = chalk.blueBright;
215
+ } else if ($__ === '\u0073' + '\u0075' + '\u006e' + '\u006c' + '\u0069' + '\u0067' + '\u0068' + '\u0074') {
216
+ ch = gr("#ffae00", "#ffbf00", "#ffdd00");
217
+ cre = chalk.hex("#f6ff00");
218
+ } else if ($__ === '\u0072' + '\u0065' + '\u0064') {
219
+ ch = gr("#ff0000", "#ff0026");
220
+ cre = chalk.hex("#ff4747");
221
+ } else if ($__ === '\u0072' + '\u0065' + '\u0074' + '\u0072' + '\u006f') {
222
+ ch = gr.retro;
223
+ cre = chalk.hex("#7d02bf");
224
+ } else if ($__ === '\u0074' + '\u0065' + '\u0065' + '\u006e') {
225
+ ch = gr.teen;
226
+ cre = chalk.hex("#fa7f7f");
227
+ } else if ($__ === '\u0073' + '\u0075' + '\u006d' + '\u006d' + '\u0065' + '\u0072') {
228
+ ch = gr.summer;
229
+ cre = chalk.hex("#f7f565");
230
+ } else if ($__ === '\u0066' + '\u006c' + '\u006f' + '\u0077' + '\u0065' + '\u0072') {
231
+ ch = gr.pastel;
232
+ cre = chalk.hex("#6ded85");
233
+ } else if ($__ === '\u0067' + '\u0068' + '\u006f' + '\u0073' + '\u0074') {
234
+ ch = gr.mind;
235
+ cre = chalk.hex("#95d0de");
236
+ } else if ($__ === '\u0070'+'\u0075'+'\u0072'+'\u0070'+'\u006C'+'\u0065') {
237
+ ch = gr("#380478", "#5800d4", "#4687f0");
238
+ cre = chalk.hex('#7a039e');
239
+ } else if ($__ === '\u0072'+'\u0061'+'\u0069'+'\u006E'+'\u0062'+'\u006F'+'\u0077') {
240
+ ch = gr.rainbow
241
+ cre = chalk.hex('#0cb3eb');
242
+ } else if ($__ === '\u006F'+'\u0072'+'\u0061'+'\u006E'+'\u0067'+'\u0065') {
243
+ ch = gr("#ff8c08", "#ffad08", "#f5bb47");
244
+ cre = chalk.hex('#ff8400');
245
+ } else {
246
+ ch = gr("#243aff", "#4687f0", "#5800d4");
247
+ cre = chalk.blueBright;
248
+
249
+ setTimeout(() => {
250
+ 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`)
251
+ }, 1000);
252
+ };
253
+
254
+ setTimeout(() => {
255
+ const title = _.DESIGN.Title || '';
256
+ const asciiTitle = generateAsciiArt(title);
257
+ console.log(
258
+ ch.multiline('\n' + asciiTitle),
259
+ '\n',
260
+ ch(' \u2771 ') + '\u0043'+'\u0072'+'\u0065'+'\u0064'+'\u0069'+'\u0074'+'\u0073'+'\u0020'+'\u0074'+'\u006f',
261
+ cre('\u0059'+'\u0061'+'\u006E'+'\u0020'+'\u004D'+'\u0061'+'\u0067'+'\u006C'+'\u0069'+'\u006E'+'\u0074'+'\u0065'),
262
+ '\n',
263
+ ch(' \u2771 ') + `\u0041`+`\u0064`+`\u006d`+`\u0069`+`\u006e`+`\u003a ${cre(`${cb}`)}\n`
264
+ );
265
+ }, 1000);
266
+ }
includes/login/src/markAsRead.js ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return async function markAsRead(threadID, read, callback) {
8
+ if (utils.getType(read) === 'Function' || utils.getType(read) === 'AsyncFunction') {
9
+ callback = read;
10
+ read = true;
11
+ }
12
+ if (read == undefined) {
13
+ read = true;
14
+ }
15
+
16
+ if (!callback) {
17
+ callback = () => { };
18
+ }
19
+
20
+ const form = {};
21
+
22
+ if (typeof ctx.globalOptions.pageID !== 'undefined') {
23
+ form["source"] = "PagesManagerMessagesInterface";
24
+ form["request_user_id"] = ctx.globalOptions.pageID;
25
+ form["ids[" + threadID + "]"] = read;
26
+ form["watermarkTimestamp"] = new Date().getTime();
27
+ form["shouldSendReadReceipt"] = true;
28
+ form["commerce_last_message_type"] = "";
29
+ //form["titanOriginatedThreadId"] = utils.generateThreadingID(ctx.clientID);
30
+
31
+ let resData;
32
+ try {
33
+ resData = await (
34
+ defaultFuncs
35
+ .post(
36
+ "https://www.facebook.com/ajax/mercury/change_read_status.php",
37
+ ctx.jar,
38
+ form
39
+ )
40
+ .then(utils.saveCookies(ctx.jar))
41
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42
+ );
43
+ } catch (e) {
44
+ callback(e);
45
+ return e;
46
+ }
47
+
48
+ if (resData.error) {
49
+ const err = resData.error;
50
+ log.error("markAsRead", err);
51
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
52
+ ctx.loggedIn = false;
53
+ }
54
+ callback(err);
55
+ return err;
56
+ }
57
+
58
+ callback();
59
+ return null;
60
+ } else {
61
+ try {
62
+ if (ctx.mqttClient) {
63
+ const err = await new Promise(r => ctx.mqttClient.publish("/mark_thread", JSON.stringify({
64
+ threadID,
65
+ mark: "read",
66
+ state: read
67
+ }), { qos: 1, retain: false }, r));
68
+ if (err) throw err;
69
+ } else {
70
+ throw {
71
+ error: "You can only use this function after you start listening."
72
+ };
73
+ }
74
+ } catch (e) {
75
+ callback(e);
76
+ return e;
77
+ }
78
+ }
79
+ };
80
+ };
includes/login/src/markAsReadAll.js ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function markAsReadAll(callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ const form = {
25
+ folder: 'inbox'
26
+ };
27
+
28
+ defaultFuncs
29
+ .post(
30
+ "https://www.facebook.com/ajax/mercury/mark_folder_as_read.php",
31
+ ctx.jar,
32
+ form
33
+ )
34
+ .then(utils.saveCookies(ctx.jar))
35
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
36
+ .then(function (resData) {
37
+ if (resData.error) {
38
+ throw resData;
39
+ }
40
+
41
+ return callback();
42
+ })
43
+ .catch(function (err) {
44
+ log.error("markAsReadAll", err);
45
+ return callback(err);
46
+ });
47
+
48
+ return returnPromise;
49
+ };
50
+ };
includes/login/src/markAsSeen.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function markAsRead(seen_timestamp, callback) {
8
+ if (utils.getType(seen_timestamp) == "Function" ||
9
+ utils.getType(seen_timestamp) == "AsyncFunction") {
10
+ callback = seen_timestamp;
11
+ seen_timestamp = Date.now();
12
+ }
13
+
14
+ let resolveFunc = function () { };
15
+ let rejectFunc = function () { };
16
+ const returnPromise = new Promise(function (resolve, reject) {
17
+ resolveFunc = resolve;
18
+ rejectFunc = reject;
19
+ });
20
+
21
+ if (!callback) {
22
+ callback = function (err, friendList) {
23
+ if (err) {
24
+ return rejectFunc(err);
25
+ }
26
+ resolveFunc(friendList);
27
+ };
28
+ }
29
+
30
+ const form = {
31
+ seen_timestamp: seen_timestamp
32
+ };
33
+
34
+ defaultFuncs
35
+ .post(
36
+ "https://www.facebook.com/ajax/mercury/mark_seen.php",
37
+ ctx.jar,
38
+ form
39
+ )
40
+ .then(utils.saveCookies(ctx.jar))
41
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
42
+ .then(function (resData) {
43
+ if (resData.error) {
44
+ throw resData;
45
+ }
46
+
47
+ return callback();
48
+ })
49
+ .catch(function (err) {
50
+ log.error("markAsSeen", err);
51
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
52
+ ctx.loggedIn = false;
53
+ }
54
+ return callback(err);
55
+ });
56
+
57
+ return returnPromise;
58
+ };
59
+ };
includes/login/src/muteThread.js ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ // muteSecond: -1=permanent mute, 0=unmute, 60=one minute, 3600=one hour, etc.
8
+ return function muteThread(threadID, muteSeconds, callback) {
9
+ let resolveFunc = function () { };
10
+ let rejectFunc = function () { };
11
+ const returnPromise = new Promise(function (resolve, reject) {
12
+ resolveFunc = resolve;
13
+ rejectFunc = reject;
14
+ });
15
+
16
+ if (!callback) {
17
+ callback = function (err, friendList) {
18
+ if (err) {
19
+ return rejectFunc(err);
20
+ }
21
+ resolveFunc(friendList);
22
+ };
23
+ }
24
+
25
+ const form = {
26
+ thread_fbid: threadID,
27
+ mute_settings: muteSeconds
28
+ };
29
+
30
+ defaultFuncs
31
+ .post(
32
+ "https://www.facebook.com/ajax/mercury/change_mute_thread.php",
33
+ ctx.jar,
34
+ form
35
+ )
36
+ .then(utils.saveCookies(ctx.jar))
37
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
38
+ .then(function (resData) {
39
+ if (resData.error) {
40
+ throw resData;
41
+ }
42
+
43
+ return callback();
44
+ })
45
+ .catch(function (err) {
46
+ log.error("muteThread", err);
47
+ return callback(err);
48
+ });
49
+
50
+ return returnPromise;
51
+ };
52
+ };
includes/login/src/refreshFb_dtsg.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ /**
8
+ * Refreshes the fb_dtsg and jazoest values.
9
+ * @param {Function} callback
10
+ * @returns {Promise}
11
+ * @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
12
+ * @description you should refresh it every 48h or less
13
+ */
14
+ return function refreshFb_dtsg(obj, callback) {
15
+ let resolveFunc = function () { };
16
+ let rejectFunc = function () { };
17
+ const returnPromise = new Promise(function (resolve, reject) {
18
+ resolveFunc = resolve;
19
+ rejectFunc = reject;
20
+ });
21
+
22
+ if (utils.getType(obj) === "Function" || utils.getType(obj) === "AsyncFunction") {
23
+ callback = obj;
24
+ obj = {};
25
+ }
26
+
27
+ if (!obj) {
28
+ obj = {};
29
+ }
30
+
31
+ if (utils.getType(obj) !== "Object") {
32
+ throw new utils.CustomError("the first parameter must be an object or a callback function");
33
+ }
34
+
35
+ if (!callback) {
36
+ callback = function (err, friendList) {
37
+ if (err) {
38
+ return rejectFunc(err);
39
+ }
40
+ resolveFunc(friendList);
41
+ };
42
+ }
43
+
44
+ if (Object.keys(obj).length == 0) {
45
+ utils
46
+ .get('https://m.facebook.com/', ctx.jar, null, ctx.globalOptions, { noRef: true })
47
+ .then(function (resData) {
48
+ const html = resData.body;
49
+ const fb_dtsg = utils.getFrom(html, 'name="fb_dtsg" value="', '"');
50
+ const jazoest = utils.getFrom(html, 'name="jazoest" value="', '"');
51
+ if (!fb_dtsg) {
52
+ throw new utils.CustomError("Could not find fb_dtsg in HTML after requesting https://www.facebook.com/");
53
+ }
54
+ ctx.fb_dtsg = fb_dtsg;
55
+ ctx.jazoest = jazoest;
56
+ callback(null, {
57
+ data: {
58
+ fb_dtsg: fb_dtsg,
59
+ jazoest: jazoest
60
+ },
61
+ message: "refreshed fb_dtsg and jazoest"
62
+ });
63
+ })
64
+ .catch(function (err) {
65
+ log.error("refreshFb_dtsg", err);
66
+ return callback(err);
67
+ });
68
+ }
69
+ else {
70
+ Object.keys(obj).forEach(function (key) {
71
+ ctx[key] = obj[key];
72
+ });
73
+ callback(null, {
74
+ data: obj,
75
+ message: "refreshed " + Object.keys(obj).join(", ")
76
+ });
77
+ }
78
+
79
+ return returnPromise;
80
+ };
81
+ };
includes/login/src/removeUserFromGroup.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function removeUserFromGroup(userID, threadID, callback) {
8
+ if (
9
+ !callback &&
10
+ (utils.getType(threadID) === "Function" ||
11
+ utils.getType(threadID) === "AsyncFunction")
12
+ ) {
13
+ throw { error: "please pass a threadID as a second argument." };
14
+ }
15
+ if (
16
+ utils.getType(threadID) !== "Number" &&
17
+ utils.getType(threadID) !== "String"
18
+ ) {
19
+ throw {
20
+ error:
21
+ "threadID should be of type Number or String and not " +
22
+ utils.getType(threadID) +
23
+ "."
24
+ };
25
+ }
26
+ if (
27
+ utils.getType(userID) !== "Number" &&
28
+ utils.getType(userID) !== "String"
29
+ ) {
30
+ throw {
31
+ error:
32
+ "userID should be of type Number or String and not " +
33
+ utils.getType(userID) +
34
+ "."
35
+ };
36
+ }
37
+
38
+ let resolveFunc = function () { };
39
+ let rejectFunc = function () { };
40
+ const returnPromise = new Promise(function (resolve, reject) {
41
+ resolveFunc = resolve;
42
+ rejectFunc = reject;
43
+ });
44
+
45
+ if (!callback) {
46
+ callback = function (err, friendList) {
47
+ if (err) {
48
+ return rejectFunc(err);
49
+ }
50
+ resolveFunc(friendList);
51
+ };
52
+ }
53
+
54
+ const form = {
55
+ uid: userID,
56
+ tid: threadID
57
+ };
58
+
59
+ defaultFuncs
60
+ .post("https://www.facebook.com/chat/remove_participants", ctx.jar, form)
61
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
62
+ .then(function (resData) {
63
+ if (!resData) {
64
+ throw { error: "Remove from group failed." };
65
+ }
66
+ if (resData.error) {
67
+ throw resData;
68
+ }
69
+
70
+ return callback();
71
+ })
72
+ .catch(function (err) {
73
+ log.error("removeUserFromGroup", err);
74
+ return callback(err);
75
+ });
76
+
77
+ return returnPromise;
78
+ };
79
+ };
includes/login/src/resolvePhotoUrl.js ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function resolvePhotoUrl(photoID, callback) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ defaultFuncs
25
+ .get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, {
26
+ photo_id: photoID
27
+ })
28
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
29
+ .then(resData => {
30
+ if (resData.error) {
31
+ throw resData;
32
+ }
33
+
34
+ const photoUrl = resData.jsmods.require[0][3][0];
35
+
36
+ return callback(null, photoUrl);
37
+ })
38
+ .catch(err => {
39
+ log.error("resolvePhotoUrl", err);
40
+ return callback(err);
41
+ });
42
+
43
+ return returnPromise;
44
+ };
45
+ };
includes/login/src/searchForThread.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ return function searchForThread(name, callback) {
7
+ let resolveFunc = function () { };
8
+ let rejectFunc = function () { };
9
+ const returnPromise = new Promise(function (resolve, reject) {
10
+ resolveFunc = resolve;
11
+ rejectFunc = reject;
12
+ });
13
+
14
+ if (!callback) {
15
+ callback = function (err, friendList) {
16
+ if (err) {
17
+ return rejectFunc(err);
18
+ }
19
+ resolveFunc(friendList);
20
+ };
21
+ }
22
+
23
+ const tmpForm = {
24
+ client: "web_messenger",
25
+ query: name,
26
+ offset: 0,
27
+ limit: 21,
28
+ index: "fbid"
29
+ };
30
+
31
+ defaultFuncs
32
+ .post(
33
+ "https://www.facebook.com/ajax/mercury/search_threads.php",
34
+ ctx.jar,
35
+ tmpForm
36
+ )
37
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
38
+ .then(function (resData) {
39
+ if (resData.error) {
40
+ throw resData;
41
+ }
42
+ if (!resData.payload.mercury_payload.threads) {
43
+ return callback({ error: "Could not find thread `" + name + "`." });
44
+ }
45
+ return callback(
46
+ null,
47
+ resData.payload.mercury_payload.threads.map(utils.formatThread)
48
+ );
49
+ });
50
+
51
+ return returnPromise;
52
+ };
53
+ };
includes/login/src/sendComment.js ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Output:
2
+ "use strict";
3
+
4
+ var utils = require("../utils");
5
+ var log = require("npmlog");
6
+ var bluebird = require("bluebird");
7
+
8
+ module.exports = function (defaultFuncs, api, ctx) {
9
+ function getGUID() {
10
+ let _0x161e32 = Date.now(),
11
+ _0x4ec135 = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
12
+ /[xy]/g,
13
+ function (_0x32f946) {
14
+ let _0x141041 = Math.floor((_0x161e32 + Math.random() * 16) % 16);
15
+ _0x161e32 = Math.floor(_0x161e32 / 16);
16
+ let _0x31fcdd = (
17
+ _0x32f946 == "x" ? _0x141041 : (_0x141041 & 0x3) | 0x8
18
+ ).toString(16);
19
+ return _0x31fcdd;
20
+ },
21
+ );
22
+ return _0x4ec135;
23
+ }
24
+
25
+ function uploadAttachment(attachment, callback) {
26
+ var uploads = [];
27
+
28
+ // create an array of promises
29
+ if (!utils.isReadableStream(attachment)) {
30
+ throw {
31
+ error:
32
+ "Attachment should be a readable stream and not " +
33
+ utils.getType(attachment) +
34
+ ".",
35
+ };
36
+ }
37
+
38
+ var form = {
39
+ file: attachment,
40
+ av: api.getCurrentUserID(),
41
+ profile_id: api.getCurrentUserID(),
42
+ source: "19",
43
+ target_id: api.getCurrentUserID(),
44
+ __user: api.getCurrentUserID(),
45
+ __a: "1",
46
+ };
47
+
48
+ uploads.push(
49
+ defaultFuncs
50
+ .postFormData(
51
+ "https://www.facebook.com/ajax/ufi/upload",
52
+ ctx.jar,
53
+ form,
54
+ {},
55
+ )
56
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
57
+ .then(function (resData) {
58
+ if (resData.error) {
59
+ throw resData;
60
+ }
61
+ return resData.payload;
62
+ }),
63
+ );
64
+
65
+ // resolve all promises
66
+ bluebird
67
+ .all(uploads)
68
+ .then(function (resData) {
69
+ callback(null, resData);
70
+ })
71
+ .catch(function (err) {
72
+ log.error("uploadAttachment", err);
73
+ return callback(err);
74
+ });
75
+ }
76
+
77
+ async function sendCommentToFb(postId, text, fileID) {
78
+ const feedback_id = Buffer.from("feedback:" + postId).toString("base64");
79
+
80
+ const ss1 = getGUID();
81
+ const ss2 = getGUID();
82
+
83
+ const form = {
84
+ av: api.getCurrentUserID(),
85
+ fb_api_req_friendly_name: "CometUFICreateCommentMutation",
86
+ fb_api_caller_class: "RelayModern",
87
+ doc_id: "4744517358977326",
88
+ variables: JSON.stringify({
89
+ displayCommentsFeedbackContext: null,
90
+ displayCommentsContextEnableComment: null,
91
+ displayCommentsContextIsAdPreview: null,
92
+ displayCommentsContextIsAggregatedShare: null,
93
+ displayCommentsContextIsStorySet: null,
94
+ feedLocation: "TIMELINE",
95
+ feedbackSource: 0,
96
+ focusCommentID: null,
97
+ includeNestedComments: false,
98
+ input: {
99
+ attachments: fileID ? [{ media: { id: fileID } }] : null,
100
+ feedback_id: feedback_id,
101
+ formatting_style: null,
102
+ message: {
103
+ ranges: [],
104
+ text: text,
105
+ },
106
+ is_tracking_encrypted: true,
107
+ tracking: [],
108
+ feedback_source: "PROFILE",
109
+ idempotence_token: "client:" + ss1,
110
+ session_id: ss2,
111
+ actor_id: api.getCurrentUserID(),
112
+ client_mutation_id: Math.round(Math.random() * 19),
113
+ },
114
+ scale: 3,
115
+ useDefaultActor: false,
116
+ UFI2CommentsProvider_commentsKey: "ProfileCometTimelineRoute",
117
+ }),
118
+ };
119
+
120
+ const res = JSON.parse(
121
+ await api.httpPost("https://www.facebook.com/api/graphql/", form),
122
+ );
123
+ return res;
124
+ }
125
+
126
+ return async function sendComment(content, postId, callback) {
127
+ if (typeof content === "object") {
128
+ var text = content.body || "";
129
+ if (content.attachment) {
130
+ if (!utils.isReadableStream(content.attachment)) {
131
+ throw new Error("Attachment must be a ReadableStream");
132
+ }
133
+
134
+ uploadAttachment(content.attachment, async function (err, files) {
135
+ if (err) {
136
+ return callback(err);
137
+ }
138
+
139
+ await sendCommentToFb(postId, text, files[0].fbid)
140
+ .then((res) => {
141
+ return callback(null, res);
142
+ })
143
+ .catch((err) => {
144
+ return callback(err);
145
+ });
146
+ });
147
+ }
148
+ } else if (typeof content === "string") {
149
+ var text = content;
150
+ await sendCommentToFb(postId, text, null)
151
+ .then((res) => {
152
+ return callback(null, res);
153
+ })
154
+ .catch((_) => {
155
+ return;
156
+ });
157
+ } else throw new Error("Invalid content");
158
+ };
159
+ };
160
+ // example usage
includes/login/src/sendMessage.js ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ var utils = require("../utils");
4
+ var log = require("npmlog");
5
+ var bluebird = require("bluebird");
6
+
7
+ var allowedProperties = {
8
+ attachment: true,
9
+ url: true,
10
+ sticker: true,
11
+ emoji: true,
12
+ emojiSize: true,
13
+ body: true,
14
+ mentions: true,
15
+ location: true,
16
+ };
17
+
18
+ module.exports = function (defaultFuncs, api, ctx) {
19
+ function uploadAttachment(attachments, callback) {
20
+ var uploads = [];
21
+
22
+ // create an array of promises
23
+ for (var i = 0; i < attachments.length; i++) {
24
+ if (!utils.isReadableStream(attachments[i])) {
25
+ throw {
26
+ error:
27
+ "Attachment should be a readable stream and not " +
28
+ utils.getType(attachments[i]) +
29
+ "."
30
+ };
31
+ }
32
+
33
+ var form = {
34
+ upload_1024: attachments[i],
35
+ voice_clip: "true"
36
+ };
37
+
38
+ uploads.push(
39
+ defaultFuncs
40
+ .postFormData(
41
+ "https://upload.facebook.com/ajax/mercury/upload.php",
42
+ ctx.jar,
43
+ form,
44
+ {}
45
+ )
46
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
47
+ .then(function (resData) {
48
+ if (resData.error) {
49
+ throw resData;
50
+ }
51
+
52
+ // We have to return the data unformatted unless we want to change it
53
+ // back in sendMessage.
54
+ return resData.payload.metadata[0];
55
+ })
56
+ );
57
+ }
58
+
59
+ // resolve all promises
60
+ bluebird
61
+ .all(uploads)
62
+ .then(function (resData) {
63
+ callback(null, resData);
64
+ })
65
+ .catch(function (err) {
66
+ log.error("uploadAttachment", err);
67
+ return callback(err);
68
+ });
69
+ }
70
+
71
+ function getUrl(url, callback) {
72
+ var form = {
73
+ image_height: 960,
74
+ image_width: 960,
75
+ uri: url
76
+ };
77
+
78
+ defaultFuncs
79
+ .post(
80
+ "https://www.facebook.com/message_share_attachment/fromURI/",
81
+ ctx.jar,
82
+ form
83
+ )
84
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
85
+ .then(function (resData) {
86
+ if (resData.error) {
87
+ return callback(resData);
88
+ }
89
+
90
+ if (!resData.payload) {
91
+ return callback({ error: "Invalid url" });
92
+ }
93
+
94
+ callback(null, resData.payload.share_data.share_params);
95
+ })
96
+ .catch(function (err) {
97
+ log.error("getUrl", err);
98
+ return callback(err);
99
+ });
100
+ }
101
+
102
+ function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
103
+ // There are three cases here:
104
+ // 1. threadID is of type array, where we're starting a new group chat with users
105
+ // specified in the array.
106
+ // 2. User is sending a message to a specific user.
107
+ // 3. No additional form params and the message goes to an existing group chat.
108
+ if (utils.getType(threadID) === "Array") {
109
+ for (var i = 0; i < threadID.length; i++) {
110
+ form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
111
+ }
112
+ form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
113
+ form["client_thread_id"] = "root:" + messageAndOTID;
114
+ log.info("sendMessage", "Sending message to multiple users: " + threadID);
115
+ } else {
116
+ // This means that threadID is the id of a user, and the chat
117
+ // is a single person chat
118
+ if (isSingleUser) {
119
+ form["specific_to_list[0]"] = "fbid:" + threadID;
120
+ form["specific_to_list[1]"] = "fbid:" + ctx.userID;
121
+ form["other_user_fbid"] = threadID;
122
+ } else {
123
+ form["thread_fbid"] = threadID;
124
+ }
125
+ }
126
+
127
+ if (ctx.globalOptions.pageID) {
128
+ form["author"] = "fbid:" + ctx.globalOptions.pageID;
129
+ form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
130
+ form["creator_info[creatorID]"] = ctx.userID;
131
+ form["creator_info[creatorType]"] = "direct_admin";
132
+ form["creator_info[labelType]"] = "sent_message";
133
+ form["creator_info[pageID]"] = ctx.globalOptions.pageID;
134
+ form["request_user_id"] = ctx.globalOptions.pageID;
135
+ form["creator_info[profileURI]"] =
136
+ "https://www.facebook.com/profile.php?id=" + ctx.userID;
137
+ }
138
+
139
+ defaultFuncs
140
+ .post("https://www.facebook.com/messaging/send/", ctx.jar, form)
141
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
142
+ .then(function (resData) {
143
+ if (!resData) {
144
+ return callback({ error: "Send message failed." });
145
+ }
146
+
147
+ if (resData.error) {
148
+ if (resData.error === 1545012) {
149
+ log.warn(
150
+ "sendMessage",
151
+ "Got error 1545012. This might mean that you're not part of the conversation " +
152
+ threadID
153
+ );
154
+ }
155
+ return callback(resData);
156
+ }
157
+
158
+ var messageInfo = resData.payload.actions.reduce(function (p, v) {
159
+ return (
160
+ {
161
+ threadID: v.thread_fbid,
162
+ messageID: v.message_id,
163
+ timestamp: v.timestamp
164
+ } || p
165
+ );
166
+ }, null);
167
+
168
+ return callback(null, messageInfo);
169
+ })
170
+ .catch(function (err) {
171
+ //log.error("sendMessage", err);
172
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
173
+ ctx.loggedIn = false;
174
+ }
175
+ return callback(err);
176
+ });
177
+ }
178
+
179
+ function send(form, threadID, messageAndOTID, callback, isGroup) {
180
+ // We're doing a query to this to check if the given id is the id of
181
+ // a user or of a group chat. The form will be different depending
182
+ // on that.
183
+ if (utils.getType(threadID) === "Array") {
184
+ sendContent(form, threadID, false, messageAndOTID, callback);
185
+ } else {
186
+ if (utils.getType(isGroup) != "Boolean")
187
+ sendContent(form, threadID, threadID.length <= 15, messageAndOTID, callback);
188
+ else
189
+ sendContent(form, threadID, !isGroup, messageAndOTID, callback);
190
+ }
191
+ }
192
+
193
+ function handleUrl(msg, form, callback, cb) {
194
+ if (msg.url) {
195
+ form["shareable_attachment[share_type]"] = "100";
196
+ getUrl(msg.url, function (err, params) {
197
+ if (err) {
198
+ return callback(err);
199
+ }
200
+
201
+ form["shareable_attachment[share_params]"] = params;
202
+ cb();
203
+ });
204
+ } else {
205
+ cb();
206
+ }
207
+ }
208
+
209
+ function handleLocation(msg, form, callback, cb) {
210
+ if (msg.location) {
211
+ if (msg.location.latitude == null || msg.location.longitude == null) {
212
+ return callback({ error: "location property needs both latitude and longitude" });
213
+ }
214
+
215
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
216
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
217
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
218
+ }
219
+
220
+ cb();
221
+ }
222
+
223
+ function handleSticker(msg, form, callback, cb) {
224
+ if (msg.sticker) {
225
+ form["sticker_id"] = msg.sticker;
226
+ }
227
+ cb();
228
+ }
229
+
230
+ function handleEmoji(msg, form, callback, cb) {
231
+ if (msg.emojiSize != null && msg.emoji == null) {
232
+ return callback({ error: "emoji property is empty" });
233
+ }
234
+ if (msg.emoji) {
235
+ if (msg.emojiSize == null) {
236
+ msg.emojiSize = "medium";
237
+ }
238
+ if (
239
+ msg.emojiSize != "small" &&
240
+ msg.emojiSize != "medium" &&
241
+ msg.emojiSize != "large"
242
+ ) {
243
+ return callback({ error: "emojiSize property is invalid" });
244
+ }
245
+ if (form["body"] != null && form["body"] != "") {
246
+ return callback({ error: "body is not empty" });
247
+ }
248
+ form["body"] = msg.emoji;
249
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
250
+ }
251
+ cb();
252
+ }
253
+
254
+ function handleAttachment(msg, form, callback, cb) {
255
+ if (msg.attachment) {
256
+ form["image_ids"] = [];
257
+ form["gif_ids"] = [];
258
+ form["file_ids"] = [];
259
+ form["video_ids"] = [];
260
+ form["audio_ids"] = [];
261
+
262
+ if (utils.getType(msg.attachment) !== "Array") {
263
+ msg.attachment = [msg.attachment];
264
+ }
265
+
266
+ uploadAttachment(msg.attachment, function (err, files) {
267
+ if (err) {
268
+ return callback(err);
269
+ }
270
+
271
+ files.forEach(function (file) {
272
+ var key = Object.keys(file);
273
+ var type = key[0]; // image_id, file_id, etc
274
+ form["" + type + "s"].push(file[type]); // push the id
275
+ });
276
+ cb();
277
+ });
278
+ } else {
279
+ cb();
280
+ }
281
+ }
282
+
283
+ function handleMention(msg, form, callback, cb) {
284
+ if (msg.mentions) {
285
+ for (let i = 0; i < msg.mentions.length; i++) {
286
+ const mention = msg.mentions[i];
287
+
288
+ const tag = mention.tag;
289
+ if (typeof tag !== "string") {
290
+ return callback({ error: "Mention tags must be strings." });
291
+ }
292
+
293
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
294
+
295
+ if (offset < 0) {
296
+ log.warn(
297
+ "handleMention",
298
+ 'Mention for "' + tag + '" not found in message string.'
299
+ );
300
+ }
301
+
302
+ if (mention.id == null) {
303
+ log.warn("handleMention", "Mention id should be non-null.");
304
+ }
305
+
306
+ const id = mention.id || 0;
307
+ const emptyChar = '\u200E';
308
+ form["body"] = emptyChar + msg.body;
309
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
310
+ form["profile_xmd[" + i + "][length]"] = tag.length;
311
+ form["profile_xmd[" + i + "][id]"] = id;
312
+ form["profile_xmd[" + i + "][type]"] = "p";
313
+ }
314
+ }
315
+ cb();
316
+ }
317
+
318
+ return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
319
+ typeof isGroup == "undefined" ? isGroup = null : "";
320
+ if (
321
+ !callback &&
322
+ (utils.getType(threadID) === "Function" ||
323
+ utils.getType(threadID) === "AsyncFunction")
324
+ ) {
325
+ return threadID({ error: "Pass a threadID as a second argument." });
326
+ }
327
+ if (
328
+ !replyToMessage &&
329
+ utils.getType(callback) === "String"
330
+ ) {
331
+ replyToMessage = callback;
332
+ callback = undefined;
333
+ }
334
+
335
+ var resolveFunc = function () { };
336
+ var rejectFunc = function () { };
337
+ var returnPromise = new Promise(function (resolve, reject) {
338
+ resolveFunc = resolve;
339
+ rejectFunc = reject;
340
+ });
341
+
342
+ if (!callback) {
343
+ callback = function (err, data) {
344
+ if (err) return rejectFunc(err);
345
+ resolveFunc(data);
346
+ };
347
+ }
348
+
349
+ var msgType = utils.getType(msg);
350
+ var threadIDType = utils.getType(threadID);
351
+ var messageIDType = utils.getType(replyToMessage);
352
+
353
+ if (msgType !== "String" && msgType !== "Object") {
354
+ return callback({
355
+ error:
356
+ "Message should be of type string or object and not " + msgType + "."
357
+ });
358
+ }
359
+
360
+ // Changing this to accomodate an array of users
361
+ if (
362
+ threadIDType !== "Array" &&
363
+ threadIDType !== "Number" &&
364
+ threadIDType !== "String"
365
+ ) {
366
+ return callback({
367
+ error:
368
+ "ThreadID should be of type number, string, or array and not " +
369
+ threadIDType +
370
+ "."
371
+ });
372
+ }
373
+
374
+ if (replyToMessage && messageIDType !== 'String') {
375
+ return callback({
376
+ error:
377
+ "MessageID should be of type string and not " +
378
+ threadIDType +
379
+ "."
380
+ });
381
+ }
382
+
383
+ if (msgType === "String") {
384
+ msg = { body: msg };
385
+ }
386
+
387
+ var disallowedProperties = Object.keys(msg).filter(
388
+ prop => !allowedProperties[prop]
389
+ );
390
+ if (disallowedProperties.length > 0) {
391
+ return callback({
392
+ error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`"
393
+ });
394
+ }
395
+
396
+ var messageAndOTID = utils.generateOfflineThreadingID();
397
+
398
+ var form = {
399
+ client: "mercury",
400
+ action_type: "ma-type:user-generated-message",
401
+ author: "fbid:" + ctx.userID,
402
+ timestamp: Date.now(),
403
+ timestamp_absolute: "Today",
404
+ timestamp_relative: utils.generateTimestampRelative(),
405
+ timestamp_time_passed: "0",
406
+ is_unread: false,
407
+ is_cleared: false,
408
+ is_forward: false,
409
+ is_filtered_content: false,
410
+ is_filtered_content_bh: false,
411
+ is_filtered_content_account: false,
412
+ is_filtered_content_quasar: false,
413
+ is_filtered_content_invalid_app: false,
414
+ is_spoof_warning: false,
415
+ source: "source:chat:web",
416
+ "source_tags[0]": "source:chat",
417
+ body: msg.body ? msg.body.toString() : "",
418
+ html_body: false,
419
+ ui_push_phase: "V3",
420
+ status: "0",
421
+ offline_threading_id: messageAndOTID,
422
+ message_id: messageAndOTID,
423
+ threading_id: utils.generateThreadingID(ctx.clientID),
424
+ "ephemeral_ttl_mode:": "0",
425
+ manual_retry_cnt: "0",
426
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
427
+ signatureID: utils.getSignatureID(),
428
+ replied_to_message_id: replyToMessage
429
+ };
430
+
431
+ handleLocation(msg, form, callback, () =>
432
+ handleSticker(msg, form, callback, () =>
433
+ handleAttachment(msg, form, callback, () =>
434
+ handleUrl(msg, form, callback, () =>
435
+ handleEmoji(msg, form, callback, () =>
436
+ handleMention(msg, form, callback, () =>
437
+ send(form, threadID, messageAndOTID, callback, isGroup)
438
+ )
439
+ )
440
+ )
441
+ )
442
+ )
443
+ );
444
+
445
+ return returnPromise;
446
+ };
447
+ };
includes/login/src/sendMessageMqtt.js ADDED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var utils = require("../utils");
2
+ var log = require("npmlog");
3
+ var bluebird = require("bluebird");
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ function uploadAttachment(attachments, callback) {
7
+ callback = callback || function () { };
8
+ var uploads = [];
9
+
10
+ // create an array of promises
11
+ for (var i = 0; i < attachments.length; i++) {
12
+ if (!utils.isReadableStream(attachments[i])) {
13
+ throw {
14
+ error:
15
+ "Attachment should be a readable stream and not " +
16
+ utils.getType(attachments[i]) +
17
+ "."
18
+ };
19
+ }
20
+
21
+ var form = {
22
+ upload_1024: attachments[i],
23
+ voice_clip: "true"
24
+ };
25
+
26
+ uploads.push(
27
+ defaultFuncs
28
+ .postFormData(
29
+ "https://upload.facebook.com/ajax/mercury/upload.php",
30
+ ctx.jar,
31
+ form,
32
+ {}
33
+ )
34
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35
+ .then(function (resData) {
36
+ if (resData.error) {
37
+ throw resData;
38
+ }
39
+
40
+ // We have to return the data unformatted unless we want to change it
41
+ // back in sendMessage.
42
+ return resData.payload.metadata[0];
43
+ })
44
+ );
45
+ }
46
+
47
+ // resolve all promises
48
+ bluebird
49
+ .all(uploads)
50
+ .then(function (resData) {
51
+ callback(null, resData);
52
+ })
53
+ .catch(function (err) {
54
+ log.error("uploadAttachment", err);
55
+ return callback(err);
56
+ });
57
+ }
58
+
59
+ let variance = 0;
60
+ const epoch_id = () => Math.floor(Date.now() * (4194304 + (variance = (variance + 0.1) % 5)));
61
+ const emojiSizes = {
62
+ small: 1,
63
+ medium: 2,
64
+ large: 3
65
+ };
66
+
67
+ function handleEmoji(msg, form, callback, cb) {
68
+ if (msg.emojiSize != null && msg.emoji == null) {
69
+ return callback({ error: "emoji property is empty" });
70
+ }
71
+ if (msg.emoji) {
72
+ if (!msg.emojiSize) {
73
+ msg.emojiSize = "small";
74
+ }
75
+ if (
76
+ msg.emojiSize !== "small" &&
77
+ msg.emojiSize !== "medium" &&
78
+ msg.emojiSize !== "large" &&
79
+ (isNaN(msg.emojiSize) || msg.emojiSize < 1 || msg.emojiSize > 3)
80
+ ) {
81
+ return callback({ error: "emojiSize property is invalid" });
82
+ }
83
+
84
+ form.payload.tasks[0].payload.send_type = 1;
85
+ form.payload.tasks[0].payload.text = msg.emoji;
86
+ form.payload.tasks[0].payload.hot_emoji_size = !isNaN(msg.emojiSize) ? msg.emojiSize : emojiSizes[msg.emojiSize];
87
+ }
88
+ cb();
89
+ }
90
+
91
+ function handleSticker(msg, form, callback, cb) {
92
+ if (msg.sticker) {
93
+ form.payload.tasks[0].payload.send_type = 2;
94
+ form.payload.tasks[0].payload.sticker_id = msg.sticker;
95
+ }
96
+ cb();
97
+ }
98
+
99
+ function handleAttachment(msg, form, callback, cb) {
100
+ if (msg.attachment) {
101
+ form.payload.tasks[0].payload.send_type = 3;
102
+ form.payload.tasks[0].payload.attachment_fbids = [];
103
+ if (form.payload.tasks[0].payload.text == "")
104
+ form.payload.tasks[0].payload.text = null;
105
+ if (utils.getType(msg.attachment) !== "Array") {
106
+ msg.attachment = [msg.attachment];
107
+ }
108
+
109
+ uploadAttachment(msg.attachment, function (err, files) {
110
+ if (err) {
111
+ return callback(err);
112
+ }
113
+
114
+ files.forEach(function (file) {
115
+ var key = Object.keys(file);
116
+ var type = key[0]; // image_id, file_id, etc
117
+ form.payload.tasks[0].payload.attachment_fbids.push(file[type]); // push the id
118
+ });
119
+ cb();
120
+ });
121
+ } else {
122
+ cb();
123
+ }
124
+ }
125
+
126
+
127
+ function handleMention(msg, form, callback, cb) {
128
+ if (msg.mentions) {
129
+ form.payload.tasks[0].payload.send_type = 1;
130
+
131
+ const arrayIds = [];
132
+ const arrayOffsets = [];
133
+ const arrayLengths = [];
134
+ const mention_types = [];
135
+
136
+ for (let i = 0; i < msg.mentions.length; i++) {
137
+ const mention = msg.mentions[i];
138
+
139
+ const tag = mention.tag;
140
+ if (typeof tag !== "string") {
141
+ return callback({ error: "Mention tags must be strings." });
142
+ }
143
+
144
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
145
+
146
+ if (offset < 0) {
147
+ log.warn(
148
+ "handleMention",
149
+ 'Mention for "' + tag + '" not found in message string.'
150
+ );
151
+ }
152
+
153
+ if (mention.id == null) {
154
+ log.warn("handleMention", "Mention id should be non-null.");
155
+ }
156
+
157
+ const id = mention.id || 0;
158
+ arrayIds.push(id);
159
+ arrayOffsets.push(offset);
160
+ arrayLengths.push(tag.length);
161
+ mention_types.push("p");
162
+ }
163
+
164
+ form.payload.tasks[0].payload.mention_data = {
165
+ mention_ids: arrayIds.join(","),
166
+ mention_offsets: arrayOffsets.join(","),
167
+ mention_lengths: arrayLengths.join(","),
168
+ mention_types: mention_types.join(",")
169
+ };
170
+ }
171
+ cb();
172
+ }
173
+
174
+ function handleLocation(msg, form, callback, cb) {
175
+ // this is not working yet
176
+ if (msg.location) {
177
+ if (msg.location.latitude == null || msg.location.longitude == null) {
178
+ return callback({ error: "location property needs both latitude and longitude" });
179
+ }
180
+
181
+ form.payload.tasks[0].payload.send_type = 1;
182
+ form.payload.tasks[0].payload.location_data = {
183
+ coordinates: {
184
+ latitude: msg.location.latitude,
185
+ longitude: msg.location.longitude
186
+ },
187
+ is_current_location: !!msg.location.current,
188
+ is_live_location: !!msg.location.live
189
+ };
190
+ }
191
+
192
+ cb();
193
+ }
194
+
195
+ function send(form, threadID, callback, replyToMessage) {
196
+ if (replyToMessage) {
197
+ form.payload.tasks[0].payload.reply_metadata = {
198
+ reply_source_id: replyToMessage,
199
+ reply_source_type: 1,
200
+ reply_type: 0
201
+ };
202
+ }
203
+ const mqttClient = ctx.mqttClient;
204
+ form.payload.tasks.forEach((task) => {
205
+ task.payload = JSON.stringify(task.payload);
206
+ });
207
+ form.payload = JSON.stringify(form.payload);
208
+ console.log(global.jsonStringifyColor(form, null, 2));
209
+
210
+ return mqttClient.publish("/ls_req", JSON.stringify(form), function (err, data) {
211
+ if (err) {
212
+ console.error('Error publishing message: ', err);
213
+ callback(err);
214
+ } else {
215
+ console.log('Message published successfully with data: ', data);
216
+ callback(null, data);
217
+ }
218
+ });
219
+ }
220
+
221
+ return function sendMessageMqtt(msg, threadID, callback, replyToMessage) {
222
+ if (
223
+ !callback &&
224
+ (utils.getType(threadID) === "Function" ||
225
+ utils.getType(threadID) === "AsyncFunction")
226
+ ) {
227
+ return threadID({ error: "Pass a threadID as a second argument." });
228
+ }
229
+ if (
230
+ !replyToMessage &&
231
+ utils.getType(callback) === "String"
232
+ ) {
233
+ replyToMessage = callback;
234
+ callback = function () { };
235
+ }
236
+
237
+
238
+ if (!callback) {
239
+ callback = function (err, friendList) {
240
+ };
241
+ }
242
+
243
+ var msgType = utils.getType(msg);
244
+ var threadIDType = utils.getType(threadID);
245
+ var messageIDType = utils.getType(replyToMessage);
246
+
247
+ if (msgType !== "String" && msgType !== "Object") {
248
+ return callback({
249
+ error:
250
+ "Message should be of type string or object and not " + msgType + "."
251
+ });
252
+ }
253
+
254
+ if (msgType === "String") {
255
+ msg = { body: msg };
256
+ }
257
+
258
+ const timestamp = Date.now();
259
+ // get full date time
260
+ const epoch = timestamp << 22;
261
+ //const otid = epoch + 0; // TODO replace with randomInt(0, 2**22)
262
+ const otid = epoch + Math.floor(Math.random() * 4194304);
263
+
264
+ const form = {
265
+ app_id: "2220391788200892",
266
+ payload: {
267
+ tasks: [
268
+ {
269
+ label: "46",
270
+ payload: {
271
+ thread_id: threadID.toString(),
272
+ otid: otid.toString(),
273
+ source: 0,
274
+ send_type: 1,
275
+ sync_group: 1,
276
+ text: msg.body != null && msg.body != undefined ? msg.body.toString() : "",
277
+ initiating_source: 1,
278
+ skip_url_preview_gen: 0
279
+ },
280
+ queue_name: threadID.toString(),
281
+ task_id: 0,
282
+ failure_count: null
283
+ },
284
+ {
285
+ label: "21",
286
+ payload: {
287
+ thread_id: threadID.toString(),
288
+ last_read_watermark_ts: Date.now(),
289
+ sync_group: 1
290
+ },
291
+ queue_name: threadID.toString(),
292
+ task_id: 1,
293
+ failure_count: null
294
+ }
295
+ ],
296
+ epoch_id: epoch_id(),
297
+ version_id: "6120284488008082",
298
+ data_trace_id: null
299
+ },
300
+ request_id: 1,
301
+ type: 3
302
+ };
303
+
304
+ handleEmoji(msg, form, callback, function () {
305
+ handleLocation(msg, form, callback, function () {
306
+ handleMention(msg, form, callback, function () {
307
+ handleSticker(msg, form, callback, function () {
308
+ handleAttachment(msg, form, callback, function () {
309
+ send(form, threadID, callback, replyToMessage);
310
+ });
311
+ });
312
+ });
313
+ });
314
+ });
315
+ };
316
+ };
includes/login/src/sendTypingIndicator.js ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ function makeTypingIndicator(typ, threadID, callback, isGroup) {
8
+ const form = {
9
+ typ: +typ,
10
+ to: "",
11
+ source: "mercury-chat",
12
+ thread: threadID
13
+ };
14
+
15
+ // Check if thread is a single person chat or a group chat
16
+ // More info on this is in api.sendMessage
17
+ if (utils.getType(isGroup) == "Boolean") {
18
+ if (!isGroup) {
19
+ form.to = threadID;
20
+ }
21
+ defaultFuncs
22
+ .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
23
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
24
+ .then(function (resData) {
25
+ if (resData.error) {
26
+ throw resData;
27
+ }
28
+
29
+ return callback();
30
+ })
31
+ .catch(function (err) {
32
+ log.error("sendTypingIndicator", err);
33
+ if (utils.getType(err) == "Object" && err.error === "Not logged in") {
34
+ ctx.loggedIn = false;
35
+ }
36
+ return callback(err);
37
+ });
38
+ } else {
39
+ api.getUserInfo(threadID, function (err, res) {
40
+ if (err) {
41
+ return callback(err);
42
+ }
43
+
44
+ // If id is single person chat
45
+ if (Object.keys(res).length > 0) {
46
+ form.to = threadID;
47
+ }
48
+
49
+ defaultFuncs
50
+ .post("https://www.facebook.com/ajax/messaging/typ.php", ctx.jar, form)
51
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
52
+ .then(function (resData) {
53
+ if (resData.error) {
54
+ throw resData;
55
+ }
56
+
57
+ return callback();
58
+ })
59
+ .catch(function (err) {
60
+ log.error("sendTypingIndicator", err);
61
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
62
+ ctx.loggedIn = false;
63
+ }
64
+ return callback(err);
65
+ });
66
+ });
67
+ }
68
+ }
69
+
70
+ return function sendTypingIndicator(threadID, callback, isGroup) {
71
+ if (
72
+ utils.getType(callback) !== "Function" &&
73
+ utils.getType(callback) !== "AsyncFunction"
74
+ ) {
75
+ if (callback) {
76
+ log.warn(
77
+ "sendTypingIndicator",
78
+ "callback is not a function - ignoring."
79
+ );
80
+ }
81
+ callback = () => { };
82
+ }
83
+
84
+ makeTypingIndicator(true, threadID, callback, isGroup);
85
+
86
+ return function end(cb) {
87
+ if (
88
+ utils.getType(cb) !== "Function" &&
89
+ utils.getType(cb) !== "AsyncFunction"
90
+ ) {
91
+ if (cb) {
92
+ log.warn(
93
+ "sendTypingIndicator",
94
+ "callback is not a function - ignoring."
95
+ );
96
+ }
97
+ cb = () => { };
98
+ }
99
+
100
+ makeTypingIndicator(false, threadID, cb, isGroup);
101
+ };
102
+ };
103
+ };
includes/login/src/setMessageReaction.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function setMessageReaction(reaction, messageID, callback, forceCustomReaction) {
8
+ let resolveFunc = function () { };
9
+ let rejectFunc = function () { };
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, friendList) {
17
+ if (err) {
18
+ return rejectFunc(err);
19
+ }
20
+ resolveFunc(friendList);
21
+ };
22
+ }
23
+
24
+ switch (reaction) {
25
+ case "\uD83D\uDE0D": //:heart_eyes:
26
+ case "\uD83D\uDE06": //:laughing:
27
+ case "\uD83D\uDE2E": //:open_mouth:
28
+ case "\uD83D\uDE22": //:cry:
29
+ case "\uD83D\uDE20": //:angry:
30
+ case "\uD83D\uDC4D": //:thumbsup:
31
+ case "\uD83D\uDC4E": //:thumbsdown:
32
+ case "\u2764": //:heart:
33
+ case "\uD83D\uDC97": //:glowingheart:
34
+ case "":
35
+ //valid
36
+ break;
37
+ case ":heart_eyes:":
38
+ case ":love:":
39
+ reaction = "\uD83D\uDE0D";
40
+ break;
41
+ case ":laughing:":
42
+ case ":haha:":
43
+ reaction = "\uD83D\uDE06";
44
+ break;
45
+ case ":open_mouth:":
46
+ case ":wow:":
47
+ reaction = "\uD83D\uDE2E";
48
+ break;
49
+ case ":cry:":
50
+ case ":sad:":
51
+ reaction = "\uD83D\uDE22";
52
+ break;
53
+ case ":angry:":
54
+ reaction = "\uD83D\uDE20";
55
+ break;
56
+ case ":thumbsup:":
57
+ case ":like:":
58
+ reaction = "\uD83D\uDC4D";
59
+ break;
60
+ case ":thumbsdown:":
61
+ case ":dislike:":
62
+ reaction = "\uD83D\uDC4E";
63
+ break;
64
+ case ":heart:":
65
+ reaction = "\u2764";
66
+ break;
67
+ case ":glowingheart:":
68
+ reaction = "\uD83D\uDC97";
69
+ break;
70
+ default:
71
+ if (forceCustomReaction) {
72
+ break;
73
+ }
74
+ return callback({ error: "Reaction is not a valid emoji." });
75
+ }
76
+
77
+ const variables = {
78
+ data: {
79
+ client_mutation_id: ctx.clientMutationId++,
80
+ actor_id: ctx.i_userID || ctx.userID,
81
+ action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
82
+ message_id: messageID,
83
+ reaction: reaction
84
+ }
85
+ };
86
+
87
+ const qs = {
88
+ doc_id: "1491398900900362",
89
+ variables: JSON.stringify(variables),
90
+ dpr: 1
91
+ };
92
+
93
+ defaultFuncs
94
+ .postFormData(
95
+ "https://www.facebook.com/webgraphql/mutation/",
96
+ ctx.jar,
97
+ {},
98
+ qs
99
+ )
100
+ .then(utils.parseAndCheckLogin(ctx.jar, defaultFuncs))
101
+ .then(function (resData) {
102
+ if (!resData) {
103
+ throw { error: "setReaction returned empty object." };
104
+ }
105
+ if (resData.error) {
106
+ throw resData;
107
+ }
108
+ callback(null);
109
+ })
110
+ .catch(function (err) {
111
+ log.error("setReaction", err);
112
+ return callback(err);
113
+ });
114
+
115
+ return returnPromise;
116
+ };
117
+ };
includes/login/src/setPostReaction.js ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * @Updated by @YanMaglinte
3
+ * updated on Sunday, 3 March 2024
4
+ * changed Buffer => Buffer.from()
5
+ * modified it since it was outdated
6
+ * Do not remove the author's name to get notified to the latest updates.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const utils = require("../utils");
12
+ const log = require("npmlog");
13
+
14
+ function formatData(resData) {
15
+ return {
16
+ viewer_feedback_reaction_info: resData.feedback_react.feedback.viewer_feedback_reaction_info,
17
+ supported_reactions: resData.feedback_react.feedback.supported_reactions,
18
+ top_reactions: resData.feedback_react.feedback.top_reactions.edges,
19
+ reaction_count: resData.feedback_react.feedback.reaction_count
20
+ };
21
+ }
22
+
23
+ module.exports = function (defaultFuncs, api, ctx) {
24
+ return function setPostReaction(postID, type, callback) {
25
+ let resolveFunc = function () { };
26
+ let rejectFunc = function () { };
27
+ const returnPromise = new Promise(function (resolve, reject) {
28
+ resolveFunc = resolve;
29
+ rejectFunc = reject;
30
+ });
31
+
32
+ if (!callback) {
33
+ if (utils.getType(type) === "Function" || utils.getType(type) === "AsyncFunction") {
34
+ callback = type;
35
+ type = 0;
36
+ }
37
+ else {
38
+ callback = function (err, data) {
39
+ if (err) {
40
+ return rejectFunc(err);
41
+ }
42
+ resolveFunc(data);
43
+ };
44
+ }
45
+ }
46
+
47
+ const map = {
48
+ unlike: 0,
49
+ like: 1,
50
+ heart: 2,
51
+ love: 16,
52
+ haha: 4,
53
+ wow: 3,
54
+ sad: 7,
55
+ angry: 8
56
+ };
57
+
58
+ if (utils.getType(type) !== "Number" && utils.getType(type) === "String") {
59
+ type = map[type.toLowerCase()];
60
+ }
61
+
62
+ if (utils.getType(type) !== "Number" && utils.getType(type) !== "String") {
63
+ throw {
64
+ error: "setPostReaction: Invalid reaction type"
65
+ };
66
+ }
67
+
68
+ if (type != 0 && !type) {
69
+ throw {
70
+ error: "setPostReaction: Invalid reaction type"
71
+ };
72
+ }
73
+
74
+ const form = {
75
+ av: ctx.i_userID || ctx.userID,
76
+ fb_api_caller_class: "RelayModern",
77
+ fb_api_req_friendly_name: "CometUFIFeedbackReactMutation",
78
+ doc_id: "4769042373179384",
79
+ variables: JSON.stringify({
80
+ input: {
81
+ actor_id: ctx.i_userID || ctx.userID,
82
+ feedback_id: (new Buffer.from("feedback:" + postID)).toString("base64"),
83
+ feedback_reaction: type,
84
+ feedback_source: "OBJECT",
85
+ is_tracking_encrypted: true,
86
+ tracking: [],
87
+ session_id: "f7dd50dd-db6e-4598-8cd9-561d5002b423",
88
+ client_mutation_id: Math.round(Math.random() * 19).toString()
89
+ },
90
+ useDefaultActor: false,
91
+ scale: 3
92
+ })
93
+ };
94
+
95
+ defaultFuncs
96
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
97
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
98
+ .then(function (resData) {
99
+ if (resData.errors) {
100
+ throw resData;
101
+ }
102
+ return callback(null, formatData(resData.data));
103
+ })
104
+ .catch(function (err) {
105
+ log.error("setPostReaction", err);
106
+ return callback(err);
107
+ });
108
+
109
+ return returnPromise;
110
+ };
111
+ };
includes/login/src/setTitle.js ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function setTitle(newTitle, threadID, callback) {
8
+ if (
9
+ !callback &&
10
+ (utils.getType(threadID) === "Function" ||
11
+ utils.getType(threadID) === "AsyncFunction")
12
+ ) {
13
+ throw { error: "please pass a threadID as a second argument." };
14
+ }
15
+
16
+ let resolveFunc = function () { };
17
+ let rejectFunc = function () { };
18
+ const returnPromise = new Promise(function (resolve, reject) {
19
+ resolveFunc = resolve;
20
+ rejectFunc = reject;
21
+ });
22
+
23
+ if (!callback) {
24
+ callback = function (err, friendList) {
25
+ if (err) {
26
+ return rejectFunc(err);
27
+ }
28
+ resolveFunc(friendList);
29
+ };
30
+ }
31
+
32
+ const messageAndOTID = utils.generateOfflineThreadingID();
33
+ const form = {
34
+ client: "mercury",
35
+ action_type: "ma-type:log-message",
36
+ author: "fbid:" + (ctx.i_userID || ctx.userID),
37
+ author_email: "",
38
+ coordinates: "",
39
+ timestamp: Date.now(),
40
+ timestamp_absolute: "Today",
41
+ timestamp_relative: utils.generateTimestampRelative(),
42
+ timestamp_time_passed: "0",
43
+ is_unread: false,
44
+ is_cleared: false,
45
+ is_forward: false,
46
+ is_filtered_content: false,
47
+ is_spoof_warning: false,
48
+ source: "source:chat:web",
49
+ "source_tags[0]": "source:chat",
50
+ status: "0",
51
+ offline_threading_id: messageAndOTID,
52
+ message_id: messageAndOTID,
53
+ threading_id: utils.generateThreadingID(ctx.clientID),
54
+ manual_retry_cnt: "0",
55
+ thread_fbid: threadID,
56
+ thread_name: newTitle,
57
+ thread_id: threadID,
58
+ log_message_type: "log:thread-name"
59
+ };
60
+
61
+ defaultFuncs
62
+ .post("https://www.facebook.com/messaging/set_thread_name/", ctx.jar, form)
63
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
64
+ .then(function (resData) {
65
+ if (resData.error && resData.error === 1545012) {
66
+ throw { error: "Cannot change chat title: Not member of chat." };
67
+ }
68
+
69
+ if (resData.error && resData.error === 1545003) {
70
+ throw { error: "Cannot set title of single-user chat." };
71
+ }
72
+
73
+ if (resData.error) {
74
+ throw resData;
75
+ }
76
+
77
+ return callback();
78
+ })
79
+ .catch(function (err) {
80
+ log.error("setTitle", err);
81
+ return callback(err);
82
+ });
83
+
84
+ return returnPromise;
85
+ };
86
+ };