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,70 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+
5
+ /**
6
+ * @param {Object} defaultFuncs
7
+ * @param {Object} api
8
+ * @param {Object} ctx
9
+ */
10
+ module.exports = function (defaultFuncs, api, ctx) {
11
+ /**
12
+ * Marks all messages as "seen" up to a specific timestamp.
13
+ * @param {number} [seen_timestamp=Date.now()] - The timestamp (in milliseconds) up to which messages should be marked as seen. If a function is provided, it's treated as the callback and the timestamp defaults to the current time.
14
+ * @param {Function} [callback] - The callback function.
15
+ * @returns {Promise<void>} A Promise that resolves on success or rejects with an error.
16
+ */
17
+ return async function markAsSeen(seen_timestamp, callback) {
18
+ let resolveFunc = function () {};
19
+ let rejectFunc = function () {};
20
+ const returnPromise = new Promise(function (resolve, reject) {
21
+ resolveFunc = resolve;
22
+ rejectFunc = reject;
23
+ });
24
+
25
+ if (utils.getType(seen_timestamp) == "Function" || utils.getType(seen_timestamp) == "AsyncFunction") {
26
+ callback = seen_timestamp;
27
+ seen_timestamp = Date.now();
28
+ } else if (seen_timestamp === undefined) {
29
+ seen_timestamp = Date.now();
30
+ }
31
+
32
+ if (!callback) {
33
+ callback = function (err, friendList) {
34
+ if (err) {
35
+ return rejectFunc(err);
36
+ }
37
+ resolveFunc(friendList);
38
+ };
39
+ }
40
+
41
+ const form = {
42
+ seen_timestamp: seen_timestamp,
43
+ };
44
+
45
+ try {
46
+ const resData = await defaultFuncs
47
+ .post(
48
+ "https://www.facebook.com/ajax/mercury/mark_seen.php",
49
+ ctx.jar,
50
+ form,
51
+ )
52
+ .then(utils.saveCookies(ctx.jar))
53
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
54
+
55
+ if (resData.error) {
56
+ throw resData;
57
+ }
58
+
59
+ return callback();
60
+ } catch (err) {
61
+ utils.error("markAsSeen", err);
62
+ if (utils.getType(err) == "Object" && err.error === "Not logged in.") {
63
+ ctx.loggedIn = false;
64
+ }
65
+ return callback(err);
66
+ }
67
+
68
+ return returnPromise;
69
+ };
70
+ };
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const e2ee = require('../datastore/models/cipher/e2ee');
5
+
6
+
7
+ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
8
+ if (v.delta.class == "NewMessage") {
9
+
10
+ if (ctx.globalOptions.pageID && ctx.globalOptions.pageID != v.queue) return;
11
+ (function resolveAttachmentUrl(i) {
12
+ if (!v.delta.attachments || i == v.delta.attachments.length || utils.getType(v.delta.attachments) !== "Array") {
13
+ var fmtMsg;
14
+ try {
15
+ fmtMsg = utils.formatDeltaMessage(v);
16
+ } catch (err) {
17
+ return;
18
+ }
19
+ if (fmtMsg) {
20
+ try {
21
+ if (e2ee.isEnabled(ctx) && !!v.delta.threadKey.threadFbId) {
22
+ const dec = e2ee.decrypt(ctx, fmtMsg.threadID, fmtMsg.body);
23
+ if (dec !== null) fmtMsg.body = dec;
24
+ }
25
+ } catch (_) {}
26
+ if (ctx.globalOptions.autoMarkDelivery) {
27
+ api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID);
28
+ }
29
+ if (!ctx.globalOptions.selfListen && fmtMsg.senderID === ctx.userID) {
30
+ return;
31
+ }
32
+ return globalCallback(null, fmtMsg);
33
+ }
34
+ } else {
35
+ var attachment = v.delta.attachments[i];
36
+ if (attachment && attachment.mercury && attachment.mercury.attach_type == "photo") {
37
+ api.resolvePhotoUrl(attachment.fbid, (err, url) => {
38
+ if (!err) attachment.mercury.metadata.url = url;
39
+ return resolveAttachmentUrl(i + 1);
40
+ });
41
+ } else {
42
+ return resolveAttachmentUrl(i + 1);
43
+ }
44
+ }
45
+ })(0);
46
+ }
47
+
48
+ if (v.delta.class == "ClientPayload") {
49
+ var clientPayload = utils.decodeClientPayload(v.delta.payload);
50
+ if (clientPayload && clientPayload.deltas) {
51
+ for (var i in clientPayload.deltas) {
52
+ var delta = clientPayload.deltas[i];
53
+
54
+ if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
55
+ const reactionEvent = {
56
+ type: "message_reaction",
57
+ threadID: (delta.deltaMessageReaction.threadKey.threadFbId || delta.deltaMessageReaction.threadKey.otherUserFbId).toString(),
58
+ messageID: delta.deltaMessageReaction.messageId,
59
+ reaction: delta.deltaMessageReaction.reaction,
60
+ senderID: delta.deltaMessageReaction.userId.toString(),
61
+ userID: delta.deltaMessageReaction.userId.toString()
62
+ };
63
+
64
+ if (!ctx.globalOptions.selfListen && reactionEvent.senderID === ctx.userID) {
65
+ return;
66
+ }
67
+
68
+ globalCallback(null, reactionEvent);
69
+ } else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
70
+ globalCallback(null, {
71
+ type: "message_unsend",
72
+ threadID: (delta.deltaRecallMessageData.threadKey.threadFbId || delta.deltaRecallMessageData.threadKey.otherUserFbId).toString(),
73
+ messageID: delta.deltaRecallMessageData.messageID,
74
+ senderID: delta.deltaRecallMessageData.senderID.toString(),
75
+ deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
76
+ timestamp: delta.deltaRecallMessageData.timestamp
77
+ });
78
+ } else if (delta.deltaMessageReply) {
79
+ var mdata = delta.deltaMessageReply.message?.data?.prng ? JSON.parse(delta.deltaMessageReply.message.data.prng) : [];
80
+ var mentions = {};
81
+ if (mdata) {
82
+ mdata.forEach(m => mentions[m.i] = (delta.deltaMessageReply.message.body || "").substring(m.o, m.o + m.l));
83
+ }
84
+
85
+ var callbackToReturn = {
86
+ type: "message_reply",
87
+ threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId || delta.deltaMessageReply.message.messageMetadata.threadKey.otherUserFbId).toString(),
88
+ messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
89
+ senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
90
+ attachments: delta.deltaMessageReply.message.attachments.map(att => {
91
+ try {
92
+ var mercury = JSON.parse(att.mercuryJSON);
93
+ Object.assign(att, mercury);
94
+ return utils.formatAttachment(att);
95
+ } catch (ex) {
96
+ return { ...att, error: ex, type: "unknown" };
97
+ }
98
+ }),
99
+ body: (delta.deltaMessageReply.message.body || ""),
100
+ isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
101
+ mentions: mentions,
102
+ timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
103
+ participantIDs: (delta.deltaMessageReply.message.participants || []).map(e => e.toString())
104
+ };
105
+ try {
106
+ if (e2ee.isEnabled(ctx) && callbackToReturn.isGroup) {
107
+ const dec = e2ee.decrypt(ctx, callbackToReturn.threadID, callbackToReturn.body);
108
+ if (dec !== null) callbackToReturn.body = dec;
109
+ }
110
+ } catch (_) {}
111
+
112
+ if (delta.deltaMessageReply.repliedToMessage) {
113
+ var rmentions = {};
114
+ var rmdata = delta.deltaMessageReply.repliedToMessage?.data?.prng ? JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng) : [];
115
+ if (rmdata) {
116
+ rmdata.forEach(m => rmentions[m.i] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(m.o, m.o + m.l));
117
+ }
118
+
119
+ callbackToReturn.messageReply = {
120
+ threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId || delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.otherUserFbId).toString(),
121
+ messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
122
+ senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
123
+ attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(att => {
124
+ try {
125
+ var mercury = JSON.parse(att.mercuryJSON);
126
+ Object.assign(att, mercury);
127
+ return utils.formatAttachment(att);
128
+ } catch (ex) {
129
+ return { ...att, error: ex, type: "unknown" };
130
+ }
131
+ }),
132
+ body: delta.deltaMessageReply.repliedToMessage.body || "",
133
+ isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
134
+ mentions: rmentions,
135
+ timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp,
136
+ participantIDs: (delta.deltaMessageReply.repliedToMessage.participants || []).map(e => e.toString())
137
+ };
138
+ try {
139
+ if (e2ee.isEnabled(ctx) && callbackToReturn.messageReply.isGroup) {
140
+ const dec2 = e2ee.decrypt(ctx, callbackToReturn.messageReply.threadID, callbackToReturn.messageReply.body);
141
+ if (dec2 !== null) callbackToReturn.messageReply.body = dec2;
142
+ }
143
+ } catch (_) {}
144
+ }
145
+ if (ctx.globalOptions.autoMarkDelivery) api.markAsDelivered(callbackToReturn.threadID, callbackToReturn.messageID);
146
+ if (!ctx.globalOptions.selfListen && callbackToReturn.senderID === ctx.userID) return;
147
+ return globalCallback(null, callbackToReturn);
148
+ }
149
+ }
150
+ return;
151
+ }
152
+ }
153
+
154
+ if (v.delta.class !== "NewMessage" && !ctx.globalOptions.listenEvents) return;
155
+ switch (v.delta.class) {
156
+ case "ReadReceipt":
157
+ var fmtMsg;
158
+ try {
159
+ fmtMsg = utils.formatDeltaReadReceipt(v.delta);
160
+ } catch (err) {
161
+ return;
162
+ }
163
+ if (fmtMsg) globalCallback(null, fmtMsg);
164
+ break;
165
+ case "AdminTextMessage":
166
+ switch (v.delta.type) {
167
+ case "instant_game_dynamic_custom_update":
168
+ case "accept_pending_thread":
169
+ case "confirm_friend_request":
170
+ case "shared_album_delete":
171
+ case "shared_album_addition":
172
+ case "pin_messages_v2":
173
+ case "unpin_messages_v2":
174
+ case "change_thread_theme":
175
+ case "change_thread_nickname":
176
+ case "change_thread_icon":
177
+ case "change_thread_quick_reaction":
178
+ case "change_thread_admins":
179
+ case "group_poll":
180
+ case "joinable_group_link_mode_change":
181
+ case "magic_words":
182
+ case "change_thread_approval_mode":
183
+ case "messenger_call_log":
184
+ case "participant_joined_group_call":
185
+ case "rtc_call_log":
186
+ case "update_vote":
187
+ var fmtEvent;
188
+ try {
189
+ fmtEvent = utils.formatDeltaEvent(v.delta);
190
+ } catch (err) {
191
+ return;
192
+ }
193
+ if (fmtEvent) globalCallback(null, fmtEvent);
194
+ break;
195
+ }
196
+ break;
197
+ case "ThreadName":
198
+ case "ParticipantsAddedToGroupThread":
199
+ case "ParticipantLeftGroupThread":
200
+ var fmtEvent2;
201
+ try {
202
+ fmtEvent2 = utils.formatDeltaEvent(v.delta);
203
+ } catch (err) {
204
+ return;
205
+ }
206
+ if (!ctx.globalOptions.selfListen && fmtEvent2 && fmtEvent2.author && fmtEvent2.author.toString() === ctx.userID) return;
207
+ if (!ctx.loggedIn) return;
208
+ if (fmtEvent2) globalCallback(null, fmtEvent2);
209
+ break;
210
+ case "ForcedFetch":
211
+ if (!v.delta.threadKey) return;
212
+ var mid = v.delta.messageId;
213
+ var tid = v.delta.threadKey.threadFbId;
214
+ if (mid && tid) {
215
+ var form = {
216
+ av: ctx.globalOptions.pageID,
217
+ queries: JSON.stringify({
218
+ o0: {
219
+ doc_id: "2848441488556444",
220
+ query_params: {
221
+ thread_and_message_id: {
222
+ thread_id: tid.toString(),
223
+ message_id: mid
224
+ }
225
+ }
226
+ }
227
+ })
228
+ };
229
+ defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
230
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
231
+ .then(resData => {
232
+ if (resData[resData.length - 1].error_results > 0) throw resData[0].o0.errors;
233
+ if (resData[resData.length - 1].successful_results === 0) throw { error: "forcedFetch: no successful_results" };
234
+ var fetchData = resData[0].o0.data.message;
235
+ if (utils.getType(fetchData) === "Object") {
236
+ if (fetchData.__typename === "UserMessage" && fetchData.extensible_attachment) {
237
+ var event = {
238
+ type: "message",
239
+ senderID: utils.formatID(fetchData.message_sender.id),
240
+ body: fetchData.message.text || "",
241
+ threadID: utils.formatID(tid.toString()),
242
+ messageID: fetchData.message_id,
243
+ attachments: [{
244
+ type: "share",
245
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
246
+ url: fetchData.extensible_attachment.story_attachment.url,
247
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
248
+ description: fetchData.extensible_attachment.story_attachment.description ? fetchData.extensible_attachment.story_attachment.description.text : "",
249
+ source: fetchData.extensible_attachment.story_attachment.source,
250
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
251
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
252
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
253
+ playable: ((fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false),
254
+ duration: ((fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0)
255
+ }],
256
+ mentions: {},
257
+ timestamp: parseInt(fetchData.timestamp_precise),
258
+ isGroup: fetchData.message_sender.id !== tid.toString()
259
+ };
260
+ try {
261
+ if (e2ee.isEnabled(ctx) && event.isGroup) {
262
+ const dec = e2ee.decrypt(ctx, event.threadID, event.body);
263
+ if (dec !== null) event.body = dec;
264
+ }
265
+ } catch (_) {}
266
+ globalCallback(null, event);
267
+ }
268
+ }
269
+ })
270
+ .catch(err => {});
271
+ }
272
+ break;
273
+ }
274
+ }
275
+
276
+ module.exports = {
277
+ parseDelta
278
+ };
@@ -0,0 +1,253 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { globalShield } = require('../datastore/models/matrix/ghost');
5
+
6
+ const MUTE_CACHE = new Map();
7
+ const MUTE_HISTORY = [];
8
+ const MAX_HISTORY = 200;
9
+ const SCHEDULED_UNMUTES = new Map();
10
+
11
+ const MUTE_PRESETS = {
12
+ off: 0,
13
+ fifteenMinutes: 15 * 60,
14
+ oneHour: 60 * 60,
15
+ eightHours: 8 * 60 * 60,
16
+ twentyFourHours: 24 * 60 * 60,
17
+ oneWeek: 7 * 24 * 60 * 60,
18
+ forever: -1
19
+ };
20
+
21
+ async function retryOp(fn, retries = 4, base = 500) {
22
+ for (let i = 0; i < retries; i++) {
23
+ try { return await fn(); } catch (err) {
24
+ if (i === retries - 1) throw err;
25
+ const isTransient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
26
+ if (!isTransient) throw err;
27
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 250));
28
+ }
29
+ }
30
+ }
31
+
32
+ async function httpMuteThread(defaultFuncs, ctx, threadID, muteSeconds) {
33
+ const res = await defaultFuncs.post(
34
+ 'https://www.facebook.com/ajax/mercury/change_mute_thread.php',
35
+ ctx.jar,
36
+ { thread_fbid: String(threadID), mute_settings: muteSeconds }
37
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
38
+ if (res && res.error) throw res;
39
+ return { success: true, threadID: String(threadID), muteSeconds, method: 'http', timestamp: Date.now() };
40
+ }
41
+
42
+ async function mqttMuteThread(ctx, threadID, muteSeconds) {
43
+ if (!ctx.mqttClient) throw new Error('MQTT not connected');
44
+ return new Promise((resolve, reject) => {
45
+ const reqID = ++ctx.wsReqNumber;
46
+ const taskID = ++ctx.wsTaskNumber;
47
+
48
+ const isMuting = muteSeconds !== 0;
49
+ const label = isMuting ? '319' : '320';
50
+
51
+ const form = JSON.stringify({
52
+ app_id: '2220391788200892',
53
+ payload: JSON.stringify({
54
+ epoch_id: utils.generateOfflineThreadingID(),
55
+ tasks: [{
56
+ failure_count: null,
57
+ label,
58
+ payload: JSON.stringify({
59
+ thread_key: String(threadID),
60
+ mute_expire_time_ms: muteSeconds === -1 ? -1 : muteSeconds > 0 ? Date.now() + muteSeconds * 1000 : 0,
61
+ sync_group: 1
62
+ }),
63
+ queue_name: 'mute_thread',
64
+ task_id: taskID
65
+ }],
66
+ version_id: '8798795233522156'
67
+ }),
68
+ request_id: reqID,
69
+ type: 3
70
+ });
71
+
72
+ let handled = false;
73
+ const onResp = (topic, message) => {
74
+ if (topic !== '/ls_resp' || handled) return;
75
+ let j;
76
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
77
+ if (j.request_id !== reqID) return;
78
+ handled = true;
79
+ clearTimeout(timer);
80
+ ctx.mqttClient.removeListener('message', onResp);
81
+ resolve({ success: true, threadID: String(threadID), muteSeconds, method: 'mqtt', timestamp: Date.now() });
82
+ };
83
+
84
+ const timer = setTimeout(() => {
85
+ if (!handled) {
86
+ handled = true;
87
+ ctx.mqttClient.removeListener('message', onResp);
88
+ reject(new Error('MQTT timeout for muteThread'));
89
+ }
90
+ }, 20000);
91
+
92
+ ctx.mqttClient.on('message', onResp);
93
+ ctx.mqttClient.publish('/ls_req', form, { qos: 1, retain: false }, (err) => {
94
+ if (err && !handled) {
95
+ handled = true;
96
+ clearTimeout(timer);
97
+ ctx.mqttClient.removeListener('message', onResp);
98
+ reject(err);
99
+ }
100
+ });
101
+ });
102
+ }
103
+
104
+ async function graphqlMuteThread(defaultFuncs, ctx, threadID, muteSeconds) {
105
+ const form = {
106
+ fb_api_caller_class: 'RelayModern',
107
+ fb_api_req_friendly_name: 'MessengerMuteThreadMutation',
108
+ doc_id: '3876984735634224',
109
+ variables: JSON.stringify({
110
+ input: {
111
+ thread_fbid: String(threadID),
112
+ mute_duration_seconds: muteSeconds,
113
+ actor_id: ctx.userID,
114
+ client_mutation_id: String(Math.round(Math.random() * 10000))
115
+ }
116
+ })
117
+ };
118
+ const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
119
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
120
+ if (res?.errors) throw new Error(JSON.stringify(res.errors));
121
+ return { success: true, threadID: String(threadID), muteSeconds, method: 'graphql', timestamp: Date.now() };
122
+ }
123
+
124
+ module.exports = (defaultFuncs, api, ctx) => {
125
+
126
+ function scheduleAutoUnmute(threadID, muteSeconds) {
127
+ const tid = String(threadID);
128
+ if (SCHEDULED_UNMUTES.has(tid)) clearTimeout(SCHEDULED_UNMUTES.get(tid));
129
+ if (muteSeconds > 0) {
130
+ const timer = setTimeout(() => {
131
+ MUTE_CACHE.delete(`mute_${tid}`);
132
+ SCHEDULED_UNMUTES.delete(tid);
133
+ utils.log('muteThread', `Auto-cleared mute cache for thread ${tid} after ${muteSeconds}s`);
134
+ }, muteSeconds * 1000 + 1000);
135
+ SCHEDULED_UNMUTES.set(tid, timer);
136
+ }
137
+ }
138
+
139
+ function recordHistory(op) {
140
+ MUTE_HISTORY.unshift({ ...op, ts: Date.now() });
141
+ if (MUTE_HISTORY.length > MAX_HISTORY) MUTE_HISTORY.length = MAX_HISTORY;
142
+ }
143
+
144
+ function resolveMuteSeconds(muteSeconds) {
145
+ if (typeof muteSeconds === 'string') {
146
+ const preset = MUTE_PRESETS[muteSeconds];
147
+ if (preset !== undefined) return preset;
148
+ const parsed = parseInt(muteSeconds, 10);
149
+ if (!isNaN(parsed)) return parsed;
150
+ throw new Error(`muteThread: unknown preset "${muteSeconds}". Use: ${Object.keys(MUTE_PRESETS).join(', ')}`);
151
+ }
152
+ if (typeof muteSeconds === 'number') return muteSeconds;
153
+ throw new Error(`muteThread: muteSeconds must be a number or preset string, got ${typeof muteSeconds}`);
154
+ }
155
+
156
+ const muteThread = async function muteThread(threadOrThreads, muteSeconds, options, callback) {
157
+ if (typeof options === 'function') { callback = options; options = {}; }
158
+ if (!options || typeof options !== 'object') options = {};
159
+ const { preferMqtt = true, useGraphQL = false, scheduleUnmute = true } = options;
160
+
161
+ let resolveFunc, rejectFunc;
162
+ const returnPromise = new Promise((resolve, reject) => {
163
+ resolveFunc = resolve;
164
+ rejectFunc = reject;
165
+ });
166
+
167
+ if (typeof callback !== 'function') {
168
+ callback = (err, result) => {
169
+ if (err) return rejectFunc(err);
170
+ resolveFunc(result);
171
+ };
172
+ }
173
+
174
+ try {
175
+ const threadIDs = Array.isArray(threadOrThreads)
176
+ ? threadOrThreads.map(String)
177
+ : [String(threadOrThreads)];
178
+
179
+ if (threadIDs.length === 0) throw new Error('muteThread: at least one threadID is required');
180
+
181
+ const resolvedMuteSeconds = resolveMuteSeconds(muteSeconds);
182
+
183
+ if (resolvedMuteSeconds < -1) throw new Error('muteThread: muteSeconds must be >= -1 (-1 = forever, 0 = unmute)');
184
+
185
+ await globalShield.addSmartDelay();
186
+
187
+ const results = [];
188
+ const errors = [];
189
+
190
+ for (const tid of threadIDs) {
191
+ const cacheKey = `mute_${tid}`;
192
+ const cached = MUTE_CACHE.get(cacheKey);
193
+ if (cached && cached.muteSeconds === resolvedMuteSeconds && (Date.now() - cached.ts < 5000)) {
194
+ results.push({ threadID: tid, ...cached.result, fromCache: true });
195
+ continue;
196
+ }
197
+
198
+ try {
199
+ let result;
200
+
201
+ if (useGraphQL) {
202
+ result = await retryOp(() => graphqlMuteThread(defaultFuncs, ctx, tid, resolvedMuteSeconds));
203
+ } else if (preferMqtt && ctx.mqttClient) {
204
+ try {
205
+ result = await retryOp(() => mqttMuteThread(ctx, tid, resolvedMuteSeconds));
206
+ } catch (mqttErr) {
207
+ utils.warn('muteThread', `MQTT failed for ${tid}, using HTTP:`, mqttErr.message);
208
+ result = await retryOp(() => httpMuteThread(defaultFuncs, ctx, tid, resolvedMuteSeconds));
209
+ }
210
+ } else {
211
+ result = await retryOp(() => httpMuteThread(defaultFuncs, ctx, tid, resolvedMuteSeconds));
212
+ }
213
+
214
+ MUTE_CACHE.set(cacheKey, { result, muteSeconds: resolvedMuteSeconds, ts: Date.now() });
215
+
216
+ if (scheduleUnmute && resolvedMuteSeconds > 0) scheduleAutoUnmute(tid, resolvedMuteSeconds);
217
+ if (resolvedMuteSeconds === 0) {
218
+ MUTE_CACHE.delete(cacheKey);
219
+ if (SCHEDULED_UNMUTES.has(tid)) { clearTimeout(SCHEDULED_UNMUTES.get(tid)); SCHEDULED_UNMUTES.delete(tid); }
220
+ }
221
+
222
+ results.push(result);
223
+ recordHistory({ threadID: tid, muteSeconds: resolvedMuteSeconds, method: result.method });
224
+ } catch (err) {
225
+ errors.push({ threadID: tid, error: err.message || String(err) });
226
+ }
227
+ }
228
+
229
+ const isSingleThread = threadIDs.length === 1;
230
+ const finalResult = isSingleThread && results.length === 1
231
+ ? results[0]
232
+ : { success: errors.length === 0, results, errors: errors.length ? errors : undefined, timestamp: Date.now() };
233
+
234
+ if (errors.length === threadIDs.length) {
235
+ return callback(new Error(`muteThread: all ${threadIDs.length} thread(s) failed — ${errors[0]?.error}`));
236
+ }
237
+
238
+ callback(null, finalResult);
239
+ } catch (err) {
240
+ utils.error('muteThread', err);
241
+ callback(err);
242
+ }
243
+
244
+ return returnPromise;
245
+ };
246
+
247
+ muteThread.presets = { ...MUTE_PRESETS };
248
+ muteThread.getHistory = (limit = 30) => MUTE_HISTORY.slice(0, limit);
249
+ muteThread.clearCache = () => { MUTE_CACHE.clear(); return { success: true }; };
250
+ muteThread.getPendingUnmutes = () => Array.from(SCHEDULED_UNMUTES.keys());
251
+
252
+ return muteThread;
253
+ };