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,395 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Sticker API Module — Advanced for Phantom SDK
5
+ *
6
+ * Provides access to Facebook's GraphQL-based sticker endpoints.
7
+ * Made by @ChoruOfficial — massively extended for phantom-fca
8
+ *
9
+ * Features:
10
+ * - Sticker search with pagination support
11
+ * - Pack listing, store browsing, and full auto-pagination
12
+ * - AI-generated & trending sticker fetching
13
+ * - Individual sticker metadata resolution
14
+ * - Pack add/remove management
15
+ * - In-memory caching (5 minutes)
16
+ * - Exponential-backoff retry on transient GraphQL errors
17
+ * - Parallel batch sticker resolution
18
+ * - Recent sticker tray management
19
+ */
20
+
21
+ const utils = require('../datastore/models/matrix/tools');
22
+
23
+ const _cache = new Map();
24
+ const CACHE_TTL = 5 * 60 * 1000;
25
+
26
+ // ── Shared helpers ────────────────────────────────────────────────────────────
27
+ function cached(key, fn) {
28
+ const hit = _cache.get(key);
29
+ if (hit && Date.now() - hit.ts < CACHE_TTL) return Promise.resolve(hit.data);
30
+ return fn().then(data => { _cache.set(key, { data, ts: Date.now() }); return data; });
31
+ }
32
+
33
+ function normalizeSticker(node) {
34
+ if (!node) return null;
35
+ return {
36
+ type: 'sticker',
37
+ ID: node.id,
38
+ stickerID: node.id,
39
+ url: node.image?.uri || node.url || null,
40
+ animatedUrl: node.animated_image?.uri || node.animatedUrl || null,
41
+ spriteUrl: node.sprite_image?.uri || null,
42
+ packID: node.pack?.id || node.packID || null,
43
+ packName: node.pack?.name || null,
44
+ label: node.label || node.accessibility_label || null,
45
+ width: node.image?.width || null,
46
+ height: node.image?.height || null,
47
+ isAnimated: !!(node.animated_image?.uri),
48
+ };
49
+ }
50
+
51
+ function normalizePack(node) {
52
+ if (!node) return null;
53
+ return {
54
+ id: node.id,
55
+ name: node.name,
56
+ thumbnail: node.thumbnail_image?.uri || null,
57
+ stickerCount: node.sticker_count || null,
58
+ isOwned: node.is_owned !== undefined ? node.is_owned : null,
59
+ isNew: node.is_new || false,
60
+ };
61
+ }
62
+
63
+ // ── Formatters ────────────────────────────────────────────────────────────────
64
+ function formatPackList(data) {
65
+ const trayPacks = data?.data?.picker_plugins?.sticker_picker?.sticker_store?.tray_packs;
66
+ const storePacks = data?.data?.viewer?.sticker_store?.available_packs;
67
+ const packData = storePacks || trayPacks;
68
+
69
+ if (!packData) return { packs: [], page_info: { has_next_page: false }, store_id: null };
70
+
71
+ const edges = Array.isArray(packData) ? packData : packData.edges || [];
72
+ return {
73
+ packs: edges.map(e => normalizePack(e.node || e)).filter(Boolean),
74
+ page_info: packData.page_info || { has_next_page: false },
75
+ store_id: data?.data?.viewer?.sticker_store?.id || null,
76
+ };
77
+ }
78
+
79
+ function formatStickerSearchResults(data) {
80
+ const edges = data?.data?.sticker_search?.sticker_results?.edges || [];
81
+ return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
82
+ }
83
+
84
+ function formatStickerPackResults(data) {
85
+ const edges = data?.data?.sticker_pack?.stickers?.edges || [];
86
+ return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
87
+ }
88
+
89
+ function formatAiStickers(data) {
90
+ const nodes = data?.data?.xfb_trending_generated_ai_stickers?.nodes || [];
91
+ return nodes.map(n => normalizeSticker(n)).filter(Boolean);
92
+ }
93
+
94
+ function formatRecentStickers(data) {
95
+ const edges = data?.data?.viewer?.recent_stickers?.edges || [];
96
+ return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
97
+ }
98
+
99
+ function formatTrayStickers(data) {
100
+ const plugins = data?.data?.picker_plugins?.sticker_picker?.recent_stickers;
101
+ if (!plugins) return [];
102
+ const stickers = Array.isArray(plugins) ? plugins : plugins.edges?.map(e => e.node) || [];
103
+ return stickers.map(normalizeSticker).filter(Boolean);
104
+ }
105
+
106
+ // ── Module export ─────────────────────────────────────────────────────────────
107
+ module.exports = function (defaultFuncs, api, ctx) {
108
+
109
+ // ── Request helper with retry ─────────────────────────────────────────────
110
+ async function makeRequest(form, retries = 3, baseMs = 500) {
111
+ let lastErr;
112
+ for (let attempt = 1; attempt <= retries; attempt++) {
113
+ try {
114
+ const resData = await defaultFuncs
115
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
116
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
117
+
118
+ if (!resData) throw new Error("GraphQL returned no data");
119
+ if (resData.errors) {
120
+ const msg = resData.errors[0]?.message || JSON.stringify(resData.errors);
121
+ throw Object.assign(new Error(msg), { isFatal: /auth|permission|checkpoint/i.test(msg) });
122
+ }
123
+ return resData;
124
+ } catch (err) {
125
+ lastErr = err;
126
+ if (err.isFatal || attempt >= retries) throw err;
127
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
128
+ utils.warn("StickerAPI", `Retry ${attempt}/${retries} in ${Math.round(wait)}ms`);
129
+ await new Promise(r => setTimeout(r, wait));
130
+ }
131
+ }
132
+ throw lastErr;
133
+ }
134
+
135
+ return {
136
+
137
+ /**
138
+ * Search for stickers by keyword.
139
+ * @param {string} query
140
+ * @param {object} [options] { limit, width, height }
141
+ * @returns {Promise<Array>}
142
+ */
143
+ search: async function (query, options = {}) {
144
+ if (!query) throw new Error("stickers.search: query is required");
145
+ const w = options.width || 128;
146
+ const h = options.height || 128;
147
+ const form = {
148
+ fb_api_caller_class: 'RelayModern',
149
+ fb_api_req_friendly_name: 'CometStickerPickerSearchResultsRootQuery',
150
+ variables: JSON.stringify({
151
+ scale: 3,
152
+ search_query: query.trim(),
153
+ sticker_height: h,
154
+ sticker_width: w,
155
+ stickerInterface: "MESSAGES",
156
+ }),
157
+ doc_id: '24004987559125954',
158
+ };
159
+ const res = await makeRequest(form);
160
+ let stickers = formatStickerSearchResults(res);
161
+ if (options.limit) stickers = stickers.slice(0, options.limit);
162
+ return stickers;
163
+ },
164
+
165
+ /**
166
+ * List the user's installed sticker packs (tray).
167
+ * @returns {Promise<Array>}
168
+ */
169
+ listPacks: async function () {
170
+ return cached('listPacks', async () => {
171
+ const form = {
172
+ fb_api_caller_class: 'RelayModern',
173
+ fb_api_req_friendly_name: 'CometStickerPickerCardQuery',
174
+ variables: JSON.stringify({ scale: 3, stickerInterface: "MESSAGES" }),
175
+ doc_id: '10095807770482952',
176
+ };
177
+ const res = await makeRequest(form);
178
+ return formatPackList(res).packs;
179
+ });
180
+ },
181
+
182
+ /**
183
+ * Get all available sticker packs from the store (fully auto-paginated).
184
+ * @param {object} [options] { maxPages }
185
+ * @returns {Promise<Array>}
186
+ */
187
+ getStorePacks: async function (options = {}) {
188
+ const maxPages = options.maxPages || 50;
189
+ utils.log("StickerAPI", "Fetching all store packs (auto-paginated)...");
190
+ let allPacks = [], page = 0;
191
+
192
+ const firstForm = {
193
+ fb_api_caller_class: 'RelayModern',
194
+ fb_api_req_friendly_name: 'CometStickersStoreDialogQuery',
195
+ variables: JSON.stringify({}),
196
+ doc_id: '29237828849196584',
197
+ };
198
+ let res = await makeRequest(firstForm);
199
+ let { packs, page_info, store_id } = formatPackList(res);
200
+ allPacks.push(...packs);
201
+ page++;
202
+
203
+ while (page_info?.has_next_page && page < maxPages) {
204
+ const nextForm = {
205
+ fb_api_caller_class: 'RelayModern',
206
+ fb_api_req_friendly_name: 'CometStickersStorePackListPaginationQuery',
207
+ variables: JSON.stringify({ count: 20, cursor: page_info.end_cursor, id: store_id }),
208
+ doc_id: '9898634630218439',
209
+ };
210
+ res = await makeRequest(nextForm);
211
+ const next = formatPackList(res);
212
+ allPacks.push(...next.packs);
213
+ page_info = next.page_info;
214
+ page++;
215
+ }
216
+
217
+ utils.log("StickerAPI", `Fetched ${allPacks.length} store packs in ${page} pages`);
218
+ return allPacks;
219
+ },
220
+
221
+ /**
222
+ * Merge user's tray packs with store packs into a deduplicated list.
223
+ * @returns {Promise<Array>}
224
+ */
225
+ listAllPacks: async function () {
226
+ const [myPacks, storePacks] = await Promise.all([this.listPacks(), this.getStorePacks()]);
227
+ const map = new Map();
228
+ [...myPacks, ...storePacks].forEach(p => map.set(p.id, p));
229
+ return Array.from(map.values());
230
+ },
231
+
232
+ /**
233
+ * Add a sticker pack by ID.
234
+ * @param {string} packID
235
+ * @returns {Promise<Object>}
236
+ */
237
+ addPack: async function (packID) {
238
+ if (!packID) throw new Error("stickers.addPack: packID is required");
239
+ const form = {
240
+ fb_api_caller_class: 'RelayModern',
241
+ fb_api_req_friendly_name: 'CometStickersStorePackMutationAddMutation',
242
+ variables: JSON.stringify({
243
+ input: {
244
+ pack_id: packID,
245
+ actor_id: ctx.userID,
246
+ client_mutation_id: String(Date.now() % 1e9),
247
+ }
248
+ }),
249
+ doc_id: '9877489362345320',
250
+ };
251
+ const res = await makeRequest(form);
252
+ return normalizePack(res.data?.sticker_pack_add?.sticker_pack) || res.data;
253
+ },
254
+
255
+ /**
256
+ * Remove a sticker pack by ID.
257
+ * @param {string} packID
258
+ * @returns {Promise<Object>}
259
+ */
260
+ removePack: async function (packID) {
261
+ if (!packID) throw new Error("stickers.removePack: packID is required");
262
+ const form = {
263
+ fb_api_caller_class: 'RelayModern',
264
+ fb_api_req_friendly_name: 'CometStickersStorePackMutationRemoveMutation',
265
+ variables: JSON.stringify({
266
+ input: {
267
+ pack_id: packID,
268
+ actor_id: ctx.userID,
269
+ client_mutation_id: String(Date.now() % 1e9),
270
+ }
271
+ }),
272
+ doc_id: '25244534060527988',
273
+ };
274
+ const res = await makeRequest(form);
275
+ return res.data;
276
+ },
277
+
278
+ /**
279
+ * Get all stickers inside a specific pack.
280
+ * @param {string} packID
281
+ * @param {object} [options] { width, height }
282
+ * @returns {Promise<Array>}
283
+ */
284
+ getStickersInPack: async function (packID, options = {}) {
285
+ if (!packID) throw new Error("stickers.getStickersInPack: packID is required");
286
+ return cached(`pack::${packID}`, async () => {
287
+ const form = {
288
+ fb_api_caller_class: 'RelayModern',
289
+ fb_api_req_friendly_name: 'CometStickerPickerPackContentRootQuery',
290
+ variables: JSON.stringify({
291
+ packID,
292
+ stickerWidth: options.width || 128,
293
+ stickerHeight: options.height || 128,
294
+ scale: 3,
295
+ }),
296
+ doc_id: '23982341384707469',
297
+ };
298
+ const res = await makeRequest(form);
299
+ return formatStickerPackResults(res);
300
+ });
301
+ },
302
+
303
+ /**
304
+ * Get trending AI-generated stickers.
305
+ * @param {object} [options] { limit }
306
+ * @returns {Promise<Array>}
307
+ */
308
+ getAiStickers: async function ({ limit = 20 } = {}) {
309
+ return cached(`ai_stickers::${limit}`, async () => {
310
+ const form = {
311
+ fb_api_caller_class: 'RelayModern',
312
+ fb_api_req_friendly_name: 'CometStickerPickerStickerGeneratedCardQuery',
313
+ variables: JSON.stringify({ limit }),
314
+ doc_id: '24151467751156443',
315
+ };
316
+ const res = await makeRequest(form);
317
+ return formatAiStickers(res);
318
+ });
319
+ },
320
+
321
+ /**
322
+ * Get the user's recent stickers.
323
+ * @param {object} [options] { limit }
324
+ * @returns {Promise<Array>}
325
+ */
326
+ getRecentStickers: async function ({ limit = 30 } = {}) {
327
+ const form = {
328
+ fb_api_caller_class: 'RelayModern',
329
+ fb_api_req_friendly_name: 'CometStickerPickerRecentStickersQuery',
330
+ variables: JSON.stringify({ scale: 3, stickerInterface: "MESSAGES" }),
331
+ doc_id: '10095807770482952',
332
+ };
333
+ const res = await makeRequest(form);
334
+ const tray = formatTrayStickers(res);
335
+ return tray.slice(0, limit);
336
+ },
337
+
338
+ /**
339
+ * Resolve full metadata for one or many sticker IDs in parallel.
340
+ * @param {string|string[]} stickerIDs
341
+ * @returns {Promise<object|object[]>}
342
+ */
343
+ resolveSticker: async function (stickerIDs) {
344
+ const ids = Array.isArray(stickerIDs) ? stickerIDs : [stickerIDs];
345
+ const form = {
346
+ fb_api_caller_class: 'RelayModern',
347
+ fb_api_req_friendly_name: 'CometStickerPickerStickerQuery',
348
+ variables: JSON.stringify({ stickerIDs: ids, scale: 3 }),
349
+ doc_id: '9580756598638278',
350
+ };
351
+ try {
352
+ const res = await makeRequest(form);
353
+ const nodes = (res?.data?.stickers || ids.map(() => null));
354
+ const resolved = (Array.isArray(nodes) ? nodes : Object.values(nodes))
355
+ .map(n => normalizeSticker(n)).filter(Boolean);
356
+ return ids.length === 1 ? (resolved[0] || null) : resolved;
357
+ } catch {
358
+ // Fallback: return minimal objects
359
+ const fallback = ids.map(id => ({ type: 'sticker', ID: id, stickerID: id }));
360
+ return ids.length === 1 ? fallback[0] : fallback;
361
+ }
362
+ },
363
+
364
+ /**
365
+ * Generate an AI sticker from a text prompt.
366
+ * @param {string} prompt
367
+ * @param {object} [options] { numResults }
368
+ * @returns {Promise<Array>}
369
+ */
370
+ generateAiSticker: async function (prompt, { numResults = 4 } = {}) {
371
+ if (!prompt) throw new Error("stickers.generateAiSticker: prompt is required");
372
+ const form = {
373
+ av: ctx.userID,
374
+ __user: ctx.userID,
375
+ fb_dtsg: ctx.fb_dtsg,
376
+ fb_api_caller_class: 'RelayModern',
377
+ fb_api_req_friendly_name: 'useGenerateAIStickerMutation',
378
+ variables: JSON.stringify({
379
+ input: {
380
+ client_mutation_id: String(Date.now() % 1e9),
381
+ actor_id: ctx.userID,
382
+ num_results: Math.min(numResults, 8),
383
+ prompt: prompt.trim(),
384
+ }
385
+ }),
386
+ server_timestamps: true,
387
+ doc_id: '6774614342659830',
388
+ };
389
+ const res = await makeRequest(form);
390
+ const stickers = res?.data?.xfb_generate_ai_sticker?.stickers || [];
391
+ return stickers.map(normalizeSticker).filter(Boolean);
392
+ },
393
+
394
+ };
395
+ };
@@ -0,0 +1,240 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+ const { URL } = require('url');
5
+ const { globalShield } = require('../datastore/models/matrix/ghost');
6
+
7
+ const STORY_CACHE = new Map();
8
+ const STORY_CACHE_TTL = 5 * 60 * 1000;
9
+ const STORY_HISTORY = [];
10
+ const MAX_HISTORY = 300;
11
+
12
+ const ALLOWED_REACTIONS = new Set(['❤️', '👍', '🤗', '😆', '😡', '😢', '😮']);
13
+
14
+ const FONT_MAP = {
15
+ headline: '1919119914775364',
16
+ classic: '516266749248495',
17
+ casual: '516266749248495',
18
+ fancy: '1790435664339626',
19
+ bold: '1919119914775364'
20
+ };
21
+
22
+ const BG_MAP = {
23
+ orange: '2163607613910521',
24
+ blue: '401372137331149',
25
+ green: '367314917184744',
26
+ modern: '554617635055752',
27
+ dark: '2163607613910521',
28
+ light: '401372137331149'
29
+ };
30
+
31
+ async function retryOp(fn, retries = 4, base = 600) {
32
+ for (let i = 0; i < retries; i++) {
33
+ try { return await fn(); } catch (err) {
34
+ if (i === retries - 1) throw err;
35
+ const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
36
+ if (!transient) throw err;
37
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
38
+ }
39
+ }
40
+ }
41
+
42
+ function extractStoryIDFromURL(url) {
43
+ try {
44
+ const u = new URL(url);
45
+ const parts = u.pathname.split('/');
46
+ const idx = parts.indexOf('stories');
47
+ if (idx !== -1 && parts.length > idx + 2) return parts[idx + 2];
48
+ const storyParam = u.searchParams.get('story_fbid');
49
+ if (storyParam) return storyParam;
50
+ return null;
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function resolveStoryID(storyIdOrUrl) {
57
+ if (!storyIdOrUrl) throw new Error('story: storyID or URL is required');
58
+ const extracted = extractStoryIDFromURL(storyIdOrUrl);
59
+ return extracted || storyIdOrUrl;
60
+ }
61
+
62
+ function recordHistory(op) {
63
+ STORY_HISTORY.unshift({ ...op, ts: Date.now() });
64
+ if (STORY_HISTORY.length > MAX_HISTORY) STORY_HISTORY.length = MAX_HISTORY;
65
+ }
66
+
67
+ module.exports = function (defaultFuncs, api, ctx) {
68
+
69
+ async function sendStoryReply(storyIdOrUrl, message, isReaction, options = {}) {
70
+ const storyID = resolveStoryID(storyIdOrUrl);
71
+ const { retry = true, timeout = 15000 } = options;
72
+
73
+ if (!message) throw new Error('story: message or reaction is required');
74
+
75
+ if (isReaction && !ALLOWED_REACTIONS.has(message)) {
76
+ throw new Error(`story: invalid reaction "${message}". Allowed: ${Array.from(ALLOWED_REACTIONS).join(' ')}`);
77
+ }
78
+
79
+ const variables = {
80
+ input: {
81
+ attribution_id_v2: 'StoriesCometSuspenseRoot.react,comet.stories.viewer,via_cold_start',
82
+ message,
83
+ story_id: storyID,
84
+ story_reply_type: isReaction ? 'LIGHT_WEIGHT' : 'TEXT',
85
+ actor_id: ctx.userID,
86
+ client_mutation_id: String(Math.floor(Math.random() * 10 + 1))
87
+ }
88
+ };
89
+
90
+ if (isReaction) {
91
+ variables.input.lightweight_reaction_actions = { offsets: [0], reaction: message };
92
+ }
93
+
94
+ const form = {
95
+ av: ctx.userID,
96
+ __user: ctx.userID,
97
+ __a: '1',
98
+ fb_dtsg: ctx.fb_dtsg,
99
+ jazoest: ctx.jazoest,
100
+ fb_api_caller_class: 'RelayModern',
101
+ fb_api_req_friendly_name: 'useStoriesSendReplyMutation',
102
+ variables: JSON.stringify(variables),
103
+ doc_id: '9697491553691692'
104
+ };
105
+
106
+ await globalShield.addSmartDelay();
107
+
108
+ const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
109
+ const res = retry ? await retryOp(exec) : await exec();
110
+
111
+ if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
112
+
113
+ const storyReplyData = res.data?.data?.direct_message_reply;
114
+ if (!storyReplyData) throw new Error('story: "direct_message_reply" not found in response');
115
+
116
+ recordHistory({ action: isReaction ? 'react' : 'reply', storyID, message });
117
+ return { success: true, storyID, [isReaction ? 'reaction' : 'message']: message, result: storyReplyData, timestamp: Date.now() };
118
+ }
119
+
120
+ async function create(message, fontName = 'classic', backgroundName = 'blue', options = {}) {
121
+ const { retry = true } = options;
122
+ if (!message || typeof message !== 'string') throw new Error('story.create: message must be a non-empty string');
123
+
124
+ const fontId = FONT_MAP[fontName.toLowerCase()] || FONT_MAP.classic;
125
+ const bgId = BG_MAP[backgroundName.toLowerCase()] || BG_MAP.blue;
126
+
127
+ const variables = {
128
+ input: {
129
+ audiences: [{ stories: { self: { target_id: ctx.userID } } }],
130
+ audiences_is_complete: true,
131
+ logging: { composer_session_id: `createStoriesText-${Date.now()}` },
132
+ navigation_data: { attribution_id_v2: 'StoriesCreateRoot.react,comet.stories.create' },
133
+ source: 'WWW',
134
+ message: { ranges: [], text: message },
135
+ text_format_metadata: { inspirations_custom_font_id: fontId },
136
+ text_format_preset_id: bgId,
137
+ tracking: [null],
138
+ actor_id: ctx.userID,
139
+ client_mutation_id: String(Math.floor(Math.random() * 9999))
140
+ }
141
+ };
142
+
143
+ const form = {
144
+ __a: '1',
145
+ fb_dtsg: ctx.fb_dtsg,
146
+ jazoest: ctx.jazoest,
147
+ fb_api_caller_class: 'RelayModern',
148
+ fb_api_req_friendly_name: 'StoriesCreateMutation',
149
+ variables: JSON.stringify(variables),
150
+ doc_id: '24226878183562473'
151
+ };
152
+
153
+ await globalShield.addSmartDelay();
154
+
155
+ const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
156
+ const res = retry ? await retryOp(exec) : await exec();
157
+
158
+ if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
159
+
160
+ const storyNode = res.data?.data?.story_create?.viewer?.actor?.story_bucket?.nodes?.[0]?.first_story_to_show;
161
+ if (!storyNode?.id) throw new Error('story.create: storyID not found in response');
162
+
163
+ recordHistory({ action: 'create', message: message.slice(0, 80), font: fontName, bg: backgroundName, storyID: storyNode.id });
164
+ utils.log('story.create', `Story created: ${storyNode.id}`);
165
+
166
+ return { success: true, storyID: storyNode.id, font: fontName, background: backgroundName, timestamp: Date.now() };
167
+ }
168
+
169
+ async function getStory(storyIdOrUrl, options = {}) {
170
+ const storyID = resolveStoryID(storyIdOrUrl);
171
+ const { skipCache = false } = options;
172
+ const cacheKey = `story_${storyID}`;
173
+
174
+ if (!skipCache) {
175
+ const cached = STORY_CACHE.get(cacheKey);
176
+ if (cached && Date.now() - cached.ts < STORY_CACHE_TTL) return { ...cached.result, fromCache: true };
177
+ }
178
+
179
+ const form = {
180
+ av: ctx.userID,
181
+ fb_api_caller_class: 'RelayModern',
182
+ fb_api_req_friendly_name: 'StoriesRootQuery',
183
+ variables: JSON.stringify({ story_bucket_id: storyID }),
184
+ doc_id: '9781019695323982'
185
+ };
186
+
187
+ const res = await retryOp(() => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {}));
188
+ if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
189
+
190
+ const result = { success: true, storyID, data: res.data, timestamp: Date.now() };
191
+ STORY_CACHE.set(cacheKey, { result, ts: Date.now() });
192
+ return result;
193
+ }
194
+
195
+ async function deleteStory(storyID, options = {}) {
196
+ if (!storyID) throw new Error('story.delete: storyID is required');
197
+ const { retry = true } = options;
198
+
199
+ const form = {
200
+ av: ctx.userID,
201
+ __user: ctx.userID,
202
+ fb_dtsg: ctx.fb_dtsg,
203
+ fb_api_caller_class: 'RelayModern',
204
+ fb_api_req_friendly_name: 'StoriesDeleteMutation',
205
+ variables: JSON.stringify({
206
+ input: {
207
+ story_id: String(storyID),
208
+ actor_id: ctx.userID,
209
+ client_mutation_id: String(Math.floor(Math.random() * 9999))
210
+ }
211
+ }),
212
+ doc_id: '5697804170303568'
213
+ };
214
+
215
+ await globalShield.addSmartDelay();
216
+
217
+ const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
218
+ const res = retry ? await retryOp(exec) : await exec();
219
+ if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
220
+
221
+ STORY_CACHE.delete(`story_${storyID}`);
222
+ recordHistory({ action: 'delete', storyID });
223
+ return { success: true, storyID, timestamp: Date.now() };
224
+ }
225
+
226
+ return {
227
+ create,
228
+ react: (storyIdOrUrl, reaction, options) => sendStoryReply(storyIdOrUrl, reaction, true, options),
229
+ msg: (storyIdOrUrl, message, options) => sendStoryReply(storyIdOrUrl, message, false, options),
230
+ reply: (storyIdOrUrl, message, options) => sendStoryReply(storyIdOrUrl, message, false, options),
231
+ get: getStory,
232
+ delete: deleteStory,
233
+ extractID: extractStoryIDFromURL,
234
+ getHistory: (limit = 50) => STORY_HISTORY.slice(0, limit),
235
+ clearCache: () => { STORY_CACHE.clear(); return { success: true }; },
236
+ allowedReactions: Array.from(ALLOWED_REACTIONS),
237
+ fonts: Object.keys(FONT_MAP),
238
+ backgrounds: Object.keys(BG_MAP)
239
+ };
240
+ };