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,131 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * deleteMessage — Advanced Message Deletion for Phantom SDK
5
+ *
6
+ * Features:
7
+ * - Primary: GraphQL mutation (newer approach, supports recall)
8
+ * - Fallback: Legacy mercury delete endpoint
9
+ * - Batch delete: pass an array of messageIDs
10
+ * - Configurable concurrency for batch operations
11
+ * - Exponential-backoff retry on transient errors
12
+ * - Detailed success/failure per-message result objects
13
+ * - Full callback + Promise dual API
14
+ */
15
+
16
+ const utils = require('../datastore/models/matrix/tools');
17
+
18
+ async function deleteGraphQL(defaultFuncs, ctx, messageID) {
19
+ const form = {
20
+ av: ctx.userID,
21
+ __user: ctx.userID,
22
+ __a: 1,
23
+ fb_dtsg: ctx.fb_dtsg,
24
+ lsd: ctx.lsd || ctx.fb_dtsg,
25
+ fb_api_caller_class: "RelayModern",
26
+ fb_api_req_friendly_name: "useDeleteMessageMutation",
27
+ variables: JSON.stringify({
28
+ input: {
29
+ client_mutation_id: String(Date.now() % 1e9),
30
+ actor_id: ctx.userID,
31
+ message_id: messageID,
32
+ delete_type: "DELETE_FOR_ME",
33
+ }
34
+ }),
35
+ server_timestamps: true,
36
+ doc_id: "6090627834316949",
37
+ };
38
+
39
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
40
+ const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(res);
41
+ if (checked?.errors) throw Object.assign(new Error(checked.errors[0]?.message || JSON.stringify(checked.errors)), { isFatal: true });
42
+ return { success: true, messageID, method: 'graphql' };
43
+ }
44
+
45
+ async function deleteLegacy(defaultFuncs, ctx, messageID) {
46
+ const res = await defaultFuncs.post(
47
+ "https://www.facebook.com/ajax/mercury/delete_messages.php",
48
+ ctx.jar,
49
+ { message_id: messageID }
50
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
51
+
52
+ if (res?.error) throw new Error(JSON.stringify(res.error));
53
+ return { success: true, messageID, method: 'legacy' };
54
+ }
55
+
56
+ async function deleteOne(defaultFuncs, ctx, messageID, retries, baseMs) {
57
+ let lastErr;
58
+ for (let attempt = 1; attempt <= retries; attempt++) {
59
+ try {
60
+ return await deleteGraphQL(defaultFuncs, ctx, messageID);
61
+ } catch (gqlErr) {
62
+ try {
63
+ return await deleteLegacy(defaultFuncs, ctx, messageID);
64
+ } catch (legacyErr) {
65
+ lastErr = gqlErr;
66
+ if (attempt >= retries || gqlErr.isFatal) throw gqlErr;
67
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
68
+ utils.warn("deleteMessage", `Retry ${attempt}/${retries} for ${messageID} in ${Math.round(wait)}ms`);
69
+ await new Promise(r => setTimeout(r, wait));
70
+ }
71
+ }
72
+ }
73
+ throw lastErr;
74
+ }
75
+
76
+ module.exports = (defaultFuncs, api, ctx) => {
77
+
78
+ /**
79
+ * Delete one or more messages.
80
+ *
81
+ * @param {string|string[]} messageID Single ID or array for batch delete
82
+ * @param {object} [options]
83
+ * @param {number} [options.retries=3] Max retry attempts per message
84
+ * @param {number} [options.concurrency=3] Max parallel deletions in batch mode
85
+ * @param {Function} [callback] Node-style (err, result) callback
86
+ * @returns {Promise<object|object[]>}
87
+ */
88
+ return async function deleteMessage(messageID, options, callback) {
89
+ if (typeof options === 'function') { callback = options; options = {}; }
90
+ options = options || {};
91
+
92
+ const retries = options.retries || 3;
93
+ const concurrency = options.concurrency || 3;
94
+ const baseMs = 500;
95
+
96
+ let resolveFunc, rejectFunc;
97
+ const returnPromise = new Promise((resolve, reject) => { resolveFunc = resolve; rejectFunc = reject; });
98
+
99
+ function done(err, result) {
100
+ if (callback) return err ? callback(err) : callback(null, result);
101
+ if (err) rejectFunc(err); else resolveFunc(result);
102
+ }
103
+
104
+ const ids = Array.isArray(messageID) ? messageID : [messageID];
105
+
106
+ if (!ids.length || ids.some(id => !id)) {
107
+ done(new Error("deleteMessage: messageID is required"));
108
+ return returnPromise;
109
+ }
110
+
111
+ try {
112
+ const results = [];
113
+ // Process in concurrency-limited batches
114
+ for (let i = 0; i < ids.length; i += concurrency) {
115
+ const batch = ids.slice(i, i + concurrency);
116
+ const batchR = await Promise.allSettled(batch.map(id => deleteOne(defaultFuncs, ctx, id, retries, baseMs)));
117
+ for (const r of batchR) {
118
+ results.push(r.status === 'fulfilled' ? r.value : { success: false, messageID: batch[results.length % batch.length], error: r.reason?.message });
119
+ }
120
+ }
121
+
122
+ const out = ids.length === 1 ? results[0] : { success: results.every(r => r.success), results };
123
+ done(null, out);
124
+ } catch (err) {
125
+ utils.error("deleteMessage", err.message || err);
126
+ done(err);
127
+ }
128
+
129
+ return returnPromise;
130
+ };
131
+ };
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { globalShield } = require('../datastore/models/matrix/ghost');
5
+
6
+ const CHUNK_SIZE = 10;
7
+ const DELETE_DELAY_MS = 500;
8
+
9
+ async function retryOp(fn, retries = 3, base = 500) {
10
+ for (let i = 0; i < retries; i++) {
11
+ try { return await fn(); } catch (err) {
12
+ if (i === retries - 1) throw err;
13
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
14
+ }
15
+ }
16
+ }
17
+
18
+ async function deleteChunk(defaultFuncs, ctx, chunk) {
19
+ const form = { client: "mercury" };
20
+ chunk.forEach((id, i) => { form[`ids[${i}]`] = id; });
21
+ const res = await defaultFuncs.post(
22
+ "https://www.facebook.com/ajax/mercury/delete_thread.php",
23
+ ctx.jar,
24
+ form
25
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
26
+ if (res && res.error) throw res;
27
+ return chunk;
28
+ }
29
+
30
+ async function mqttDeleteThread(ctx, threadIDs) {
31
+ return new Promise((resolve, reject) => {
32
+ if (!ctx.mqttClient) return reject(new Error("MQTT not connected"));
33
+
34
+ const reqID = ++ctx.wsReqNumber;
35
+ const tasks = threadIDs.map((id) => ({
36
+ failure_count: null,
37
+ label: "19",
38
+ payload: JSON.stringify({ thread_key: id, sync_group: 1 }),
39
+ queue_name: "delete_thread",
40
+ task_id: ++ctx.wsTaskNumber
41
+ }));
42
+
43
+ const form = JSON.stringify({
44
+ app_id: "2220391788200892",
45
+ payload: JSON.stringify({
46
+ epoch_id: utils.generateOfflineThreadingID(),
47
+ tasks,
48
+ version_id: "8798795233522156"
49
+ }),
50
+ request_id: reqID,
51
+ type: 3
52
+ });
53
+
54
+ let handled = false;
55
+ const onResp = (topic, message) => {
56
+ if (topic !== "/ls_resp" || handled) return;
57
+ let j;
58
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
59
+ if (j.request_id !== reqID) return;
60
+ handled = true;
61
+ clearTimeout(timer);
62
+ ctx.mqttClient.removeListener("message", onResp);
63
+ resolve({ success: true, deletedThreads: threadIDs, method: "mqtt" });
64
+ };
65
+
66
+ const timer = setTimeout(() => {
67
+ if (!handled) {
68
+ handled = true;
69
+ ctx.mqttClient.removeListener("message", onResp);
70
+ resolve({ success: true, deletedThreads: threadIDs, method: "mqtt_no_ack" });
71
+ }
72
+ }, 15000);
73
+
74
+ ctx.mqttClient.on("message", onResp);
75
+ ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
76
+ if (err && !handled) {
77
+ handled = true;
78
+ clearTimeout(timer);
79
+ ctx.mqttClient.removeListener("message", onResp);
80
+ reject(err);
81
+ }
82
+ });
83
+ });
84
+ }
85
+
86
+ module.exports = (defaultFuncs, api, ctx) => {
87
+ return async function deleteThread(threadOrThreads, callback) {
88
+ let resolveFunc, rejectFunc;
89
+ const returnPromise = new Promise((resolve, reject) => {
90
+ resolveFunc = resolve;
91
+ rejectFunc = reject;
92
+ });
93
+
94
+ if (typeof callback !== "function") {
95
+ callback = (err, result) => {
96
+ if (err) return rejectFunc(err);
97
+ resolveFunc(result);
98
+ };
99
+ }
100
+
101
+ try {
102
+ const threads = Array.isArray(threadOrThreads)
103
+ ? threadOrThreads.map(String)
104
+ : [String(threadOrThreads)];
105
+
106
+ if (threads.length === 0) throw new Error("deleteThread: no threadIDs provided");
107
+
108
+ await globalShield.addSmartDelay();
109
+
110
+ const deleted = [];
111
+ const failed = [];
112
+
113
+ if (ctx.mqttClient) {
114
+ for (let i = 0; i < threads.length; i += CHUNK_SIZE) {
115
+ const chunk = threads.slice(i, i + CHUNK_SIZE);
116
+ try {
117
+ const res = await retryOp(() => mqttDeleteThread(ctx, chunk));
118
+ deleted.push(...res.deletedThreads);
119
+ } catch (err) {
120
+ utils.warn("deleteThread", "MQTT chunk failed, using HTTP:", err.message);
121
+ try {
122
+ const res = await retryOp(() => deleteChunk(defaultFuncs, ctx, chunk));
123
+ deleted.push(...res);
124
+ } catch (httpErr) {
125
+ failed.push(...chunk);
126
+ }
127
+ }
128
+ if (i + CHUNK_SIZE < threads.length) {
129
+ await new Promise(r => setTimeout(r, DELETE_DELAY_MS));
130
+ }
131
+ }
132
+ } else {
133
+ for (let i = 0; i < threads.length; i += CHUNK_SIZE) {
134
+ const chunk = threads.slice(i, i + CHUNK_SIZE);
135
+ try {
136
+ const res = await retryOp(() => deleteChunk(defaultFuncs, ctx, chunk));
137
+ deleted.push(...res);
138
+ } catch (err) {
139
+ failed.push(...chunk);
140
+ }
141
+ if (i + CHUNK_SIZE < threads.length) {
142
+ await new Promise(r => setTimeout(r, DELETE_DELAY_MS));
143
+ }
144
+ }
145
+ }
146
+
147
+ callback(null, { success: true, deleted, failed, count: deleted.length });
148
+ } catch (err) {
149
+ utils.error("deleteThread", err);
150
+ callback(err);
151
+ }
152
+
153
+ return returnPromise;
154
+ };
155
+ };
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+
3
+ const e2ee = require('../datastore/models/cipher/e2ee');
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ const PEER_CACHE = new Map();
7
+
8
+ function getPeerCount() {
9
+ if (!ctx._e2eePeers) return 0;
10
+ return Object.keys(ctx._e2eePeers).length;
11
+ }
12
+
13
+ function getStats() {
14
+ return {
15
+ enabled: e2ee.isEnabled(ctx),
16
+ peerCount: getPeerCount(),
17
+ hasPublicKey: !!e2ee.getPublicKey(ctx),
18
+ peerIDs: ctx._e2eePeers ? Object.keys(ctx._e2eePeers) : []
19
+ };
20
+ }
21
+
22
+ function exportKeyring() {
23
+ return {
24
+ publicKey: e2ee.getPublicKey(ctx),
25
+ peers: ctx._e2eePeers ? { ...ctx._e2eePeers } : {},
26
+ exportedAt: Date.now()
27
+ };
28
+ }
29
+
30
+ function importKeyring(keyring) {
31
+ if (!keyring || typeof keyring !== "object") throw new Error("e2ee.importKeyring: invalid keyring object");
32
+ if (keyring.peers && typeof keyring.peers === "object") {
33
+ for (const [threadID, key] of Object.entries(keyring.peers)) {
34
+ e2ee.setPeerKey(ctx, threadID, key);
35
+ }
36
+ }
37
+ return { success: true, importedPeers: Object.keys(keyring.peers || {}).length };
38
+ }
39
+
40
+ function encryptBatch(entries) {
41
+ if (!Array.isArray(entries)) throw new Error("e2ee.encryptBatch: entries must be an array");
42
+ return entries.map(({ threadID, text }) => {
43
+ try {
44
+ return { threadID, encrypted: e2ee.encrypt(ctx, threadID, text), error: null };
45
+ } catch (err) {
46
+ return { threadID, encrypted: null, error: err.message };
47
+ }
48
+ });
49
+ }
50
+
51
+ function decryptBatch(entries) {
52
+ if (!Array.isArray(entries)) throw new Error("e2ee.decryptBatch: entries must be an array");
53
+ return entries.map(({ threadID, armored }) => {
54
+ try {
55
+ return { threadID, decrypted: e2ee.decrypt(ctx, threadID, armored), error: null };
56
+ } catch (err) {
57
+ return { threadID, decrypted: null, error: err.message };
58
+ }
59
+ });
60
+ }
61
+
62
+ function rotatePeerKey(threadID, newPeerPublicKeyB64) {
63
+ e2ee.clearPeerKey(ctx, threadID);
64
+ e2ee.setPeerKey(ctx, newPeerPublicKeyB64 ? threadID : null, newPeerPublicKeyB64);
65
+ return { success: true, threadID, rotated: !!newPeerPublicKeyB64 };
66
+ }
67
+
68
+ return {
69
+ enable() { e2ee.enable(ctx); return { success: true, enabled: true }; },
70
+ disable() { e2ee.disable(ctx); return { success: true, enabled: false }; },
71
+ isEnabled() { return e2ee.isEnabled(ctx); },
72
+ getPublicKey() { return e2ee.getPublicKey(ctx); },
73
+ setPeerKey(threadID, peerPublicKeyB64) {
74
+ if (!threadID) throw new Error("e2ee.setPeerKey: threadID is required");
75
+ if (!peerPublicKeyB64) throw new Error("e2ee.setPeerKey: peerPublicKeyB64 is required");
76
+ e2ee.setPeerKey(ctx, threadID, peerPublicKeyB64);
77
+ PEER_CACHE.set(threadID, { key: peerPublicKeyB64, ts: Date.now() });
78
+ return { success: true, threadID };
79
+ },
80
+ clearPeerKey(threadID) {
81
+ e2ee.clearPeerKey(ctx, threadID);
82
+ PEER_CACHE.delete(threadID);
83
+ return { success: true, threadID };
84
+ },
85
+ hasPeer(threadID) { return e2ee.hasPeer(ctx, threadID); },
86
+ encrypt(threadID, text) { return e2ee.encrypt(ctx, threadID, text); },
87
+ decrypt(threadID, armored) { return e2ee.decrypt(ctx, threadID, armored); },
88
+ getStats,
89
+ exportKeyring,
90
+ importKeyring,
91
+ encryptBatch,
92
+ decryptBatch,
93
+ rotatePeerKey,
94
+ clearAllPeers() {
95
+ const peers = ctx._e2eePeers ? Object.keys(ctx._e2eePeers) : [];
96
+ peers.forEach(id => e2ee.clearPeerKey(ctx, id));
97
+ PEER_CACHE.clear();
98
+ return { success: true, clearedCount: peers.length };
99
+ }
100
+ };
101
+ };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * editMessage — Advanced Message Editor for Phantom SDK
5
+ *
6
+ * @author RFS-ADRENO (original) — massively extended for phantom-fca
7
+ *
8
+ * Features:
9
+ * - MQTT path (primary) — low-latency realtime edit via /ls_req
10
+ * - GraphQL mutation fallback — when MQTT unavailable or fails
11
+ * - Input validation (text length, messageID format)
12
+ * - Retry with exponential backoff on MQTT publish errors
13
+ * - Promise callback support (returns Promise when no callback given)
14
+ * - Preserves originalText + editedAt metadata in resolve value
15
+ */
16
+
17
+ const utils = require('../datastore/models/matrix/tools');
18
+
19
+ const MAX_TEXT_LENGTH = 20000;
20
+
21
+ async function editViaMqtt(ctx, text, messageID, retries, baseMs) {
22
+ if (!ctx.mqttClient) throw new Error("editMessage: MQTT client is not connected");
23
+
24
+ let lastErr;
25
+ for (let attempt = 1; attempt <= retries; attempt++) {
26
+ ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
27
+ ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
28
+
29
+ const payload = {
30
+ failure_count: null,
31
+ label: '742',
32
+ payload: JSON.stringify({ message_id: messageID, text }),
33
+ queue_name: 'edit_message',
34
+ task_id: ctx.wsTaskNumber,
35
+ };
36
+
37
+ const content = {
38
+ app_id: '2220391788200892',
39
+ payload: JSON.stringify({
40
+ data_trace_id: null,
41
+ epoch_id: parseInt(utils.generateOfflineThreadingID ? utils.generateOfflineThreadingID() : Date.now()),
42
+ tasks: [payload],
43
+ version_id: '6903494529735864',
44
+ }),
45
+ request_id: ctx.wsReqNumber,
46
+ type: 3,
47
+ };
48
+
49
+ try {
50
+ await new Promise((resolve, reject) => {
51
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, err => {
52
+ if (err) reject(new Error(`MQTT publish failed: ${err.message}`));
53
+ else resolve();
54
+ });
55
+ });
56
+ return { success: true, messageID, text, method: 'mqtt', editedAt: Date.now() };
57
+ } catch (err) {
58
+ lastErr = err;
59
+ if (attempt >= retries) throw err;
60
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 150;
61
+ utils.warn("editMessage", `MQTT retry ${attempt}/${retries} in ${Math.round(wait)}ms`);
62
+ await new Promise(r => setTimeout(r, wait));
63
+ }
64
+ }
65
+ throw lastErr;
66
+ }
67
+
68
+ async function editViaGraphQL(defaultFuncs, ctx, text, messageID) {
69
+ const form = {
70
+ av: ctx.userID,
71
+ __user: ctx.userID,
72
+ __a: 1,
73
+ fb_dtsg: ctx.fb_dtsg,
74
+ lsd: ctx.lsd || ctx.fb_dtsg,
75
+ fb_api_caller_class: "RelayModern",
76
+ fb_api_req_friendly_name: "useEditMessageMutation",
77
+ variables: JSON.stringify({
78
+ input: {
79
+ client_mutation_id: String(Date.now() % 1e9),
80
+ actor_id: ctx.userID,
81
+ message_id: messageID,
82
+ text,
83
+ }
84
+ }),
85
+ server_timestamps: true,
86
+ doc_id: "6363606433743285",
87
+ };
88
+
89
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
90
+ const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(res);
91
+ if (checked?.errors) throw new Error(checked.errors[0]?.message || JSON.stringify(checked.errors));
92
+ return { success: true, messageID, text, method: 'graphql', editedAt: Date.now() };
93
+ }
94
+
95
+ module.exports = function (defaultFuncs, api, ctx) {
96
+
97
+ /**
98
+ * Edit a previously sent message.
99
+ *
100
+ * @param {string} text New message text
101
+ * @param {string} messageID ID of the message to edit
102
+ * @param {object} [options] { retries, preferGraphQL }
103
+ * @param {Function} [callback] Node-style (err, result) callback
104
+ * @returns {Promise<object>} { success, messageID, text, method, editedAt }
105
+ */
106
+ return async function editMessage(text, messageID, options, callback) {
107
+ if (typeof options === 'function') { callback = options; options = {}; }
108
+ options = options || {};
109
+
110
+ const retries = options.retries || 3;
111
+ const baseMs = 500;
112
+
113
+ let resolveFunc, rejectFunc;
114
+ const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
115
+
116
+ function done(err, data) {
117
+ if (callback) return err ? callback(err) : callback(null, data);
118
+ if (err) rejectFunc(err); else resolveFunc(data);
119
+ }
120
+
121
+ // Validation
122
+ if (typeof text !== 'string' || !text.trim()) {
123
+ done(new Error("editMessage: text must be a non-empty string"));
124
+ return promise;
125
+ }
126
+ if (!messageID) {
127
+ done(new Error("editMessage: messageID is required"));
128
+ return promise;
129
+ }
130
+ if (text.length > MAX_TEXT_LENGTH) {
131
+ done(new Error(`editMessage: text exceeds maximum length of ${MAX_TEXT_LENGTH} characters`));
132
+ return promise;
133
+ }
134
+
135
+ const sanitized = text.trimEnd();
136
+
137
+ try {
138
+ let result;
139
+ if (!options.preferGraphQL && ctx.mqttClient) {
140
+ try {
141
+ result = await editViaMqtt(ctx, sanitized, messageID, retries, baseMs);
142
+ } catch (mqttErr) {
143
+ utils.warn("editMessage", `MQTT failed — falling back to GraphQL: ${mqttErr.message}`);
144
+ result = await editViaGraphQL(defaultFuncs, ctx, sanitized, messageID);
145
+ }
146
+ } else {
147
+ result = await editViaGraphQL(defaultFuncs, ctx, sanitized, messageID);
148
+ }
149
+
150
+ done(null, result);
151
+ } catch (err) {
152
+ utils.error("editMessage", err.message || err);
153
+ done(err);
154
+ }
155
+
156
+ return promise;
157
+ };
158
+ };
@@ -0,0 +1,143 @@
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 mqttSetEmoji(ctx, emoji, threadID, initiatorID) {
16
+ return new Promise((resolve, reject) => {
17
+ const reqID = ++ctx.wsReqNumber;
18
+ const taskID = ++ctx.wsTaskNumber;
19
+
20
+ const context = JSON.stringify({
21
+ app_id: ctx.appID || "2220391788200892",
22
+ payload: JSON.stringify({
23
+ epoch_id: parseInt(utils.generateOfflineThreadingID()),
24
+ tasks: [{
25
+ failure_count: null,
26
+ label: "100003",
27
+ payload: JSON.stringify({
28
+ thread_key: String(threadID),
29
+ custom_emoji: emoji,
30
+ avatar_sticker_instruction_key_id: null,
31
+ sync_group: 1
32
+ }),
33
+ queue_name: "thread_quick_reaction",
34
+ task_id: taskID
35
+ }],
36
+ version_id: "24631415369801570"
37
+ }),
38
+ request_id: reqID,
39
+ type: 3
40
+ });
41
+
42
+ ctx.mqttClient.publish("/ls_req", context, { qos: 1, retain: false }, (err) => {
43
+ if (err) return reject(err);
44
+ resolve({
45
+ type: "thread_emoji_update",
46
+ threadID,
47
+ newEmoji: emoji,
48
+ senderID: initiatorID,
49
+ BotID: ctx.userID,
50
+ timestamp: Date.now(),
51
+ method: "mqtt"
52
+ });
53
+ });
54
+ });
55
+ }
56
+
57
+ async function httpSetEmoji(defaultFuncs, ctx, emoji, threadID, initiatorID) {
58
+ const form = {
59
+ emoji_choice: emoji,
60
+ thread_or_other_fbid: threadID
61
+ };
62
+ const res = await defaultFuncs.post(
63
+ "https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
64
+ ctx.jar, form
65
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
66
+ if (res.error === 1357031) throw { error: "Thread has no messages yet. Send a message first." };
67
+ if (res.error) throw res;
68
+ return {
69
+ type: "thread_emoji_update",
70
+ threadID,
71
+ newEmoji: emoji,
72
+ senderID: initiatorID,
73
+ BotID: ctx.userID,
74
+ timestamp: Date.now(),
75
+ method: "http"
76
+ };
77
+ }
78
+
79
+ module.exports = (defaultFuncs, api, ctx) => {
80
+ return function emojiSet(emoji, threadID, callback, initiatorID) {
81
+ let _callback, _initiatorID;
82
+ let resolveFunc, rejectFunc;
83
+ const returnPromise = new Promise((resolve, reject) => {
84
+ resolveFunc = resolve;
85
+ rejectFunc = reject;
86
+ });
87
+
88
+ const type = utils.getType(callback);
89
+ if (type === "Function" || type === "AsyncFunction") {
90
+ _callback = callback;
91
+ _initiatorID = initiatorID;
92
+ } else if (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction") {
93
+ _callback = threadID;
94
+ threadID = null;
95
+ _initiatorID = callback;
96
+ } else if (type === "String") {
97
+ _initiatorID = callback;
98
+ _callback = undefined;
99
+ }
100
+
101
+ if (typeof _callback !== "function") {
102
+ _callback = (err, data) => {
103
+ if (err) return rejectFunc(err);
104
+ resolveFunc(data);
105
+ };
106
+ } else {
107
+ const orig = _callback;
108
+ _callback = (err, data) => {
109
+ orig(err, data);
110
+ if (err) rejectFunc(err); else resolveFunc(data);
111
+ };
112
+ }
113
+
114
+ _initiatorID = _initiatorID || ctx.userID;
115
+ threadID = threadID || ctx.threadID;
116
+
117
+ if (!emoji) return _callback(new Error("emoji: emoji character is required")), returnPromise;
118
+ if (!threadID) return _callback(new Error("emoji: threadID is required")), returnPromise;
119
+
120
+ (async () => {
121
+ try {
122
+ await globalShield.addSmartDelay();
123
+ let result;
124
+ if (ctx.mqttClient) {
125
+ try {
126
+ result = await retryOp(() => mqttSetEmoji(ctx, emoji, threadID, _initiatorID));
127
+ } catch (mqttErr) {
128
+ utils.warn("emoji", "MQTT failed, using HTTP:", mqttErr.message);
129
+ result = await retryOp(() => httpSetEmoji(defaultFuncs, ctx, emoji, threadID, _initiatorID));
130
+ }
131
+ } else {
132
+ result = await retryOp(() => httpSetEmoji(defaultFuncs, ctx, emoji, threadID, _initiatorID));
133
+ }
134
+ _callback(null, result);
135
+ } catch (err) {
136
+ utils.error("emoji", err);
137
+ _callback(err);
138
+ }
139
+ })();
140
+
141
+ return returnPromise;
142
+ };
143
+ };