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,333 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * createAITheme — Advanced AI Theme Generation for Phantom SDK
5
+ * Generates Facebook Messenger AI-powered themes from natural-language prompts.
6
+ *
7
+ * Features:
8
+ * - Automatic prompt enrichment with context-aware suffixes
9
+ * - In-memory response cache (5-minute TTL, keyed by prompt+count)
10
+ * - Exponential-backoff retry on transient network failures
11
+ * - Parallel dual-batch generation for large numThemes requests
12
+ * - Deep theme normalisation: preview URLs, gradient extraction, dark-mode detection
13
+ * - Color-harmony accessibility scoring (WCAG contrast ratio)
14
+ * - Rank ordering by accessibility score
15
+ * - Full callback + Promise dual API
16
+ *
17
+ * @by Allou Mohamed (original) — massively extended for phantom-fca
18
+ */
19
+
20
+ const utils = require('../datastore/models/matrix/tools');
21
+
22
+ // ── In-memory cache ──────────────────────────────────────────────────────────
23
+ const _cache = new Map();
24
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
25
+
26
+ // ── Prompt enrichment table ──────────────────────────────────────────────────
27
+ const PROMPT_ENRICHERS = [
28
+ { re: /\bnature\b/i, add: ", lush greenery, organic textures, earthy tones" },
29
+ { re: /\bocean|sea\b/i, add: ", deep blues, aqua gradients, bioluminescent wave patterns" },
30
+ { re: /\bspace|galaxy\b/i, add: ", cosmic purples, starfield depth, nebula glow, zero-gravity" },
31
+ { re: /\bsunset\b/i, add: ", warm amber, golden-hour glow, gradient horizon fade" },
32
+ { re: /\bneon\b/i, add: ", cyberpunk aesthetic, high-contrast vivid saturation, grid glow" },
33
+ { re: /\bminimal\b/i, add: ", clean whitespace, muted tones, geometric precision" },
34
+ { re: /\bvintage\b/i, add: ", muted sepia, retro film-grain palette, nostalgic warmth" },
35
+ { re: /\bforest\b/i, add: ", deep canopy greens, dappled forest light, mossy hues" },
36
+ { re: /\bfire|flame\b/i, add: ", molten ember reds, scorched black, dynamic orange heat" },
37
+ { re: /\bpastel\b/i, add: ", soft blush rose, lavender mist, peachy cloud tones" },
38
+ { re: /\bcity|urban\b/i, add: ", concrete greys, skyline neon, metropolitan night glow" },
39
+ { re: /\bwinter|snow\b/i, add: ", icy blue-white, frosted crystal clarity, cold silence" },
40
+ { re: /\bspring|bloom\b/i, add: ", fresh petal pink, verdant growth, dewdrop luminosity" },
41
+ { re: /\boutdoor|adventure\b/i, add: ", rugged terrain, horizon distance, open-sky freedom" },
42
+ ];
43
+
44
+ function enrichPrompt(raw) {
45
+ if (!raw || typeof raw !== 'string') return raw;
46
+ let out = raw.trim();
47
+ for (const { re, add } of PROMPT_ENRICHERS) {
48
+ if (re.test(out) && !out.toLowerCase().includes(add.slice(2, 14))) {
49
+ out += add;
50
+ break;
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+
56
+ // ── Color helpers ────────────────────────────────────────────────────────────
57
+ function hexToRgb(hex) {
58
+ if (!hex || typeof hex !== 'string') return null;
59
+ const c = hex.replace(/^#/, '').replace(/^[Ff]{2}/, '');
60
+ if (c.length !== 6) return null;
61
+ return {
62
+ r: parseInt(c.slice(0, 2), 16),
63
+ g: parseInt(c.slice(2, 4), 16),
64
+ b: parseInt(c.slice(4, 6), 16),
65
+ };
66
+ }
67
+
68
+ function relativeLuminance({ r, g, b }) {
69
+ const lin = v => {
70
+ v /= 255;
71
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
72
+ };
73
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
74
+ }
75
+
76
+ function isDark(hex) {
77
+ const rgb = hexToRgb(hex);
78
+ if (!rgb) return null;
79
+ return relativeLuminance(rgb) < 0.179;
80
+ }
81
+
82
+ function wcagContrast(hex1, hex2) {
83
+ const r1 = hexToRgb(hex1), r2 = hexToRgb(hex2);
84
+ if (!r1 || !r2) return null;
85
+ const L1 = relativeLuminance(r1), L2 = relativeLuminance(r2);
86
+ const [hi, lo] = L1 > L2 ? [L1, L2] : [L2, L1];
87
+ return +((hi + 0.05) / (lo + 0.05)).toFixed(2);
88
+ }
89
+
90
+ // ── Theme accessibility scoring (0–100) ──────────────────────────────────────
91
+ function scoreTheme(t) {
92
+ let s = 40;
93
+ const grads = t.gradient_colors || [];
94
+
95
+ if (grads.length >= 2) s += 10;
96
+ if (grads.length >= 3) s += 5;
97
+ if (t.preview_image_urls?.light_mode) s += 15;
98
+ if (t.preview_image_urls?.dark_mode && t.preview_image_urls.dark_mode !== t.preview_image_urls.light_mode) s += 8;
99
+ if (t.alternative_themes?.length) s += 7;
100
+ if (t.background_asset) s += 5;
101
+
102
+ if (grads.length >= 2) {
103
+ const ratio = wcagContrast(grads[0], grads[grads.length - 1]);
104
+ if (ratio !== null) {
105
+ if (ratio > 3) s += 5;
106
+ if (ratio > 4.5) s += 5;
107
+ if (ratio > 7) s += 5;
108
+ }
109
+ }
110
+
111
+ return Math.min(100, s);
112
+ }
113
+
114
+ // ── URL extraction ───────────────────────────────────────────────────────────
115
+ function extractUrl(obj) {
116
+ if (!obj) return null;
117
+ if (typeof obj === 'string') return obj;
118
+ return obj.uri || obj.url || null;
119
+ }
120
+
121
+ // ── Gradient extraction ──────────────────────────────────────────────────────
122
+ function extractGradient(theme) {
123
+ const raw = theme.gradient_colors || theme.gradient || theme.colors;
124
+ if (!raw) return [];
125
+ if (Array.isArray(raw)) return raw;
126
+ if (typeof raw === 'string') {
127
+ try { return JSON.parse(raw); } catch { return [raw]; }
128
+ }
129
+ return [];
130
+ }
131
+
132
+ // ── Full theme normalisation ─────────────────────────────────────────────────
133
+ function normalizeTheme(theme, index) {
134
+ const t = { ...theme };
135
+
136
+ // Preview images
137
+ let lightUrl = null, darkUrl = null;
138
+ const previewSrc = t.preview_image_urls || t.preview_images || t.preview_urls;
139
+
140
+ if (previewSrc) {
141
+ if (typeof previewSrc === 'string') {
142
+ lightUrl = darkUrl = previewSrc;
143
+ } else if (Array.isArray(previewSrc)) {
144
+ lightUrl = extractUrl(previewSrc[0]);
145
+ darkUrl = extractUrl(previewSrc[1]) || lightUrl;
146
+ } else {
147
+ lightUrl = extractUrl(previewSrc.light_mode) || extractUrl(previewSrc.light);
148
+ darkUrl = extractUrl(previewSrc.dark_mode) || extractUrl(previewSrc.dark);
149
+ }
150
+ }
151
+
152
+ if (!lightUrl) lightUrl = extractUrl(t.background_asset?.image) || extractUrl(t.icon_asset?.image);
153
+ if (!darkUrl && Array.isArray(t.alternative_themes) && t.alternative_themes.length) {
154
+ const alt = t.alternative_themes[0];
155
+ darkUrl = extractUrl(alt?.background_asset?.image) || extractUrl(alt?.icon_asset?.image);
156
+ }
157
+ if (lightUrl && !darkUrl) darkUrl = lightUrl;
158
+ if (darkUrl && !lightUrl) lightUrl = darkUrl;
159
+ if (lightUrl || darkUrl) t.preview_image_urls = { light_mode: lightUrl, dark_mode: darkUrl };
160
+
161
+ // Gradient
162
+ t.gradient_colors = extractGradient(theme);
163
+
164
+ // Primary color
165
+ t.primary_color = theme.primary_color || theme.fallback_color || t.gradient_colors[0] || null;
166
+
167
+ // Dark-mode flag
168
+ t.is_dark = t.primary_color ? isDark(t.primary_color) : null;
169
+
170
+ // WCAG contrast between first and last gradient stops
171
+ if (t.gradient_colors.length >= 2) {
172
+ t.gradient_contrast = wcagContrast(t.gradient_colors[0], t.gradient_colors[t.gradient_colors.length - 1]);
173
+ }
174
+
175
+ // Accessibility + ranking
176
+ t.accessibility_score = scoreTheme(t);
177
+ t.rank = index;
178
+
179
+ // Background image shortcut
180
+ t.backgroundImage = extractUrl(t.background_asset?.image) || lightUrl || null;
181
+
182
+ return t;
183
+ }
184
+
185
+ // ── GraphQL form builder ─────────────────────────────────────────────────────
186
+ function buildForm(ctx, prompt, numThemes) {
187
+ return {
188
+ av: ctx.i_userID || ctx.userID,
189
+ qpl_active_flow_ids: "25308101,25309433,521482085",
190
+ fb_api_caller_class: "RelayModern",
191
+ fb_api_req_friendly_name: "useGenerateAIThemeMutation",
192
+ variables: JSON.stringify({
193
+ input: {
194
+ client_mutation_id: String(Date.now() % 1e9),
195
+ actor_id: ctx.i_userID || ctx.userID,
196
+ bypass_cache: true,
197
+ caller: "MESSENGER",
198
+ num_themes: numThemes,
199
+ prompt,
200
+ }
201
+ }),
202
+ server_timestamps: true,
203
+ doc_id: "23873748445608673",
204
+ fb_api_analytics_tags: JSON.stringify(["qpl_active_flow_ids=25308101,25309433,521482085"]),
205
+ fb_dtsg: ctx.fb_dtsg,
206
+ };
207
+ }
208
+
209
+ // ── Fetch with retry ─────────────────────────────────────────────────────────
210
+ async function fetchRaw(defaultFuncs, ctx, form, maxRetries = 3, baseMs = 700) {
211
+ let lastErr;
212
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
213
+ try {
214
+ const res = await defaultFuncs
215
+ .post("https://web.facebook.com/api/graphql/", ctx.jar, form)
216
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
217
+
218
+ if (res.errors) {
219
+ const msg = res.errors[0]?.message || JSON.stringify(res.errors);
220
+ throw Object.assign(new Error(msg), { isFatal: true });
221
+ }
222
+
223
+ const payload = res?.data?.xfb_generate_ai_themes_from_prompt;
224
+ if (!payload?.themes?.length)
225
+ throw Object.assign(new Error("No themes returned for the given prompt"), { isFatal: true });
226
+
227
+ return payload.themes;
228
+ } catch (err) {
229
+ lastErr = err;
230
+ if (err.isFatal || attempt >= maxRetries) throw err;
231
+ const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 300;
232
+ utils.warn("createAITheme", `Attempt ${attempt} failed (${err.message}) — retry in ${Math.round(wait)}ms`);
233
+ await new Promise(r => setTimeout(r, wait));
234
+ }
235
+ }
236
+ throw lastErr;
237
+ }
238
+
239
+ // ── Module export ─────────────────────────────────────────────────────────────
240
+ module.exports = function (defaultFuncs, api, ctx) {
241
+
242
+ /**
243
+ * Generate AI-powered Messenger themes from a natural-language prompt.
244
+ *
245
+ * @param {string} prompt Natural-language description
246
+ * @param {number} [numThemes=3] Number of themes to generate (1–10)
247
+ * @param {object} [options]
248
+ * @param {boolean} [options.enrichPrompt=true] Auto-enrich the prompt
249
+ * @param {boolean} [options.useCache=true] Use/populate the 5-min cache
250
+ * @param {boolean} [options.parallel=false] Split large requests into 2 parallel calls
251
+ * @param {number} [options.retries=3] Max retry attempts on transient errors
252
+ * @param {Function} [callback] Node-style (err, themes[]) callback
253
+ * @returns {Promise<Array>} Array of fully-normalised theme objects
254
+ *
255
+ * Each theme contains:
256
+ * id, name, description, primary_color, gradient_colors, gradient_contrast,
257
+ * preview_image_urls { light_mode, dark_mode }, backgroundImage,
258
+ * is_dark, accessibility_score, rank, alternative_themes, background_asset, ...
259
+ */
260
+ return async function createAITheme(prompt, numThemes, options, callback) {
261
+ // ── Argument normalisation ───────────────────────────────────────────────
262
+ if (typeof numThemes === 'function') { callback = numThemes; numThemes = 3; options = {}; }
263
+ else if (typeof numThemes === 'object' && numThemes !== null) { options = numThemes; numThemes = 3; }
264
+ if (typeof options === 'function') { callback = options; options = {}; }
265
+ options = options || {};
266
+ numThemes = Math.max(1, Math.min(10, Number.isFinite(+numThemes) ? +numThemes : 3));
267
+
268
+ let resolveFunc, rejectFunc;
269
+ const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
270
+
271
+ function done(err, data) {
272
+ if (callback) return err ? callback(err) : callback(null, data);
273
+ if (err) rejectFunc(err); else resolveFunc(data);
274
+ }
275
+
276
+ // ── Validation ───────────────────────────────────────────────────────────
277
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
278
+ done(new Error("createAITheme: 'prompt' must be a non-empty string"));
279
+ return promise;
280
+ }
281
+
282
+ const rawPrompt = prompt.trim();
283
+ const finalPrompt = options.enrichPrompt !== false ? enrichPrompt(rawPrompt) : rawPrompt;
284
+ const cacheKey = `${finalPrompt}::${numThemes}`;
285
+
286
+ // ── Cache lookup ──────────────────────────────────────────────────────────
287
+ if (options.useCache !== false) {
288
+ const hit = _cache.get(cacheKey);
289
+ if (hit && Date.now() - hit.ts < CACHE_TTL) {
290
+ utils.log("createAITheme", `Cache hit — "${rawPrompt}"`);
291
+ done(null, hit.data);
292
+ return promise;
293
+ }
294
+ }
295
+
296
+ utils.log("createAITheme", `Generating ${numThemes} theme(s) — "${rawPrompt}"`);
297
+ if (finalPrompt !== rawPrompt)
298
+ utils.log("createAITheme", `Enriched prompt: "${finalPrompt}"`);
299
+
300
+ try {
301
+ const retries = options.retries || 3;
302
+ let rawThemes;
303
+
304
+ if (options.parallel && numThemes > 5) {
305
+ const half = Math.ceil(numThemes / 2);
306
+ const [a, b] = await Promise.all([
307
+ fetchRaw(defaultFuncs, ctx, buildForm(ctx, finalPrompt, half), retries),
308
+ fetchRaw(defaultFuncs, ctx, buildForm(ctx, finalPrompt, numThemes - half), retries),
309
+ ]);
310
+ rawThemes = [...a, ...b];
311
+ } else {
312
+ rawThemes = await fetchRaw(defaultFuncs, ctx, buildForm(ctx, finalPrompt, numThemes), retries);
313
+ }
314
+
315
+ const themes = rawThemes.map((t, i) => normalizeTheme(t, i));
316
+
317
+ // Sort best first
318
+ themes.sort((a, b) => (b.accessibility_score || 0) - (a.accessibility_score || 0));
319
+ themes.forEach((t, i) => { t.rank = i; });
320
+
321
+ if (options.useCache !== false)
322
+ _cache.set(cacheKey, { data: themes, ts: Date.now() });
323
+
324
+ utils.log("createAITheme", `Done — ${themes.length} theme(s) generated`);
325
+ done(null, themes);
326
+ } catch (err) {
327
+ utils.error("createAITheme", err.message || err);
328
+ done(err);
329
+ }
330
+
331
+ return promise;
332
+ };
333
+ };
@@ -0,0 +1,99 @@
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 = 700) {
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() * 300));
11
+ }
12
+ }
13
+ }
14
+
15
+ module.exports = (defaultFuncs, api, ctx) => {
16
+ return async function createNewGroup(participantIDs, groupTitle, options, callback) {
17
+ let resolveFunc, rejectFunc;
18
+ const returnPromise = new Promise((resolve, reject) => {
19
+ resolveFunc = resolve;
20
+ rejectFunc = reject;
21
+ });
22
+
23
+ if (typeof groupTitle === "function") { callback = groupTitle; groupTitle = null; options = {}; }
24
+ if (typeof options === "function") { callback = options; options = {}; }
25
+ if (typeof callback !== "function") {
26
+ callback = (err, data) => {
27
+ if (err) return rejectFunc(err);
28
+ resolveFunc(data);
29
+ };
30
+ }
31
+ if (!options || typeof options !== "object") options = {};
32
+
33
+ try {
34
+ if (!Array.isArray(participantIDs)) throw new Error("createNewGroup: participantIDs must be an array");
35
+ if (participantIDs.length < 1) throw new Error("createNewGroup: at least 1 participantID required");
36
+ if (participantIDs.length > 250) throw new Error("createNewGroup: maximum 250 participants allowed");
37
+
38
+ const actorID = ctx.i_userID || ctx.userID;
39
+
40
+ const pids = participantIDs
41
+ .map(String)
42
+ .filter(id => id !== String(actorID))
43
+ .map(id => ({ fbid: id }));
44
+ pids.push({ fbid: actorID });
45
+
46
+ const uniqueMap = new Map();
47
+ for (const p of pids) uniqueMap.set(p.fbid, p);
48
+ const dedupedPids = Array.from(uniqueMap.values());
49
+
50
+ await globalShield.addSmartDelay();
51
+
52
+ const form = {
53
+ fb_api_caller_class: "RelayModern",
54
+ fb_api_req_friendly_name: "MessengerGroupCreateMutation",
55
+ av: actorID,
56
+ doc_id: "577041672419534",
57
+ variables: JSON.stringify({
58
+ input: {
59
+ entry_point: options.entryPoint || "jewel_new_group",
60
+ actor_id: actorID,
61
+ participants: dedupedPids,
62
+ client_mutation_id: String(Math.round(Math.random() * 10000)),
63
+ thread_settings: {
64
+ name: groupTitle || null,
65
+ joinable_mode: options.joinableMode || "PRIVATE",
66
+ thread_image_fbid: options.imageID || null
67
+ }
68
+ }
69
+ })
70
+ };
71
+
72
+ const res = await retryOp(() =>
73
+ defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
74
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
75
+ );
76
+
77
+ if (res.errors) throw res;
78
+
79
+ const thread = res.data?.messenger_group_thread_create?.thread;
80
+ if (!thread) throw new Error("createNewGroup: no thread data in response");
81
+
82
+ const threadID = thread.thread_key?.thread_fbid;
83
+ const result = {
84
+ threadID,
85
+ name: thread.name || groupTitle || null,
86
+ participantCount: dedupedPids.length,
87
+ participants: dedupedPids.map(p => p.fbid),
88
+ timestamp: Date.now()
89
+ };
90
+
91
+ callback(null, result);
92
+ } catch (err) {
93
+ utils.error("createNewGroup", err);
94
+ callback(err);
95
+ }
96
+
97
+ return returnPromise;
98
+ };
99
+ };
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+
3
+ const utils = require('../datastore/models/matrix/tools');
4
+
5
+ async function retryOp(fn, retries = 3, base = 600) {
6
+ for (let i = 0; i < retries; i++) {
7
+ try { return await fn(); } catch (err) {
8
+ if (i === retries - 1) throw err;
9
+ await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
10
+ }
11
+ }
12
+ }
13
+
14
+ async function mqttCreatePoll(ctx, threadID, questionText, options) {
15
+ return new Promise((resolve, reject) => {
16
+ const reqID = ++ctx.wsReqNumber;
17
+ const taskID = ++ctx.wsTaskNumber;
18
+
19
+ const normalizedOptions = options.map(opt =>
20
+ typeof opt === "string" ? { text: opt } : opt
21
+ );
22
+
23
+ const form = JSON.stringify({
24
+ app_id: "772021112871879",
25
+ payload: JSON.stringify({
26
+ epoch_id: utils.generateOfflineThreadingID(),
27
+ tasks: [{
28
+ failure_count: null,
29
+ label: "163",
30
+ payload: JSON.stringify({
31
+ question_text: questionText,
32
+ thread_key: threadID,
33
+ options: normalizedOptions,
34
+ sync_group: 1
35
+ }),
36
+ queue_name: "poll_creation",
37
+ task_id: taskID
38
+ }],
39
+ version_id: "8768858626531631"
40
+ }),
41
+ request_id: reqID,
42
+ type: 3
43
+ });
44
+
45
+ let handled = false;
46
+ const onResp = (topic, message) => {
47
+ if (topic !== "/ls_resp" || handled) return;
48
+ let j;
49
+ try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
50
+ if (j.request_id !== reqID) return;
51
+ handled = true;
52
+ clearTimeout(timer);
53
+ ctx.mqttClient.removeListener("message", onResp);
54
+ resolve({
55
+ success: true,
56
+ threadID,
57
+ question: questionText,
58
+ options: normalizedOptions,
59
+ timestamp: Date.now(),
60
+ response: j.payload
61
+ });
62
+ };
63
+
64
+ const timer = setTimeout(() => {
65
+ if (!handled) {
66
+ handled = true;
67
+ ctx.mqttClient.removeListener("message", onResp);
68
+ resolve({ success: true, threadID, question: questionText, options, sent: true });
69
+ }
70
+ }, 20000);
71
+
72
+ ctx.mqttClient.on("message", onResp);
73
+ ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
74
+ if (err && !handled) {
75
+ handled = true;
76
+ clearTimeout(timer);
77
+ ctx.mqttClient.removeListener("message", onResp);
78
+ reject(err);
79
+ }
80
+ });
81
+ });
82
+ }
83
+
84
+ async function httpCreatePoll(defaultFuncs, ctx, threadID, questionText, options) {
85
+ const form = {
86
+ fb_api_caller_class: "RelayModern",
87
+ fb_api_req_friendly_name: "MessengerCreatePollMutation",
88
+ doc_id: "4738060229603032",
89
+ variables: JSON.stringify({
90
+ input: {
91
+ thread_id: threadID,
92
+ question: { text: questionText },
93
+ options: options.map(opt => ({
94
+ text: typeof opt === "string" ? opt : opt.text
95
+ })),
96
+ actor_id: ctx.userID,
97
+ client_mutation_id: String(Math.round(Math.random() * 10000))
98
+ }
99
+ })
100
+ };
101
+ const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
102
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs));
103
+ if (res.errors) throw new Error(JSON.stringify(res.errors));
104
+ return { success: true, threadID, question: questionText, options, method: "graphql" };
105
+ }
106
+
107
+ module.exports = (defaultFuncs, api, ctx) => {
108
+ return async function createPoll(threadID, questionText, options, callback) {
109
+ let resolveFunc, rejectFunc;
110
+ const returnPromise = new Promise((resolve, reject) => {
111
+ resolveFunc = resolve;
112
+ rejectFunc = reject;
113
+ });
114
+
115
+ if (typeof callback !== "function") {
116
+ callback = (err, result) => {
117
+ if (err) return rejectFunc(err);
118
+ resolveFunc(result);
119
+ };
120
+ }
121
+
122
+ try {
123
+ if (!threadID) throw new Error("createPoll: threadID is required");
124
+ if (!questionText || typeof questionText !== "string") throw new Error("createPoll: questionText must be a string");
125
+ if (!Array.isArray(options) || options.length < 2) throw new Error("createPoll: options must be an array with at least 2 entries");
126
+ if (options.length > 10) throw new Error("createPoll: maximum 10 poll options allowed");
127
+
128
+ let result;
129
+ if (ctx.mqttClient) {
130
+ try {
131
+ result = await retryOp(() => mqttCreatePoll(ctx, String(threadID), questionText, options));
132
+ } catch (mqttErr) {
133
+ utils.warn("createPoll", "MQTT failed, using HTTP:", mqttErr.message);
134
+ result = await retryOp(() => httpCreatePoll(defaultFuncs, ctx, String(threadID), questionText, options));
135
+ }
136
+ } else {
137
+ result = await retryOp(() => httpCreatePoll(defaultFuncs, ctx, String(threadID), questionText, options));
138
+ }
139
+
140
+ callback(null, result);
141
+ } catch (err) {
142
+ utils.error("createPoll", err);
143
+ callback(err);
144
+ }
145
+
146
+ return returnPromise;
147
+ };
148
+ };