fca-phantom 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +534 -0
  3. package/index.js +35 -0
  4. package/package.json +101 -0
  5. package/phantom/core/builder/bootstrap.js +334 -0
  6. package/phantom/core/builder/config.js +78 -0
  7. package/phantom/core/builder/forge.js +113 -0
  8. package/phantom/core/builder/ignite.js +386 -0
  9. package/phantom/core/builder/options.js +61 -0
  10. package/phantom/core/engine.js +71 -0
  11. package/phantom/core/reactor.js +2 -0
  12. package/phantom/datastore/appState.js +2 -0
  13. package/phantom/datastore/appStateBackup.js +34 -0
  14. package/phantom/datastore/models/cipher/e2ee.js +48 -0
  15. package/phantom/datastore/models/cipher/vault.js +153 -0
  16. package/phantom/datastore/models/index.js +3 -0
  17. package/phantom/datastore/models/matrix/auth.js +151 -0
  18. package/phantom/datastore/models/matrix/cache.js +3 -0
  19. package/phantom/datastore/models/matrix/checker.js +2 -0
  20. package/phantom/datastore/models/matrix/clients.js +2 -0
  21. package/phantom/datastore/models/matrix/constants.js +2 -0
  22. package/phantom/datastore/models/matrix/credentials.js +2 -0
  23. package/phantom/datastore/models/matrix/cycle.js +2 -0
  24. package/phantom/datastore/models/matrix/gate.js +282 -0
  25. package/phantom/datastore/models/matrix/ghost.js +332 -0
  26. package/phantom/datastore/models/matrix/headers.js +193 -0
  27. package/phantom/datastore/models/matrix/heartbeat.js +298 -0
  28. package/phantom/datastore/models/matrix/identity.js +235 -0
  29. package/phantom/datastore/models/matrix/logger.js +271 -0
  30. package/phantom/datastore/models/matrix/monitor.js +2 -0
  31. package/phantom/datastore/models/matrix/net.js +316 -0
  32. package/phantom/datastore/models/matrix/response.js +193 -0
  33. package/phantom/datastore/models/matrix/revive.js +255 -0
  34. package/phantom/datastore/models/matrix/signals.js +2 -0
  35. package/phantom/datastore/models/matrix/store.js +263 -0
  36. package/phantom/datastore/models/matrix/telemetry.js +272 -0
  37. package/phantom/datastore/models/matrix/tools.js +93 -0
  38. package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
  39. package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
  40. package/phantom/datastore/models/matrix/transform/index.js +203 -0
  41. package/phantom/datastore/models/matrix/validator.js +157 -0
  42. package/phantom/datastore/models/types/index.d.ts +498 -0
  43. package/phantom/datastore/schema.js +167 -0
  44. package/phantom/datastore/session.js +129 -0
  45. package/phantom/datastore/threads.js +22 -0
  46. package/phantom/datastore/users.js +26 -0
  47. package/phantom/dispatch/addExternalModule.js +239 -0
  48. package/phantom/dispatch/addUserToGroup.js +161 -0
  49. package/phantom/dispatch/changeAdminStatus.js +142 -0
  50. package/phantom/dispatch/changeArchivedStatus.js +135 -0
  51. package/phantom/dispatch/changeAvatar.js +123 -0
  52. package/phantom/dispatch/changeBio.js +86 -0
  53. package/phantom/dispatch/changeBlockedStatus.js +86 -0
  54. package/phantom/dispatch/changeGroupImage.js +145 -0
  55. package/phantom/dispatch/changeThreadColor.js +172 -0
  56. package/phantom/dispatch/changeThreadEmoji.js +130 -0
  57. package/phantom/dispatch/comment.js +136 -0
  58. package/phantom/dispatch/createAITheme.js +333 -0
  59. package/phantom/dispatch/createNewGroup.js +99 -0
  60. package/phantom/dispatch/createPoll.js +148 -0
  61. package/phantom/dispatch/deleteMessage.js +131 -0
  62. package/phantom/dispatch/deleteThread.js +155 -0
  63. package/phantom/dispatch/e2ee.js +101 -0
  64. package/phantom/dispatch/editMessage.js +158 -0
  65. package/phantom/dispatch/emoji.js +143 -0
  66. package/phantom/dispatch/fetchThemeData.js +233 -0
  67. package/phantom/dispatch/follow.js +111 -0
  68. package/phantom/dispatch/forwardMessage.js +110 -0
  69. package/phantom/dispatch/friend.js +189 -0
  70. package/phantom/dispatch/gcmember.js +138 -0
  71. package/phantom/dispatch/gcname.js +131 -0
  72. package/phantom/dispatch/gcrule.js +111 -0
  73. package/phantom/dispatch/getAccess.js +109 -0
  74. package/phantom/dispatch/getBotInfo.js +81 -0
  75. package/phantom/dispatch/getBotInitialData.js +110 -0
  76. package/phantom/dispatch/getFriendsList.js +118 -0
  77. package/phantom/dispatch/getMessage.js +199 -0
  78. package/phantom/dispatch/getTheme.js +199 -0
  79. package/phantom/dispatch/getThemeInfo.js +160 -0
  80. package/phantom/dispatch/getThreadHistory.js +139 -0
  81. package/phantom/dispatch/getThreadInfo.js +153 -0
  82. package/phantom/dispatch/getThreadList.js +132 -0
  83. package/phantom/dispatch/getThreadPictures.js +93 -0
  84. package/phantom/dispatch/getUserID.js +147 -0
  85. package/phantom/dispatch/getUserInfo.js +513 -0
  86. package/phantom/dispatch/getUserInfoV2.js +146 -0
  87. package/phantom/dispatch/handleMessageRequest.js +50 -0
  88. package/phantom/dispatch/httpGet.js +63 -0
  89. package/phantom/dispatch/httpPost.js +89 -0
  90. package/phantom/dispatch/httpPostFormData.js +69 -0
  91. package/phantom/dispatch/listenMqtt.js +1236 -0
  92. package/phantom/dispatch/listenSpeed.js +179 -0
  93. package/phantom/dispatch/logout.js +93 -0
  94. package/phantom/dispatch/markAsDelivered.js +92 -0
  95. package/phantom/dispatch/markAsRead.js +119 -0
  96. package/phantom/dispatch/markAsReadAll.js +215 -0
  97. package/phantom/dispatch/markAsSeen.js +70 -0
  98. package/phantom/dispatch/mqttDeltaValue.js +278 -0
  99. package/phantom/dispatch/muteThread.js +253 -0
  100. package/phantom/dispatch/nickname.js +132 -0
  101. package/phantom/dispatch/notes.js +263 -0
  102. package/phantom/dispatch/pinMessage.js +238 -0
  103. package/phantom/dispatch/produceMetaTheme.js +335 -0
  104. package/phantom/dispatch/realtime.js +291 -0
  105. package/phantom/dispatch/removeUserFromGroup.js +248 -0
  106. package/phantom/dispatch/resolvePhotoUrl.js +217 -0
  107. package/phantom/dispatch/searchForThread.js +258 -0
  108. package/phantom/dispatch/sendMessage.js +354 -0
  109. package/phantom/dispatch/sendMessageMqtt.js +249 -0
  110. package/phantom/dispatch/sendTypingIndicator.js +206 -0
  111. package/phantom/dispatch/setMessageReaction.js +188 -0
  112. package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
  113. package/phantom/dispatch/setThreadTheme.js +330 -0
  114. package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
  115. package/phantom/dispatch/share.js +200 -0
  116. package/phantom/dispatch/shareContact.js +216 -0
  117. package/phantom/dispatch/stickers.js +395 -0
  118. package/phantom/dispatch/story.js +240 -0
  119. package/phantom/dispatch/theme.js +296 -0
  120. package/phantom/dispatch/unfriend.js +199 -0
  121. package/phantom/dispatch/unsendMessage.js +124 -0
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+
5
+ async function retryOp(fn, retries = 3, base = 700) {
6
+ for (let i = 0; i < retries; i++) {
7
+ try { return await fn(); } catch (err) {
8
+ if (i === retries - 1) throw err;
9
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
10
+ }
11
+ }
12
+ }
13
+
14
+ async function handleUpload(defaultFuncs, ctx, image) {
15
+ const form = { images_only: "true", "attachment[]": image };
16
+ return retryOp(() =>
17
+ defaultFuncs.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {})
18
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
19
+ .then(resData => {
20
+ if (resData.error) throw resData;
21
+ if (!resData.payload?.metadata?.[0]) throw new Error("changeGroupImage: empty upload payload");
22
+ return resData.payload.metadata[0];
23
+ })
24
+ );
25
+ }
26
+
27
+ async function mqttSetImage(ctx, threadID, imageID) {
28
+ return new Promise((resolve, reject) => {
29
+ const reqID = ++ctx.wsReqNumber;
30
+ const taskID = ++ctx.wsTaskNumber;
31
+
32
+ const form = JSON.stringify({
33
+ app_id: "2220391788200892",
34
+ payload: JSON.stringify({
35
+ epoch_id: utils.generateOfflineThreadingID(),
36
+ tasks: [{
37
+ failure_count: null,
38
+ label: "37",
39
+ payload: JSON.stringify({ thread_key: threadID, image_id: imageID, sync_group: 1 }),
40
+ queue_name: "thread_image",
41
+ task_id: taskID
42
+ }],
43
+ version_id: "8798795233522156"
44
+ }),
45
+ request_id: reqID,
46
+ type: 3
47
+ });
48
+
49
+ let handled = false;
50
+ const onResp = (topic, message) => {
51
+ if (topic !== "/ls_resp" || handled) return;
52
+ let j;
53
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
54
+ if (j.request_id !== reqID) return;
55
+ handled = true;
56
+ clearTimeout(timer);
57
+ ctx.mqttClient.removeListener("message", onResp);
58
+ resolve({ success: true, imageID, response: j.payload });
59
+ };
60
+
61
+ const timer = setTimeout(() => {
62
+ if (!handled) {
63
+ handled = true;
64
+ ctx.mqttClient.removeListener("message", onResp);
65
+ reject(new Error("MQTT timeout for changeGroupImage"));
66
+ }
67
+ }, 30000);
68
+
69
+ ctx.mqttClient.on("message", onResp);
70
+ ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
71
+ if (err && !handled) {
72
+ handled = true;
73
+ clearTimeout(timer);
74
+ ctx.mqttClient.removeListener("message", onResp);
75
+ reject(err);
76
+ }
77
+ });
78
+ });
79
+ }
80
+
81
+ async function httpSetImage(defaultFuncs, ctx, threadID, imageID) {
82
+ const form = {
83
+ fb_api_caller_class: "RelayModern",
84
+ fb_api_req_friendly_name: "MessengerChangeGroupImageMutation",
85
+ doc_id: "5768799839818799",
86
+ variables: JSON.stringify({
87
+ input: {
88
+ thread_fbid: threadID,
89
+ image_id: imageID,
90
+ actor_id: ctx.userID,
91
+ client_mutation_id: String(Math.round(Math.random() * 10000))
92
+ }
93
+ })
94
+ };
95
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
96
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
97
+ if (res.errors) throw new Error(JSON.stringify(res.errors));
98
+ return { success: true, imageID, method: "graphql" };
99
+ }
100
+
101
+ module.exports = (defaultFuncs, api, ctx) => {
102
+ return async function changeGroupImage(image, threadID, callback) {
103
+ let resolveFunc, rejectFunc;
104
+ const returnPromise = new Promise((resolve, reject) => {
105
+ resolveFunc = resolve;
106
+ rejectFunc = reject;
107
+ });
108
+
109
+ if (typeof callback !== "function") {
110
+ callback = (err, result) => {
111
+ if (err) return rejectFunc(err);
112
+ resolveFunc(result);
113
+ };
114
+ }
115
+
116
+ try {
117
+ if (!threadID || (typeof threadID !== "string" && typeof threadID !== "number"))
118
+ throw new Error("changeGroupImage: invalid threadID");
119
+ if (!utils.isReadableStream(image)) throw new Error("changeGroupImage: image must be a readable stream");
120
+
121
+ const payload = await handleUpload(defaultFuncs, ctx, image);
122
+ const imageID = payload.image_id;
123
+ if (!imageID) throw new Error("changeGroupImage: upload returned no image_id");
124
+
125
+ let result;
126
+ if (ctx.mqttClient) {
127
+ try {
128
+ result = await retryOp(() => mqttSetImage(ctx, String(threadID), imageID));
129
+ } catch (mqttErr) {
130
+ utils.warn("changeGroupImage", "MQTT failed, using HTTP:", mqttErr.message);
131
+ result = await retryOp(() => httpSetImage(defaultFuncs, ctx, String(threadID), imageID));
132
+ }
133
+ } else {
134
+ result = await retryOp(() => httpSetImage(defaultFuncs, ctx, String(threadID), imageID));
135
+ }
136
+
137
+ callback(null, result);
138
+ } catch (err) {
139
+ utils.error("changeGroupImage", err);
140
+ callback(err);
141
+ }
142
+
143
+ return returnPromise;
144
+ };
145
+ };
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { globalShield } = require('../datastore/models/matrix/ghost');
5
+
6
+ const COLOR_PALETTE = {
7
+ blue: "196241301102133",
8
+ purple: "370940413392601",
9
+ green: "169463077092846",
10
+ pink: "230032715012014",
11
+ orange: "175615189761153",
12
+ red: "2136751179887052",
13
+ yellow: "2058653964378557",
14
+ teal: "417639218648241",
15
+ black: "539927563794799",
16
+ white: "2873642392710980",
17
+ coral: "4286946214700532",
18
+ lime: "4286946214700540",
19
+ lavender: "4286946214700548",
20
+ aqua: "4286946214700556",
21
+ default: "196241301102133"
22
+ };
23
+
24
+ async function retryOp(fn, retries = 3, base = 500) {
25
+ for (let i = 0; i < retries; i++) {
26
+ try { return await fn(); } catch (err) {
27
+ if (i === retries - 1) throw err;
28
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
29
+ }
30
+ }
31
+ }
32
+
33
+ function resolveColor(color) {
34
+ if (!color) return COLOR_PALETTE.default;
35
+ const s = String(color).trim();
36
+ if (/^\d+$/.test(s)) return s;
37
+ return COLOR_PALETTE[s.toLowerCase()] || s;
38
+ }
39
+
40
+ async function mqttChangeColor(ctx, threadID, colorFBID) {
41
+ return new Promise((resolve, reject) => {
42
+ const reqID = ++ctx.wsReqNumber;
43
+ const taskID = ++ctx.wsTaskNumber;
44
+
45
+ const form = JSON.stringify({
46
+ app_id: "2220391788200892",
47
+ payload: JSON.stringify({
48
+ data_trace_id: null,
49
+ epoch_id: parseInt(utils.generateOfflineThreadingID()),
50
+ tasks: [{
51
+ failure_count: null,
52
+ label: "43",
53
+ payload: JSON.stringify({
54
+ thread_key: threadID,
55
+ theme_fbid: colorFBID,
56
+ source: null,
57
+ sync_group: 1,
58
+ payload: null
59
+ }),
60
+ queue_name: "thread_theme",
61
+ task_id: taskID
62
+ }],
63
+ version_id: "8798795233522156"
64
+ }),
65
+ request_id: reqID,
66
+ type: 3
67
+ });
68
+
69
+ let handled = false;
70
+ const onResp = (topic, message) => {
71
+ if (topic !== "/ls_resp" || handled) return;
72
+ let j;
73
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
74
+ if (j.request_id !== reqID) return;
75
+ handled = true;
76
+ clearTimeout(timer);
77
+ ctx.mqttClient.removeListener("message", onResp);
78
+ try {
79
+ const msgID = j.payload.step[1][2][2][1][2];
80
+ const msgReplace = j.payload.step[1][2][2][1][4];
81
+ resolve({ body: msgReplace, messageID: msgID, threadID, colorFBID, method: "mqtt" });
82
+ } catch {
83
+ resolve({ success: true, threadID, colorFBID, method: "mqtt" });
84
+ }
85
+ };
86
+
87
+ const timer = setTimeout(() => {
88
+ if (!handled) {
89
+ handled = true;
90
+ ctx.mqttClient.removeListener("message", onResp);
91
+ reject(new Error("MQTT timeout for changeThreadColor"));
92
+ }
93
+ }, 30000);
94
+
95
+ ctx.mqttClient.on("message", onResp);
96
+ ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
97
+ if (err && !handled) {
98
+ handled = true;
99
+ clearTimeout(timer);
100
+ ctx.mqttClient.removeListener("message", onResp);
101
+ reject(err);
102
+ }
103
+ });
104
+ });
105
+ }
106
+
107
+ async function httpChangeColor(defaultFuncs, ctx, threadID, colorFBID) {
108
+ const form = {
109
+ dpr: 1,
110
+ queries: JSON.stringify({
111
+ o0: {
112
+ doc_id: "1727493033983591",
113
+ query_params: {
114
+ data: {
115
+ actor_id: ctx.userID,
116
+ client_mutation_id: "0",
117
+ source: "SETTINGS",
118
+ theme_id: colorFBID,
119
+ thread_id: threadID
120
+ }
121
+ }
122
+ }
123
+ })
124
+ };
125
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
126
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
127
+ if (res?.[0]?.o0?.errors) throw new Error(JSON.stringify(res[0].o0.errors));
128
+ return { success: true, threadID, colorFBID, method: "http" };
129
+ }
130
+
131
+ module.exports = (defaultFuncs, api, ctx) => {
132
+ return async function changeThreadColor(color, threadID, callback) {
133
+ let resolveFunc, rejectFunc;
134
+ const returnPromise = new Promise((resolve, reject) => {
135
+ resolveFunc = resolve;
136
+ rejectFunc = reject;
137
+ });
138
+
139
+ if (typeof callback !== "function") {
140
+ callback = (err, data) => {
141
+ if (err) return rejectFunc(err);
142
+ resolveFunc(data);
143
+ };
144
+ }
145
+
146
+ try {
147
+ if (!threadID) throw new Error("changeThreadColor: threadID is required");
148
+ const colorFBID = resolveColor(color);
149
+
150
+ await globalShield.addSmartDelay();
151
+
152
+ let result;
153
+ if (ctx.mqttClient) {
154
+ try {
155
+ result = await retryOp(() => mqttChangeColor(ctx, threadID, colorFBID));
156
+ } catch (mqttErr) {
157
+ utils.warn("changeThreadColor", "MQTT failed, using HTTP:", mqttErr.message);
158
+ result = await retryOp(() => httpChangeColor(defaultFuncs, ctx, threadID, colorFBID));
159
+ }
160
+ } else {
161
+ result = await retryOp(() => httpChangeColor(defaultFuncs, ctx, threadID, colorFBID));
162
+ }
163
+
164
+ callback(null, result);
165
+ } catch (err) {
166
+ utils.error("changeThreadColor", err);
167
+ callback(err);
168
+ }
169
+
170
+ return returnPromise;
171
+ };
172
+ };
@@ -0,0 +1,130 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { globalShield } = require('../datastore/models/matrix/ghost');
5
+
6
+ async function retryOp(fn, retries = 3, base = 500) {
7
+ for (let i = 0; i < retries; i++) {
8
+ try { return await fn(); } catch (err) {
9
+ if (i === retries - 1) throw err;
10
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
11
+ }
12
+ }
13
+ }
14
+
15
+ async function mqttChangeEmoji(ctx, emoji, threadID) {
16
+ return new Promise((resolve, reject) => {
17
+ const reqID = ++ctx.wsReqNumber;
18
+ const taskID = ++ctx.wsTaskNumber;
19
+
20
+ const form = JSON.stringify({
21
+ app_id: "2220391788200892",
22
+ payload: JSON.stringify({
23
+ epoch_id: utils.generateOfflineThreadingID(),
24
+ tasks: [{
25
+ failure_count: null,
26
+ label: "127",
27
+ payload: JSON.stringify({ thread_key: threadID, custom_emoji: emoji, sync_group: 1 }),
28
+ queue_name: "thread_emoji",
29
+ task_id: taskID
30
+ }],
31
+ version_id: "8798795233522156"
32
+ }),
33
+ request_id: reqID,
34
+ type: 3
35
+ });
36
+
37
+ let handled = false;
38
+ const onResp = (topic, message) => {
39
+ if (topic !== "/ls_resp" || handled) return;
40
+ let j;
41
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
42
+ if (j.request_id !== reqID) return;
43
+ handled = true;
44
+ clearTimeout(timer);
45
+ ctx.mqttClient.removeListener("message", onResp);
46
+ resolve({ success: true, emoji, threadID, method: "mqtt" });
47
+ };
48
+
49
+ const timer = setTimeout(() => {
50
+ if (!handled) {
51
+ handled = true;
52
+ ctx.mqttClient.removeListener("message", onResp);
53
+ reject(new Error("MQTT timeout for changeThreadEmoji"));
54
+ }
55
+ }, 20000);
56
+
57
+ ctx.mqttClient.on("message", onResp);
58
+ ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
59
+ if (err && !handled) {
60
+ handled = true;
61
+ clearTimeout(timer);
62
+ ctx.mqttClient.removeListener("message", onResp);
63
+ reject(err);
64
+ }
65
+ });
66
+ });
67
+ }
68
+
69
+ async function httpChangeEmoji(defaultFuncs, ctx, emoji, threadID) {
70
+ const form = {
71
+ emoji_choice: emoji,
72
+ thread_or_other_fbid: threadID
73
+ };
74
+
75
+ const resData = await defaultFuncs.post(
76
+ "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
77
+ ctx.jar,
78
+ form
79
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
80
+
81
+ if (resData.error === 1357031) {
82
+ throw { error: "Thread has no messages yet. Send at least one message before changing emoji." };
83
+ }
84
+ if (resData.error) throw resData;
85
+
86
+ return { success: true, emoji, threadID, method: "http" };
87
+ }
88
+
89
+ module.exports = (defaultFuncs, api, ctx) => {
90
+ return async function changeThreadEmoji(emoji, threadID, callback) {
91
+ let resolveFunc, rejectFunc;
92
+ const returnPromise = new Promise((resolve, reject) => {
93
+ resolveFunc = resolve;
94
+ rejectFunc = reject;
95
+ });
96
+
97
+ if (typeof callback !== "function") {
98
+ callback = (err, data) => {
99
+ if (err) return rejectFunc(err);
100
+ resolveFunc(data);
101
+ };
102
+ }
103
+
104
+ try {
105
+ if (!emoji || typeof emoji !== "string") throw new Error("changeThreadEmoji: emoji must be a non-empty string");
106
+ if (!threadID) throw new Error("changeThreadEmoji: threadID is required");
107
+
108
+ await globalShield.addSmartDelay();
109
+
110
+ let result;
111
+ if (ctx.mqttClient) {
112
+ try {
113
+ result = await retryOp(() => mqttChangeEmoji(ctx, emoji, threadID));
114
+ } catch (mqttErr) {
115
+ utils.warn("changeThreadEmoji", "MQTT failed, using HTTP:", mqttErr.message);
116
+ result = await retryOp(() => httpChangeEmoji(defaultFuncs, ctx, emoji, threadID));
117
+ }
118
+ } else {
119
+ result = await retryOp(() => httpChangeEmoji(defaultFuncs, ctx, emoji, threadID));
120
+ }
121
+
122
+ callback(null, result);
123
+ } catch (err) {
124
+ utils.error("changeThreadEmoji", err);
125
+ callback(err);
126
+ }
127
+
128
+ return returnPromise;
129
+ };
130
+ };
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+
5
+ async function retryOp(fn, retries = 3, base = 600) {
6
+ for (let i = 0; i < retries; i++) {
7
+ try { return await fn(); } catch (err) {
8
+ if (i === retries - 1) throw err;
9
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
10
+ }
11
+ }
12
+ }
13
+
14
+ async function handleUpload(defaultFuncs, ctx, msg, form) {
15
+ if (!msg.attachments || !msg.attachments.length) return;
16
+
17
+ const uploads = msg.attachments.map(item => {
18
+ if (!utils.isReadableStream(item)) throw new Error("comment: attachments must be readable streams");
19
+ return defaultFuncs.postFormData("https://www.facebook.com/ajax/ufi/upload/", ctx.jar, {
20
+ profile_id: ctx.userID,
21
+ source: 19,
22
+ target_id: ctx.userID,
23
+ file: item
24
+ }).then(utils.parseAndCheckLogin(ctx, defaultFuncs)).then(res => {
25
+ if (res.error || !res.payload?.fbid) throw res;
26
+ return { media: { id: res.payload.fbid } };
27
+ });
28
+ });
29
+
30
+ const results = await Promise.all(uploads);
31
+ form.input.attachments.push(...results);
32
+ }
33
+
34
+ function handleURL(msg, form) {
35
+ if (typeof msg.url === "string") {
36
+ form.input.attachments.push({ link: { external: { url: msg.url } } });
37
+ }
38
+ }
39
+
40
+ function handleMentions(msg, form) {
41
+ if (!msg.mentions) return;
42
+ for (const { tag, id, fromIndex } of msg.mentions) {
43
+ if (typeof tag !== "string" || !id) continue;
44
+ const offset = msg.body.indexOf(tag, fromIndex || 0);
45
+ if (offset < 0) continue;
46
+ form.input.message.ranges.push({ entity: { id }, length: tag.length, offset });
47
+ }
48
+ }
49
+
50
+ function handleSticker(msg, form) {
51
+ if (msg.sticker) form.input.attachments.push({ media: { id: String(msg.sticker) } });
52
+ }
53
+
54
+ async function submitComment(defaultFuncs, ctx, form) {
55
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, {
56
+ fb_api_caller_class: "RelayModern",
57
+ fb_api_req_friendly_name: "useCometUFICreateCommentMutation",
58
+ variables: JSON.stringify(form),
59
+ server_timestamps: true,
60
+ doc_id: 6993516810709754
61
+ }).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
62
+
63
+ if (res.errors) throw res;
64
+
65
+ const edge = res.data?.comment_create?.feedback_comment_edge;
66
+ if (!edge) throw new Error("comment: no comment edge returned");
67
+
68
+ return {
69
+ id: edge.node.id,
70
+ url: edge.node.feedback?.url,
71
+ count: res.data.comment_create.feedback?.total_comment_count,
72
+ text: edge.node.body?.text || "",
73
+ timestamp: Date.now()
74
+ };
75
+ }
76
+
77
+ module.exports = (defaultFuncs, api, ctx) => {
78
+ return async function createCommentPost(msg, postID, replyCommentID, callback) {
79
+ let resolveFunc, rejectFunc;
80
+ const returnPromise = new Promise((resolve, reject) => {
81
+ resolveFunc = resolve;
82
+ rejectFunc = reject;
83
+ });
84
+
85
+ if (typeof replyCommentID === "function") { callback = replyCommentID; replyCommentID = null; }
86
+ if (typeof callback !== "function") {
87
+ callback = (err, data) => {
88
+ if (err) return rejectFunc(err);
89
+ resolveFunc(data);
90
+ };
91
+ }
92
+
93
+ try {
94
+ if (typeof msg !== "string" && typeof msg !== "object") throw new Error("comment: msg must be string or object");
95
+ if (!postID || typeof postID !== "string") throw new Error("comment: postID must be a string");
96
+
97
+ const msgObj = typeof msg === "string" ? { body: msg } : { ...msg };
98
+ msgObj.mentions = msgObj.mentions || [];
99
+ msgObj.attachments = msgObj.attachments || [];
100
+
101
+ const form = {
102
+ feedLocation: "NEWSFEED",
103
+ feedbackSource: 1,
104
+ groupID: null,
105
+ input: {
106
+ client_mutation_id: String(Math.round(Math.random() * 19)),
107
+ actor_id: ctx.userID,
108
+ attachments: [],
109
+ feedback_id: Buffer.from("feedback:" + postID).toString("base64"),
110
+ message: { ranges: [], text: msgObj.body || "" },
111
+ reply_comment_parent_fbid: replyCommentID || null,
112
+ is_tracking_encrypted: true,
113
+ tracking: [],
114
+ feedback_source: "NEWS_FEED",
115
+ idempotence_token: "client:" + utils.getGUID(),
116
+ session_id: utils.getGUID()
117
+ },
118
+ scale: 1,
119
+ useDefaultActor: false
120
+ };
121
+
122
+ await handleUpload(defaultFuncs, ctx, msgObj, form);
123
+ handleURL(msgObj, form);
124
+ handleMentions(msgObj, form);
125
+ handleSticker(msgObj, form);
126
+
127
+ const result = await retryOp(() => submitComment(defaultFuncs, ctx, form));
128
+ callback(null, result);
129
+ } catch (err) {
130
+ utils.error("comment", err);
131
+ callback(err);
132
+ }
133
+
134
+ return returnPromise;
135
+ };
136
+ };