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,179 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const mqtt = require('mqtt');
5
+ const websocket = require('websocket-stream');
6
+ const HttpsProxyAgent = require('https-proxy-agent');
7
+ const EventEmitter = require('events');
8
+
9
+ function connectLightspeed(ctx, globalCallback) {
10
+ let client;
11
+ let isStopped = false;
12
+ const guard = (label, fn) => (...args) => {
13
+ try {
14
+ return fn(...args);
15
+ } catch (err) {
16
+ utils.error(`[Lightspeed] ${label} handler error:`, err && err.message ? err.message : err);
17
+ }
18
+ };
19
+
20
+ function startConnection(retryCount = 0) {
21
+ if (isStopped) return;
22
+
23
+ const chatOn = ctx.globalOptions.online;
24
+ const foreground = false;
25
+ const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
26
+ const cookies = ctx.jar.getCookiesSync('https://www.facebook.com').join('; ');
27
+ const cid = ctx.clientID;
28
+
29
+ const username = {
30
+ u: ctx.userID,
31
+ s: sessionID,
32
+ chat_on: chatOn,
33
+ fg: foreground,
34
+ d: cid,
35
+ ct: 'websocket',
36
+ aid: '2220391788200892',
37
+ mqtt_sid: '',
38
+ cp: 3,
39
+ ecp: 10,
40
+ st: [],
41
+ pm: [],
42
+ dc: '',
43
+ no_auto_fg: true,
44
+ gas: null,
45
+ pack: [],
46
+ a: ctx.globalOptions.userAgent,
47
+ };
48
+
49
+ const queryParams = new URLSearchParams({
50
+ 'x-dgw-appid': '2220391788200892',
51
+ 'x-dgw-appversion': '0',
52
+ 'x-dgw-authtype': '1:0',
53
+ 'x-dgw-version': '5',
54
+ 'x-dgw-uuid': ctx.userID,
55
+ 'x-dgw-tier': 'prod',
56
+ 'x-dgw-loggingid': utils.getGUID(),
57
+ 'x-dgw-regionhint': ctx.region || 'PRN',
58
+ 'x-dgw-deviceid': ctx.clientID
59
+ });
60
+ const host = `wss://gateway.facebook.com/ws/lightspeed?${queryParams.toString()}`;
61
+
62
+ // Generate a unique clientId per session, just like a real browser would
63
+ const lsClientId = 'mqttwsclient_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
64
+ const options = {
65
+ clientId: lsClientId,
66
+ protocolId: 'MQIsdp',
67
+ protocolVersion: 3,
68
+ username: JSON.stringify(username),
69
+ clean: true,
70
+ wsOptions: {
71
+ headers: {
72
+ 'Cookie': cookies,
73
+ 'Origin': 'https://www.facebook.com',
74
+ 'User-Agent': username.a,
75
+ 'Referer': 'https://www.facebook.com/',
76
+ 'Host': new URL(host).hostname
77
+ }
78
+ },
79
+ keepalive: 60, // 60s is standard; 10s was too aggressive (sent PING every 10 seconds)
80
+ reconnectPeriod: 0
81
+ };
82
+
83
+ if (ctx.globalOptions.proxy) {
84
+ options.wsOptions.agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
85
+ }
86
+
87
+ try {
88
+ client = new mqtt.Client(_ => websocket(host, options.wsOptions), options);
89
+ utils.log("[Lightspeed] Attempting MQTT connection...");
90
+ } catch (err) {
91
+ utils.error("[Lightspeed] MQTT Client creation failed:", err.message);
92
+ reconnect(retryCount + 1);
93
+ return;
94
+ }
95
+
96
+ client.on('connect', guard("connect", () => {
97
+ utils.log("[Lightspeed] MQTT client connected. Attempting to subscribe to topics...");
98
+ retryCount = 0;
99
+
100
+
101
+ const topicsToSubscribe = [
102
+ "/t_ms", // Para sa mga messages at deltas
103
+ "/orca_presence", // Para sa online status
104
+ "/messaging_events" // Para sa ibang events
105
+ ];
106
+
107
+ topicsToSubscribe.forEach(topic => {
108
+ client.subscribe(topic, (err) => {
109
+ if (err) {
110
+ utils.error(`[Lightspeed] Failed to subscribe to topic ${topic}:`, err.message);
111
+ } else {
112
+ utils.log(`[Lightspeed] Subscribed to topic: ${topic}`);
113
+ }
114
+ });
115
+ });
116
+
117
+ }));
118
+
119
+ client.on('message', guard("message", (topic, payload) => {
120
+ utils.log(`[Lightspeed] Payload Received on topic ${topic}:`);
121
+ globalCallback(null, { type: 'lightspeed_message', topic: topic.toString(), payload: payload });
122
+ }));
123
+
124
+ client.on('close', guard("close", () => {
125
+ utils.warn(`[Lightspeed] Connection closed.`);
126
+ if (!isStopped) {
127
+ reconnect(retryCount + 1);
128
+ }
129
+ }));
130
+
131
+ client.on('error', guard("error", (err) => {
132
+ utils.error("[Lightspeed] MQTT Connection Error:", err.message);
133
+ }));
134
+ }
135
+
136
+ function reconnect(retryCount) {
137
+ const delay = Math.min(3000 * Math.pow(2, retryCount), 60000);
138
+ utils.log(`[Lightspeed] Reconnecting in ${delay / 1000} seconds...`);
139
+ setTimeout(() => startConnection(retryCount), delay);
140
+ }
141
+
142
+ startConnection();
143
+
144
+ return {
145
+ stop: () => {
146
+ isStopped = true;
147
+ if (client) client.end(true);
148
+ utils.log("[Lightspeed] Listener has been manually stopped.");
149
+ }
150
+ };
151
+ }
152
+
153
+ module.exports = function (defaultFuncs, api, ctx) {
154
+ return (callback) => {
155
+ class MessageEmitter extends EventEmitter {
156
+ constructor() {
157
+ super();
158
+ this.listener = null;
159
+ }
160
+ stop() {
161
+ if (this.listener) {
162
+ this.listener.stop();
163
+ }
164
+ this.emit('stop');
165
+ }
166
+ }
167
+ const msgEmitter = new MessageEmitter();
168
+ const globalCallback = (error, message) => {
169
+ if (error) return msgEmitter.emit("error", error);
170
+ msgEmitter.emit("message", message);
171
+ };
172
+ if (typeof callback === 'function') {
173
+ msgEmitter.listener = connectLightspeed(ctx, callback);
174
+ } else {
175
+ msgEmitter.listener = connectLightspeed(ctx, globalCallback);
176
+ }
177
+ return msgEmitter;
178
+ };
179
+ };
@@ -0,0 +1,93 @@
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
+ * @returns {function(): Promise<void>}
10
+ */
11
+ module.exports = function (defaultFuncs, api, ctx) {
12
+ /**
13
+ * Logs the current user out of Facebook.
14
+ * @returns {Promise<void>} A promise that resolves when logout is successful or rejects on error.
15
+ */
16
+ return async function logout() {
17
+ const form = {
18
+ pmid: "0",
19
+ };
20
+
21
+ try {
22
+ const resData = await defaultFuncs
23
+ .post(
24
+ "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
25
+ ctx.jar,
26
+ form,
27
+ )
28
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
29
+
30
+ const elem = resData.jsmods.instances[0][2][0].find(v => v.value === "logout");
31
+ if (!elem) {
32
+ throw { error: "Could not find logout form element." };
33
+ }
34
+
35
+ const html = resData.jsmods.markup.find(v => v[0] === elem.markup.__m)[1].__html;
36
+
37
+ const logoutForm = {
38
+ fb_dtsg: utils.getFrom(html, '"fb_dtsg" value="', '"'),
39
+ ref: utils.getFrom(html, '"ref" value="', '"'),
40
+ h: utils.getFrom(html, '"h" value="', '"'),
41
+ };
42
+
43
+ const logoutRes = await defaultFuncs
44
+ .post("https://www.facebook.com/logout.php", ctx.jar, logoutForm)
45
+ .then(utils.saveCookies(ctx.jar));
46
+
47
+ if (!logoutRes.headers || !logoutRes.headers.location) {
48
+ throw { error: "An error occurred when logging out." };
49
+ }
50
+
51
+ await defaultFuncs
52
+ .get(logoutRes.headers.location, ctx.jar)
53
+ .then(utils.saveCookies(ctx.jar));
54
+
55
+ ctx.loggedIn = false;
56
+
57
+ // Clear sensitive session tokens so stale credentials cannot be reused
58
+ // if this ctx object is accidentally referenced again after logout.
59
+ ctx.fb_dtsg = undefined;
60
+ ctx.fb_dtsg_ag = undefined;
61
+ ctx.lsd = undefined;
62
+ ctx.access_token = undefined;
63
+
64
+ // Stop background timers that are owned by this session, if present.
65
+ if (typeof ctx._stopCycle === 'function') {
66
+ try { ctx._stopCycle(); } catch (_) {}
67
+ }
68
+ if (typeof ctx._stopRevive === 'function') {
69
+ try { ctx._stopRevive(); } catch (_) {}
70
+ }
71
+ if (typeof ctx._stopCookieBackup === 'function') {
72
+ try { ctx._stopCookieBackup(); } catch (_) {}
73
+ }
74
+
75
+ // Stop auto backup
76
+ try {
77
+ const { stopAutoBackup } = require('../datastore/appStateBackup');
78
+ stopAutoBackup();
79
+ } catch (_) {}
80
+
81
+ // Invalidate the response cache so nothing stale is served after logout.
82
+ if (ctx.cache && typeof ctx.cache.clear === 'function') {
83
+ ctx.cache.clear();
84
+ }
85
+
86
+ utils.log("logout", "Logged out successfully.");
87
+
88
+ } catch (err) {
89
+ utils.error("logout", err);
90
+ throw err;
91
+ }
92
+ };
93
+ };
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * markAsDelivered — Advanced Delivery Receipt Handler for Phantom SDK
5
+ *
6
+ * Features:
7
+ * - Batch delivery receipt: mark multiple messages in one call
8
+ * - Exponential-backoff retry on transient failures
9
+ * - Full callback + Promise dual API
10
+ * - Validates threadID and messageID presence
11
+ */
12
+
13
+ const utils = require('../datastore/models/matrix/tools');
14
+
15
+ async function sendReceipt(defaultFuncs, ctx, threadID, messageID, retries, baseMs) {
16
+ const form = {};
17
+ form["message_ids[0]"] = messageID;
18
+ form[`thread_ids[${threadID}][0]`] = messageID;
19
+
20
+ let lastErr;
21
+ for (let attempt = 1; attempt <= retries; attempt++) {
22
+ try {
23
+ const resData = await defaultFuncs
24
+ .post("https://www.facebook.com/ajax/mercury/delivery_receipts.php", ctx.jar, form)
25
+ .then(utils.saveCookies ? utils.saveCookies(ctx.jar) : r => r)
26
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
27
+
28
+ if (resData?.error) throw new Error(JSON.stringify(resData.error));
29
+ return { success: true, threadID, messageID };
30
+ } catch (err) {
31
+ lastErr = err;
32
+ if (attempt >= retries || /auth|checkpoint|fatal/i.test(err.message)) throw err;
33
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
34
+ utils.warn("markAsDelivered", `Retry ${attempt}/${retries} for msg ${messageID} in ${Math.round(wait)}ms`);
35
+ await new Promise(r => setTimeout(r, wait));
36
+ }
37
+ }
38
+ throw lastErr;
39
+ }
40
+
41
+ module.exports = function (defaultFuncs, api, ctx) {
42
+
43
+ /**
44
+ * Mark one or more messages as delivered.
45
+ *
46
+ * @param {string} threadID Thread that contains the message(s)
47
+ * @param {string|string[]} messageID Single message ID or array for batch
48
+ * @param {object} [options] { retries, concurrency }
49
+ * @param {Function} [callback] Node-style (err, result) callback
50
+ * @returns {Promise<object|object[]>}
51
+ */
52
+ return async function markAsDelivered(threadID, messageID, options, callback) {
53
+ if (typeof options === 'function') { callback = options; options = {}; }
54
+ options = options || {};
55
+
56
+ const retries = options.retries || 3;
57
+ const concurrency = options.concurrency || 4;
58
+ const baseMs = 400;
59
+
60
+ let resolveFunc, rejectFunc;
61
+ const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
62
+
63
+ function done(err, data) {
64
+ if (callback) return err ? callback(err) : callback(null, data);
65
+ if (err) rejectFunc(err); else resolveFunc(data);
66
+ }
67
+
68
+ if (!threadID) { done(new Error("markAsDelivered: threadID is required")); return promise; }
69
+ if (!messageID) { done(new Error("markAsDelivered: messageID is required")); return promise; }
70
+
71
+ const ids = Array.isArray(messageID) ? messageID.filter(Boolean) : [messageID].filter(Boolean);
72
+
73
+ try {
74
+ const results = [];
75
+ for (let i = 0; i < ids.length; i += concurrency) {
76
+ const batch = ids.slice(i, i + concurrency);
77
+ const settled = await Promise.allSettled(batch.map(mid => sendReceipt(defaultFuncs, ctx, threadID, mid, retries, baseMs)));
78
+ for (const r of settled) {
79
+ if (r.status === 'fulfilled') results.push(r.value);
80
+ else results.push({ success: false, threadID, messageID: batch[results.length % batch.length], error: r.reason?.message });
81
+ }
82
+ }
83
+ const out = ids.length === 1 ? results[0] : { success: results.every(r => r.success), results };
84
+ done(null, out);
85
+ } catch (err) {
86
+ utils.error("markAsDelivered", err.message || err);
87
+ done(err);
88
+ }
89
+
90
+ return promise;
91
+ };
92
+ };
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * markAsRead — Advanced Read/Unread Marker for Phantom SDK
5
+ *
6
+ * Features:
7
+ * - MQTT path (primary) with configurable timeout (default 8s)
8
+ * - HTTP legacy fallback via change_read_status.php
9
+ * - GraphQL MQTT-based alternative for newer endpoints
10
+ * - Batch marking: pass array of threadIDs
11
+ * - Mark as unread (read=false) support
12
+ * - Full callback + Promise dual API
13
+ */
14
+
15
+ const utils = require('../datastore/models/matrix/tools');
16
+
17
+ async function markViaMqtt(ctx, threadID, read, timeoutMs) {
18
+ if (!ctx.mqttClient) throw new Error("markAsRead: MQTT client not connected");
19
+
20
+ return new Promise((resolve, reject) => {
21
+ const timer = setTimeout(() => reject(new Error(`markAsRead: MQTT publish timed out after ${timeoutMs}ms`)), timeoutMs);
22
+ ctx.mqttClient.publish(
23
+ "/mark_thread",
24
+ JSON.stringify({ threadID, mark: "read", state: read }),
25
+ { qos: 1, retain: false },
26
+ err => {
27
+ clearTimeout(timer);
28
+ if (err) reject(err);
29
+ else resolve({ success: true, threadID, read, method: 'mqtt' });
30
+ }
31
+ );
32
+ });
33
+ }
34
+
35
+ async function markViaHttp(defaultFuncs, ctx, threadID, read) {
36
+ const form = {
37
+ "source": "PagesManagerMessagesInterface",
38
+ "request_user_id": ctx.globalOptions?.pageID || ctx.userID,
39
+ [`ids[${threadID}]`]: read,
40
+ "watermarkTimestamp": Date.now(),
41
+ "shouldSendReadReceipt": true,
42
+ "commerce_last_message_type": "",
43
+ };
44
+
45
+ const resData = await defaultFuncs
46
+ .post("https://www.facebook.com/ajax/mercury/change_read_status.php", ctx.jar, form)
47
+ .then(utils.saveCookies ? utils.saveCookies(ctx.jar) : r => r)
48
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
49
+
50
+ if (resData?.error) throw new Error(JSON.stringify(resData.error));
51
+ return { success: true, threadID, read, method: 'http' };
52
+ }
53
+
54
+ async function markOne(defaultFuncs, ctx, threadID, read, timeoutMs) {
55
+ // Pages use HTTP; normal accounts prefer MQTT
56
+ if (ctx.globalOptions?.pageID) {
57
+ return markViaHttp(defaultFuncs, ctx, threadID, read);
58
+ }
59
+ try {
60
+ return await markViaMqtt(ctx, threadID, read, timeoutMs);
61
+ } catch (mqttErr) {
62
+ utils.warn("markAsRead", `MQTT failed for ${threadID} — HTTP fallback: ${mqttErr.message}`);
63
+ return markViaHttp(defaultFuncs, ctx, threadID, read);
64
+ }
65
+ }
66
+
67
+ module.exports = function (defaultFuncs, api, ctx) {
68
+
69
+ /**
70
+ * Mark one or more threads as read (or unread).
71
+ *
72
+ * @param {string|string[]} threadID Single thread ID or array
73
+ * @param {boolean} [read=true] true=read, false=unread
74
+ * @param {object} [options] { timeout, concurrency }
75
+ * @param {Function} [callback] Node-style (err, result) callback
76
+ * @returns {Promise<object|object[]>}
77
+ */
78
+ return async function markAsRead(threadID, read, options, callback) {
79
+ // Arg normalisation
80
+ if (typeof read === 'function') { callback = read; read = true; options = {}; }
81
+ if (typeof read === 'object' && !Array.isArray(read)) { options = read; read = true; }
82
+ if (typeof options === 'function') { callback = options; options = {}; }
83
+ if (read === undefined || read === null) read = true;
84
+ options = options || {};
85
+
86
+ const timeoutMs = options.timeout || 8000;
87
+ const concurrency = options.concurrency || 5;
88
+
89
+ let resolveFunc, rejectFunc;
90
+ const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
91
+
92
+ function done(err, data) {
93
+ if (callback) return err ? callback(err) : callback(null, data);
94
+ if (err) rejectFunc(err); else resolveFunc(data);
95
+ }
96
+
97
+ const ids = Array.isArray(threadID) ? threadID.filter(Boolean) : [threadID].filter(Boolean);
98
+ if (!ids.length) { done(new Error("markAsRead: threadID is required")); return promise; }
99
+
100
+ try {
101
+ const results = [];
102
+ for (let i = 0; i < ids.length; i += concurrency) {
103
+ const batch = ids.slice(i, i + concurrency);
104
+ const settled = await Promise.allSettled(batch.map(tid => markOne(defaultFuncs, ctx, tid, read, timeoutMs)));
105
+ for (const r of settled) {
106
+ if (r.status === 'fulfilled') results.push(r.value);
107
+ else results.push({ success: false, threadID: batch[results.length % batch.length], error: r.reason?.message });
108
+ }
109
+ }
110
+ const out = ids.length === 1 ? results[0] : { success: results.every(r => r.success), results };
111
+ done(null, out);
112
+ } catch (err) {
113
+ utils.error("markAsRead", err.message || err);
114
+ done(err);
115
+ }
116
+
117
+ return promise;
118
+ };
119
+ };
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { globalShield } = require('../datastore/models/matrix/ghost');
5
+
6
+ const MARK_ALL_CACHE = new Map();
7
+ const MARK_ALL_TTL = 30 * 1000;
8
+ const OPERATION_HISTORY = [];
9
+ const MAX_HISTORY = 100;
10
+
11
+ async function retryOp(fn, retries = 4, base = 500, label = 'markAsReadAll') {
12
+ for (let i = 0; i < retries; i++) {
13
+ try { return await fn(); } catch (err) {
14
+ const isTransient = /network|timeout|ECONNRESET|ETIMEDOUT|503|429/i.test(String(err?.message || err));
15
+ if (!isTransient || i === retries - 1) throw err;
16
+ const delay = base * Math.pow(2, i) + Math.random() * 300;
17
+ utils.warn('markAsReadAll', `Retry ${i + 1}/${retries} in ${Math.round(delay)}ms — ${err.message || err}`);
18
+ await new Promise(r => setTimeout(r, delay));
19
+ }
20
+ }
21
+ }
22
+
23
+ async function httpMarkAllRead(defaultFuncs, ctx, folder = 'inbox') {
24
+ const res = await defaultFuncs.post(
25
+ 'https://www.facebook.com/ajax/mercury/mark_folder_as_read.php',
26
+ ctx.jar,
27
+ { folder }
28
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
29
+ if (res && res.error) throw res;
30
+ return { success: true, folder, method: 'http', timestamp: Date.now() };
31
+ }
32
+
33
+ async function mqttMarkAllRead(ctx, folder = 'inbox') {
34
+ if (!ctx.mqttClient) throw new Error('MQTT not connected');
35
+ return new Promise((resolve, reject) => {
36
+ const reqID = ++ctx.wsReqNumber;
37
+ const taskID = ++ctx.wsTaskNumber;
38
+
39
+ const form = JSON.stringify({
40
+ app_id: '2220391788200892',
41
+ payload: JSON.stringify({
42
+ epoch_id: utils.generateOfflineThreadingID(),
43
+ tasks: [{
44
+ failure_count: null,
45
+ label: '21',
46
+ payload: JSON.stringify({
47
+ sync_group: 1,
48
+ folder: folder === 'inbox' ? 1 : 2,
49
+ mark_as_read: true
50
+ }),
51
+ queue_name: 'mark_folder_read',
52
+ task_id: taskID
53
+ }],
54
+ version_id: '8798795233522156'
55
+ }),
56
+ request_id: reqID,
57
+ type: 3
58
+ });
59
+
60
+ let handled = false;
61
+ const onResp = (topic, message) => {
62
+ if (topic !== '/ls_resp' || handled) return;
63
+ let j;
64
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
65
+ if (j.request_id !== reqID) return;
66
+ handled = true;
67
+ clearTimeout(timer);
68
+ ctx.mqttClient.removeListener('message', onResp);
69
+ resolve({ success: true, folder, method: 'mqtt', timestamp: Date.now() });
70
+ };
71
+
72
+ const timer = setTimeout(() => {
73
+ if (!handled) {
74
+ handled = true;
75
+ ctx.mqttClient.removeListener('message', onResp);
76
+ reject(new Error('MQTT timeout for markAsReadAll'));
77
+ }
78
+ }, 15000);
79
+
80
+ ctx.mqttClient.on('message', onResp);
81
+ ctx.mqttClient.publish('/ls_req', form, { qos: 1, retain: false }, (err) => {
82
+ if (err && !handled) {
83
+ handled = true;
84
+ clearTimeout(timer);
85
+ ctx.mqttClient.removeListener('message', onResp);
86
+ reject(err);
87
+ }
88
+ });
89
+ });
90
+ }
91
+
92
+ async function graphqlMarkAllRead(defaultFuncs, ctx) {
93
+ const form = {
94
+ fb_api_caller_class: 'RelayModern',
95
+ fb_api_req_friendly_name: 'MessengerMarkAllAsReadMutation',
96
+ doc_id: '5044869612218012',
97
+ variables: JSON.stringify({
98
+ input: {
99
+ actor_id: ctx.userID,
100
+ client_mutation_id: String(Math.round(Math.random() * 10000))
101
+ }
102
+ })
103
+ };
104
+ const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
105
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
106
+ if (res?.errors) throw new Error(JSON.stringify(res.errors));
107
+ return { success: true, method: 'graphql', timestamp: Date.now() };
108
+ }
109
+
110
+ module.exports = function (defaultFuncs, api, ctx) {
111
+
112
+ function recordHistory(op) {
113
+ OPERATION_HISTORY.unshift({ ...op, ts: Date.now() });
114
+ if (OPERATION_HISTORY.length > MAX_HISTORY) OPERATION_HISTORY.length = MAX_HISTORY;
115
+ }
116
+
117
+ return async function markAsReadAll(options, callback) {
118
+ if (typeof options === 'function') { callback = options; options = {}; }
119
+ if (typeof options === 'string') options = { folder: options };
120
+ if (!options || typeof options !== 'object') options = {};
121
+
122
+ const {
123
+ folder = 'inbox',
124
+ folders = null,
125
+ preferMqtt = true,
126
+ useGraphQL = false,
127
+ skipCache = false
128
+ } = options;
129
+
130
+ let resolveFunc, rejectFunc;
131
+ const returnPromise = new Promise((resolve, reject) => {
132
+ resolveFunc = resolve;
133
+ rejectFunc = reject;
134
+ });
135
+
136
+ if (typeof callback !== 'function') {
137
+ callback = (err, data) => {
138
+ if (err) return rejectFunc(err);
139
+ resolveFunc(data);
140
+ };
141
+ }
142
+
143
+ try {
144
+ const foldersToMark = folders && Array.isArray(folders) ? folders : [folder];
145
+
146
+ if (!skipCache) {
147
+ const cacheKey = `markAll_${foldersToMark.sort().join('+')}`;
148
+ const cached = MARK_ALL_CACHE.get(cacheKey);
149
+ if (cached && (Date.now() - cached.ts < MARK_ALL_TTL)) {
150
+ utils.log('markAsReadAll', 'Cache hit — skipping redundant request');
151
+ return callback(null, cached.result);
152
+ }
153
+ }
154
+
155
+ await globalShield.addSmartDelay();
156
+
157
+ const results = [];
158
+ const errors = [];
159
+
160
+ for (const f of foldersToMark) {
161
+ try {
162
+ let result;
163
+
164
+ if (useGraphQL) {
165
+ result = await retryOp(() => graphqlMarkAllRead(defaultFuncs, ctx));
166
+ } else if (preferMqtt && ctx.mqttClient) {
167
+ try {
168
+ result = await retryOp(() => mqttMarkAllRead(ctx, f));
169
+ } catch (mqttErr) {
170
+ utils.warn('markAsReadAll', `MQTT failed for folder "${f}", falling back to HTTP:`, mqttErr.message);
171
+ result = await retryOp(() => httpMarkAllRead(defaultFuncs, ctx, f));
172
+ }
173
+ } else {
174
+ result = await retryOp(() => httpMarkAllRead(defaultFuncs, ctx, f));
175
+ }
176
+
177
+ results.push({ folder: f, ...result });
178
+ } catch (folderErr) {
179
+ errors.push({ folder: f, error: folderErr.message || String(folderErr) });
180
+ }
181
+ }
182
+
183
+ const finalResult = {
184
+ success: errors.length === 0,
185
+ results,
186
+ errors: errors.length ? errors : undefined,
187
+ markedFolders: results.map(r => r.folder),
188
+ timestamp: Date.now()
189
+ };
190
+
191
+ const cacheKey = `markAll_${foldersToMark.sort().join('+')}`;
192
+ MARK_ALL_CACHE.set(cacheKey, { result: finalResult, ts: Date.now() });
193
+
194
+ recordHistory({ action: 'markAsReadAll', folders: foldersToMark, success: finalResult.success });
195
+
196
+ utils.log('markAsReadAll', `Marked ${results.length} folder(s) as read${errors.length ? `, ${errors.length} failed` : ''}`);
197
+
198
+ if (errors.length === foldersToMark.length) {
199
+ return callback(new Error(`markAsReadAll: all ${foldersToMark.length} folder(s) failed`));
200
+ }
201
+
202
+ callback(null, finalResult);
203
+ } catch (err) {
204
+ utils.error('markAsReadAll', err);
205
+ callback(err);
206
+ }
207
+
208
+ return returnPromise;
209
+ };
210
+
211
+ Object.assign(module.exports, {
212
+ getHistory: (limit = 20) => OPERATION_HISTORY.slice(0, limit),
213
+ clearCache: () => { MARK_ALL_CACHE.clear(); return { success: true }; }
214
+ });
215
+ };