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,132 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ /**
7
+ * Made by Choru Official
8
+ * Mqtt
9
+ * Sets a nickname for a participant in a Facebook thread via MQTT.
10
+ *
11
+ * @param {string} nickname The new nickname to set.
12
+ * @param {string} threadID The ID of the thread.
13
+ * @param {string} participantID The ID of the participant whose nickname will be changed. Defaults to the current user's ID if not provided or a function.
14
+ * @param {Function} [callback] Optional callback function to be invoked upon completion.
15
+ * @param {string} [initiatorID] The ID of the user who initiated the nickname change (e.g., from event.senderID).
16
+ * @returns {Promise<object>} A promise that resolves with a structured event object on success or rejects on error.
17
+ */
18
+ return function setNickname(nickname, threadID, participantID, callback, initiatorID) {
19
+ let _callback;
20
+ let _initiatorID;
21
+
22
+ let _resolvePromise;
23
+ let _rejectPromise;
24
+ const returnPromise = new Promise((resolve, reject) => {
25
+ _resolvePromise = resolve;
26
+ _rejectPromise = reject;
27
+ });
28
+
29
+ if (utils.getType(callback) === "Function" || utils.getType(callback) === "AsyncFunction") {
30
+ _callback = callback;
31
+ _initiatorID = initiatorID;
32
+ } else if (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction") {
33
+ _callback = threadID;
34
+ threadID = null;
35
+ _initiatorID = callback;
36
+ } else if (utils.getType(participantID) === "Function" || utils.getType(participantID) === "AsyncFunction") {
37
+ _callback = participantID;
38
+ participantID = ctx.userID;
39
+ _initiatorID = callback;
40
+ }
41
+ else if (utils.getType(callback) === "string") {
42
+ _initiatorID = callback;
43
+ _callback = undefined;
44
+ } else {
45
+ _callback = undefined;
46
+ _initiatorID = undefined;
47
+ }
48
+
49
+ if (!_callback) {
50
+ _callback = function (__err, __data) {
51
+ if (__err) _rejectPromise(__err);
52
+ else _resolvePromise(__data);
53
+ };
54
+ } else {
55
+ const originalCallback = _callback;
56
+ _callback = function(__err, __data) {
57
+ if (__err) {
58
+ originalCallback(__err);
59
+ _rejectPromise(__err);
60
+ } else {
61
+ originalCallback(null, __data);
62
+ _resolvePromise(__data);
63
+ }
64
+ };
65
+ }
66
+
67
+ _initiatorID = _initiatorID || ctx.userID;
68
+
69
+ threadID = threadID || ctx.threadID;
70
+ participantID = participantID || ctx.userID;
71
+
72
+ if (!threadID) {
73
+ return _callback(new Error("threadID is required to set a nickname."));
74
+ }
75
+ if (typeof nickname !== 'string') {
76
+ return _callback(new Error("nickname must be a string."));
77
+ }
78
+
79
+ if (!ctx.mqttClient) {
80
+ return _callback(new Error("Not connected to MQTT"));
81
+ }
82
+
83
+ ctx.wsReqNumber += 1;
84
+ ctx.wsTaskNumber += 1;
85
+
86
+ const queryPayload = {
87
+ thread_key: threadID.toString(),
88
+ contact_id: participantID.toString(),
89
+ nickname: nickname,
90
+ sync_group: 1,
91
+ };
92
+
93
+ const query = {
94
+ failure_count: null,
95
+ label: '44',
96
+ payload: JSON.stringify(queryPayload),
97
+ queue_name: 'thread_participant_nickname',
98
+ task_id: ctx.wsTaskNumber,
99
+ };
100
+
101
+ const context = {
102
+ app_id: ctx.appID,
103
+ payload: {
104
+ epoch_id: parseInt(utils.generateOfflineThreadingID()),
105
+ tasks: [query],
106
+ version_id: '24631415369801570',
107
+ },
108
+ request_id: ctx.wsReqNumber,
109
+ type: 3,
110
+ };
111
+ context.payload = JSON.stringify(context.payload);
112
+
113
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
114
+ if (err) {
115
+ return _callback(new Error(`MQTT publish failed for setNickname: ${err.message || err}`));
116
+ }
117
+
118
+ const nicknameChangeEvent = {
119
+ type: "thread_nickname_update",
120
+ threadID: threadID,
121
+ participantID: participantID,
122
+ newNickname: nickname,
123
+ senderID: _initiatorID,
124
+ BotID: ctx.userID,
125
+ timestamp: Date.now(),
126
+ };
127
+ _callback(null, nicknameChangeEvent);
128
+ });
129
+
130
+ return returnPromise;
131
+ };
132
+ };
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Notes Module — Advanced Messenger Notes for Phantom SDK
5
+ *
6
+ * @ChoruOfficial (original) — massively extended for phantom-fca
7
+ *
8
+ * Features:
9
+ * - Full Promise + callback dual API for all methods
10
+ * - Privacy levels: EVERYONE | FRIENDS | CLOSE_FRIENDS | CUSTOM
11
+ * - Customisable note duration (default 24h)
12
+ * - Optional emoji reaction attachment
13
+ * - Exponential-backoff retry on transient GraphQL errors
14
+ * - Batch delete support
15
+ * - Structured output with noteID, text, expiresAt, privacyLevel
16
+ */
17
+
18
+ const utils = require('../datastore/models/matrix/tools');
19
+
20
+ const PRIVACY_VALUES = new Set(['EVERYONE', 'FRIENDS', 'CLOSE_FRIENDS', 'CUSTOM', 'ONLY_ME']);
21
+ const DEFAULT_DURATION = 86400; // 24 hours in seconds
22
+
23
+ function wrapPromise(fn) {
24
+ let resolve, reject;
25
+ const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
26
+ const cb = (err, data) => { if (err) reject(err); else resolve(data); };
27
+ fn(cb);
28
+ return promise;
29
+ }
30
+
31
+ async function gqlPost(defaultFuncs, ctx, form, retries = 3, baseMs = 500) {
32
+ let lastErr;
33
+ for (let attempt = 1; attempt <= retries; attempt++) {
34
+ try {
35
+ const resData = await defaultFuncs
36
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
37
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
38
+ if (resData?.errors) {
39
+ const msg = resData.errors[0]?.message || JSON.stringify(resData.errors);
40
+ throw Object.assign(new Error(msg), { isFatal: /auth|permission|checkpoint/i.test(msg) });
41
+ }
42
+ return resData;
43
+ } catch (err) {
44
+ lastErr = err;
45
+ if (err.isFatal || attempt >= retries) throw err;
46
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
47
+ utils.warn("notes", `Retry ${attempt}/${retries} in ${Math.round(wait)}ms`);
48
+ await new Promise(r => setTimeout(r, wait));
49
+ }
50
+ }
51
+ throw lastErr;
52
+ }
53
+
54
+ module.exports = function (defaultFuncs, api, ctx) {
55
+
56
+ /**
57
+ * Check the currently active Messenger note for this account.
58
+ * @param {Function} [callback]
59
+ * @returns {Promise<object|null>}
60
+ */
61
+ function checkNote(callback) {
62
+ const form = {
63
+ fb_api_caller_class: "RelayModern",
64
+ fb_api_req_friendly_name: "MWInboxTrayNoteCreationDialogQuery",
65
+ variables: JSON.stringify({ scale: 2 }),
66
+ doc_id: "30899655739648624",
67
+ };
68
+
69
+ const exec = async cb => {
70
+ try {
71
+ const res = await gqlPost(defaultFuncs, ctx, form);
72
+ const note = res?.data?.viewer?.actor?.msgr_user_rich_status || null;
73
+ const out = note ? {
74
+ noteID: note.id,
75
+ text: note.text || note.description || null,
76
+ emoji: note.emoji || null,
77
+ privacyLevel: note.privacy || null,
78
+ createdAt: note.creation_time || null,
79
+ expiresAt: note.expiration_time || null,
80
+ isActive: true,
81
+ } : null;
82
+ cb(null, out);
83
+ } catch (err) {
84
+ utils.error("notes.checkNote", err.message || err);
85
+ cb(err);
86
+ }
87
+ };
88
+
89
+ if (callback) { exec(callback); return; }
90
+ return wrapPromise(exec);
91
+ }
92
+
93
+ /**
94
+ * Create a new Messenger note.
95
+ * @param {string} text
96
+ * @param {object} [options] { privacy, duration, emoji }
97
+ * @param {Function} [callback]
98
+ * @returns {Promise<object>}
99
+ */
100
+ function createNote(text, options, callback) {
101
+ if (typeof options === 'function') { callback = options; options = {}; }
102
+ options = options || {};
103
+
104
+ const privacy = PRIVACY_VALUES.has(options.privacy) ? options.privacy : 'EVERYONE';
105
+ const duration = options.duration || DEFAULT_DURATION;
106
+
107
+ const input = {
108
+ client_mutation_id: String(Date.now() % 1e9),
109
+ actor_id: ctx.userID,
110
+ description: typeof text === 'string' ? text : String(text),
111
+ duration,
112
+ note_type: "TEXT_NOTE",
113
+ privacy,
114
+ session_id: utils.getGUID ? utils.getGUID() : String(Math.random()),
115
+ };
116
+ if (options.emoji) input.emoji = options.emoji;
117
+
118
+ const form = {
119
+ fb_api_caller_class: "RelayModern",
120
+ fb_api_req_friendly_name: "MWInboxTrayNoteCreationDialogCreationStepContentMutation",
121
+ variables: JSON.stringify({ input }),
122
+ doc_id: "24060573783603122",
123
+ };
124
+
125
+ const exec = async cb => {
126
+ try {
127
+ const res = await gqlPost(defaultFuncs, ctx, form);
128
+ const status = res?.data?.xfb_rich_status_create?.status;
129
+ if (!status) throw new Error("No note status in response — creation may have failed");
130
+ cb(null, {
131
+ noteID: status.id,
132
+ text: status.text || text,
133
+ emoji: status.emoji || options.emoji || null,
134
+ privacyLevel: privacy,
135
+ duration,
136
+ expiresAt: status.expiration_time || (Date.now() + duration * 1000),
137
+ createdAt: Date.now(),
138
+ });
139
+ } catch (err) {
140
+ utils.error("notes.createNote", err.message || err);
141
+ cb(err);
142
+ }
143
+ };
144
+
145
+ if (callback) { exec(callback); return; }
146
+ return wrapPromise(exec);
147
+ }
148
+
149
+ /**
150
+ * Delete a Messenger note by ID.
151
+ * @param {string|string[]} noteID One ID or array for batch delete
152
+ * @param {Function} [callback]
153
+ * @returns {Promise<object>}
154
+ */
155
+ function deleteNote(noteID, callback) {
156
+ const ids = Array.isArray(noteID) ? noteID : [noteID];
157
+
158
+ const deleteOne = async id => {
159
+ const form = {
160
+ fb_api_caller_class: "RelayModern",
161
+ fb_api_req_friendly_name: "useMWInboxTrayDeleteNoteMutation",
162
+ variables: JSON.stringify({
163
+ input: {
164
+ client_mutation_id: String(Date.now() % 1e9),
165
+ actor_id: ctx.userID,
166
+ rich_status_id: id,
167
+ }
168
+ }),
169
+ doc_id: "9532619970198958",
170
+ };
171
+ const res = await gqlPost(defaultFuncs, ctx, form);
172
+ if (!res?.data?.xfb_rich_status_delete) throw new Error(`Delete note ${id}: no confirmation in response`);
173
+ return { success: true, noteID: id };
174
+ };
175
+
176
+ const exec = async cb => {
177
+ try {
178
+ const results = await Promise.all(ids.map(deleteOne));
179
+ cb(null, ids.length === 1 ? results[0] : results);
180
+ } catch (err) {
181
+ utils.error("notes.deleteNote", err.message || err);
182
+ cb(err);
183
+ }
184
+ };
185
+
186
+ if (callback) { exec(callback); return; }
187
+ return wrapPromise(exec);
188
+ }
189
+
190
+ /**
191
+ * Delete the old note and create a new one atomically (sequential).
192
+ * @param {string} oldNoteID
193
+ * @param {string} newText
194
+ * @param {object} [options] createNote options { privacy, duration, emoji }
195
+ * @param {Function} [callback]
196
+ * @returns {Promise<{ deleted, created }>}
197
+ */
198
+ function recreateNote(oldNoteID, newText, options, callback) {
199
+ if (typeof options === 'function') { callback = options; options = {}; }
200
+ options = options || {};
201
+
202
+ const exec = async cb => {
203
+ try {
204
+ const deleted = await new Promise((res, rej) => deleteNote(oldNoteID, (err, d) => err ? rej(err) : res(d)));
205
+ const created = await new Promise((res, rej) => createNote(newText, options, (err, c) => err ? rej(err) : res(c)));
206
+ cb(null, { deleted, created });
207
+ } catch (err) {
208
+ utils.error("notes.recreateNote", err.message || err);
209
+ cb(err);
210
+ }
211
+ };
212
+
213
+ if (callback) { exec(callback); return; }
214
+ return wrapPromise(exec);
215
+ }
216
+
217
+ /**
218
+ * Update an existing note: delete old + create new. Alias for recreateNote.
219
+ */
220
+ function updateNote(oldNoteID, newText, options, callback) {
221
+ return recreateNote(oldNoteID, newText, options, callback);
222
+ }
223
+
224
+ /**
225
+ * Delete the current active note (if any) and optionally create a new one.
226
+ * @param {string} [newText] If provided, create a replacement note
227
+ * @param {object} [options]
228
+ * @param {Function} [callback]
229
+ * @returns {Promise<object>}
230
+ */
231
+ async function replaceActiveNote(newText, options, callback) {
232
+ if (typeof options === 'function') { callback = options; options = {}; }
233
+ options = options || {};
234
+
235
+ const exec = async cb => {
236
+ try {
237
+ const current = await new Promise((res, rej) => checkNote((err, n) => err ? rej(err) : res(n)));
238
+ let deleted = null;
239
+ if (current?.noteID) {
240
+ deleted = await new Promise((res, rej) => deleteNote(current.noteID, (err, d) => err ? rej(err) : res(d)));
241
+ }
242
+ if (!newText) return cb(null, { deleted, created: null });
243
+ const created = await new Promise((res, rej) => createNote(newText, options, (err, c) => err ? rej(err) : res(c)));
244
+ cb(null, { deleted, created });
245
+ } catch (err) {
246
+ utils.error("notes.replaceActiveNote", err.message || err);
247
+ cb(err);
248
+ }
249
+ };
250
+
251
+ if (callback) { exec(callback); return; }
252
+ return wrapPromise(exec);
253
+ }
254
+
255
+ return {
256
+ create: createNote,
257
+ delete: deleteNote,
258
+ recreate: recreateNote,
259
+ update: updateNote,
260
+ check: checkNote,
261
+ replaceActive: replaceActiveNote,
262
+ };
263
+ };
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const _ = require('lodash');
5
+ const deepdash = require('deepdash');
6
+ const { JSONPath } = require('jsonpath-plus');
7
+ const { globalShield } = require('../datastore/models/matrix/ghost');
8
+
9
+ deepdash(_);
10
+
11
+ const PIN_CACHE = new Map();
12
+ const PIN_CACHE_TTL = 2 * 60 * 1000;
13
+ const PIN_HISTORY = [];
14
+ const MAX_HISTORY = 200;
15
+ const INFLIGHT_PINS = new Map();
16
+
17
+ async function retryOp(fn, retries = 4, base = 400) {
18
+ for (let i = 0; i < retries; i++) {
19
+ try { return await fn(); } catch (err) {
20
+ if (i === retries - 1) throw err;
21
+ const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
22
+ if (!transient) throw err;
23
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
24
+ }
25
+ }
26
+ }
27
+
28
+ function extractAndSearchLightspeedRequest(allJsonData) {
29
+ const lightReq = _.get(allJsonData, '__bbox.result.data.viewer.lightspeed_web_request');
30
+ if (!lightReq) return null;
31
+
32
+ try {
33
+ const matches = JSONPath({
34
+ path: `$..[?(@ === "setPinnedMessage" || @ === "deleteThenInsertMessage")]`,
35
+ json: lightReq
36
+ });
37
+ return { lightReq, pinnedCommands: matches, commandCount: matches.length };
38
+ } catch {
39
+ return { lightReq, pinnedCommands: [], commandCount: 0 };
40
+ }
41
+ }
42
+
43
+ function mqttPublishWithAck(ctx, content, timeoutMs = 15000) {
44
+ return new Promise((resolve, reject) => {
45
+ const reqID = content.request_id;
46
+ let handled = false;
47
+
48
+ const onResp = (topic, message) => {
49
+ if (topic !== '/ls_resp' || handled) return;
50
+ let j;
51
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
52
+ if (j.request_id !== reqID) return;
53
+ handled = true;
54
+ clearTimeout(timer);
55
+ ctx.mqttClient.removeListener('message', onResp);
56
+ resolve({ success: true, request_id: reqID, acked: true });
57
+ };
58
+
59
+ const timer = setTimeout(() => {
60
+ if (!handled) {
61
+ handled = true;
62
+ ctx.mqttClient.removeListener('message', onResp);
63
+ resolve({ success: true, request_id: reqID, acked: false, timeout: true });
64
+ }
65
+ }, timeoutMs);
66
+
67
+ ctx.mqttClient.on('message', onResp);
68
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, (err) => {
69
+ if (err && !handled) {
70
+ handled = true;
71
+ clearTimeout(timer);
72
+ ctx.mqttClient.removeListener('message', onResp);
73
+ reject(err);
74
+ }
75
+ });
76
+ });
77
+ }
78
+
79
+ function buildPinRequest(ctx, tasks, epochIncrement = 0) {
80
+ return {
81
+ app_id: '2220391788200892',
82
+ payload: JSON.stringify({
83
+ epoch_id: parseInt(utils.generateOfflineThreadingID()) + epochIncrement,
84
+ tasks,
85
+ version_id: '9523201934447612'
86
+ }),
87
+ request_id: (ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1),
88
+ type: 3
89
+ };
90
+ }
91
+
92
+ function recordHistory(op) {
93
+ PIN_HISTORY.unshift({ ...op, ts: Date.now() });
94
+ if (PIN_HISTORY.length > MAX_HISTORY) PIN_HISTORY.length = MAX_HISTORY;
95
+ }
96
+
97
+ module.exports = function (defaultFuncs, api, ctx) {
98
+
99
+ return async function pin(action, threadID, messageID, options, callback) {
100
+ if (typeof options === 'function') { callback = options; options = {}; }
101
+ if (!options || typeof options !== 'object') options = {};
102
+ const { useAck = true, skipCache = false } = options;
103
+
104
+ const VALID_ACTIONS = ['pin', 'unpin', 'list'];
105
+ if (!VALID_ACTIONS.includes(action)) {
106
+ throw new Error(`pinMessage: invalid action "${action}". Use: ${VALID_ACTIONS.join(', ')}`);
107
+ }
108
+
109
+ if (action === 'list') {
110
+ if (!threadID) throw new Error('pinMessage: "list" requires threadID');
111
+
112
+ const cacheKey = `pins_${threadID}`;
113
+ if (!skipCache) {
114
+ const cached = PIN_CACHE.get(cacheKey);
115
+ if (cached && Date.now() - cached.ts < PIN_CACHE_TTL) {
116
+ return { ...cached.result, fromCache: true };
117
+ }
118
+ }
119
+
120
+ if (INFLIGHT_PINS.has(cacheKey)) return INFLIGHT_PINS.get(cacheKey);
121
+
122
+ const inflightPromise = (async () => {
123
+ try {
124
+ const url = `https://www.facebook.com/messages/t/${threadID}/`;
125
+ const allJsonData = await retryOp(() => utils.json(url, ctx.jar, null, ctx.globalOptions, ctx));
126
+
127
+ const extracted = extractAndSearchLightspeedRequest(allJsonData);
128
+ if (!extracted || !extracted.lightReq) {
129
+ utils.warn('pinMessage', `No pinned messages found for thread ${threadID}`);
130
+ return { threadID: String(threadID), pins: [], count: 0, timestamp: Date.now() };
131
+ }
132
+
133
+ const result = {
134
+ threadID: String(threadID),
135
+ lightspeedData: extracted.lightReq,
136
+ pinnedCommands: extracted.pinnedCommands,
137
+ count: extracted.commandCount,
138
+ hasPins: extracted.commandCount > 0,
139
+ timestamp: Date.now()
140
+ };
141
+
142
+ PIN_CACHE.set(cacheKey, { result, ts: Date.now() });
143
+ recordHistory({ action: 'list', threadID: String(threadID), pinCount: extracted.commandCount });
144
+ return result;
145
+ } finally {
146
+ INFLIGHT_PINS.delete(cacheKey);
147
+ }
148
+ })();
149
+
150
+ INFLIGHT_PINS.set(cacheKey, inflightPromise);
151
+ return inflightPromise;
152
+ }
153
+
154
+ if (!ctx.mqttClient) throw new Error('pinMessage: MQTT not connected (start listening first)');
155
+ if (!threadID || !messageID) throw new Error(`pinMessage: "${action}" requires both threadID and messageID`);
156
+
157
+ await globalShield.addSmartDelay();
158
+
159
+ PIN_CACHE.delete(`pins_${threadID}`);
160
+
161
+ let results;
162
+
163
+ if (action === 'pin') {
164
+ const pinTask = {
165
+ label: '430',
166
+ payload: JSON.stringify({ thread_key: String(threadID), message_id: String(messageID), timestamp_ms: Date.now() }),
167
+ queue_name: `pin_msg_v2_${threadID}`,
168
+ task_id: (ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1)
169
+ };
170
+ const searchTask = {
171
+ label: '751',
172
+ payload: JSON.stringify({ thread_key: String(threadID), message_id: String(messageID), pinned_message_state: 1 }),
173
+ queue_name: 'set_pinned_message_search',
174
+ task_id: (ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1)
175
+ };
176
+
177
+ const publish = useAck ? mqttPublishWithAck : (c) => new Promise((res, rej) => {
178
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(c), { qos: 1, retain: false }, e => e ? rej(e) : res({ success: true, request_id: c.request_id }));
179
+ });
180
+
181
+ results = await Promise.all([
182
+ retryOp(() => publish(ctx, buildPinRequest(ctx, [pinTask], 0))),
183
+ retryOp(() => publish(ctx, buildPinRequest(ctx, [searchTask], 1)))
184
+ ]);
185
+
186
+ recordHistory({ action: 'pin', threadID: String(threadID), messageID: String(messageID) });
187
+ utils.log('pinMessage', `Pinned message ${messageID} in thread ${threadID}`);
188
+ } else if (action === 'unpin') {
189
+ const searchTask1 = {
190
+ label: '751',
191
+ payload: JSON.stringify({ thread_key: String(threadID), message_id: String(messageID), pinned_message_state: 0 }),
192
+ queue_name: 'set_pinned_message_search',
193
+ task_id: (ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1)
194
+ };
195
+ const unpinTask = {
196
+ label: '431',
197
+ payload: JSON.stringify({ thread_key: String(threadID), message_id: String(messageID), timestamp_ms: Date.now() }),
198
+ queue_name: `unpin_msg_v2_${threadID}`,
199
+ task_id: (ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1)
200
+ };
201
+ const searchTask2 = {
202
+ label: '751',
203
+ payload: JSON.stringify({ thread_key: String(threadID), message_id: String(messageID), pinned_message_state: 0 }),
204
+ queue_name: 'set_pinned_message_search',
205
+ task_id: (ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1)
206
+ };
207
+
208
+ const publish = useAck ? (c) => mqttPublishWithAck(ctx, c) : (c) => new Promise((res, rej) => {
209
+ ctx.mqttClient.publish('/ls_req', JSON.stringify(c), { qos: 1, retain: false }, e => e ? rej(e) : res({ success: true, request_id: c.request_id }));
210
+ });
211
+
212
+ await retryOp(() => publish(buildPinRequest(ctx, [searchTask1], 0)));
213
+ await retryOp(() => publish(buildPinRequest(ctx, [unpinTask], 1)));
214
+ results = [await retryOp(() => publish(buildPinRequest(ctx, [searchTask2], 2)))];
215
+
216
+ recordHistory({ action: 'unpin', threadID: String(threadID), messageID: String(messageID) });
217
+ utils.log('pinMessage', `Unpinned message ${messageID} in thread ${threadID}`);
218
+ }
219
+
220
+ return {
221
+ success: true,
222
+ action,
223
+ threadID: String(threadID),
224
+ messageID: String(messageID),
225
+ publishResults: results,
226
+ timestamp: Date.now()
227
+ };
228
+ };
229
+
230
+ Object.assign(module.exports, {
231
+ getHistory: (limit = 50) => PIN_HISTORY.slice(0, limit),
232
+ clearCache: (threadID) => {
233
+ if (threadID) PIN_CACHE.delete(`pins_${threadID}`);
234
+ else PIN_CACHE.clear();
235
+ return { success: true };
236
+ }
237
+ });
238
+ };