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.
- package/LICENSE +58 -0
- package/README.md +534 -0
- package/index.js +35 -0
- package/package.json +101 -0
- package/phantom/core/builder/bootstrap.js +334 -0
- package/phantom/core/builder/config.js +78 -0
- package/phantom/core/builder/forge.js +113 -0
- package/phantom/core/builder/ignite.js +386 -0
- package/phantom/core/builder/options.js +61 -0
- package/phantom/core/engine.js +71 -0
- package/phantom/core/reactor.js +2 -0
- package/phantom/datastore/appState.js +2 -0
- package/phantom/datastore/appStateBackup.js +34 -0
- package/phantom/datastore/models/cipher/e2ee.js +48 -0
- package/phantom/datastore/models/cipher/vault.js +153 -0
- package/phantom/datastore/models/index.js +3 -0
- package/phantom/datastore/models/matrix/auth.js +151 -0
- package/phantom/datastore/models/matrix/cache.js +3 -0
- package/phantom/datastore/models/matrix/checker.js +2 -0
- package/phantom/datastore/models/matrix/clients.js +2 -0
- package/phantom/datastore/models/matrix/constants.js +2 -0
- package/phantom/datastore/models/matrix/credentials.js +2 -0
- package/phantom/datastore/models/matrix/cycle.js +2 -0
- package/phantom/datastore/models/matrix/gate.js +282 -0
- package/phantom/datastore/models/matrix/ghost.js +332 -0
- package/phantom/datastore/models/matrix/headers.js +193 -0
- package/phantom/datastore/models/matrix/heartbeat.js +298 -0
- package/phantom/datastore/models/matrix/identity.js +235 -0
- package/phantom/datastore/models/matrix/logger.js +271 -0
- package/phantom/datastore/models/matrix/monitor.js +2 -0
- package/phantom/datastore/models/matrix/net.js +316 -0
- package/phantom/datastore/models/matrix/response.js +193 -0
- package/phantom/datastore/models/matrix/revive.js +255 -0
- package/phantom/datastore/models/matrix/signals.js +2 -0
- package/phantom/datastore/models/matrix/store.js +263 -0
- package/phantom/datastore/models/matrix/telemetry.js +272 -0
- package/phantom/datastore/models/matrix/tools.js +93 -0
- package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
- package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
- package/phantom/datastore/models/matrix/transform/index.js +203 -0
- package/phantom/datastore/models/matrix/validator.js +157 -0
- package/phantom/datastore/models/types/index.d.ts +498 -0
- package/phantom/datastore/schema.js +167 -0
- package/phantom/datastore/session.js +129 -0
- package/phantom/datastore/threads.js +22 -0
- package/phantom/datastore/users.js +26 -0
- package/phantom/dispatch/addExternalModule.js +239 -0
- package/phantom/dispatch/addUserToGroup.js +161 -0
- package/phantom/dispatch/changeAdminStatus.js +142 -0
- package/phantom/dispatch/changeArchivedStatus.js +135 -0
- package/phantom/dispatch/changeAvatar.js +123 -0
- package/phantom/dispatch/changeBio.js +86 -0
- package/phantom/dispatch/changeBlockedStatus.js +86 -0
- package/phantom/dispatch/changeGroupImage.js +145 -0
- package/phantom/dispatch/changeThreadColor.js +172 -0
- package/phantom/dispatch/changeThreadEmoji.js +130 -0
- package/phantom/dispatch/comment.js +136 -0
- package/phantom/dispatch/createAITheme.js +333 -0
- package/phantom/dispatch/createNewGroup.js +99 -0
- package/phantom/dispatch/createPoll.js +148 -0
- package/phantom/dispatch/deleteMessage.js +131 -0
- package/phantom/dispatch/deleteThread.js +155 -0
- package/phantom/dispatch/e2ee.js +101 -0
- package/phantom/dispatch/editMessage.js +158 -0
- package/phantom/dispatch/emoji.js +143 -0
- package/phantom/dispatch/fetchThemeData.js +233 -0
- package/phantom/dispatch/follow.js +111 -0
- package/phantom/dispatch/forwardMessage.js +110 -0
- package/phantom/dispatch/friend.js +189 -0
- package/phantom/dispatch/gcmember.js +138 -0
- package/phantom/dispatch/gcname.js +131 -0
- package/phantom/dispatch/gcrule.js +111 -0
- package/phantom/dispatch/getAccess.js +109 -0
- package/phantom/dispatch/getBotInfo.js +81 -0
- package/phantom/dispatch/getBotInitialData.js +110 -0
- package/phantom/dispatch/getFriendsList.js +118 -0
- package/phantom/dispatch/getMessage.js +199 -0
- package/phantom/dispatch/getTheme.js +199 -0
- package/phantom/dispatch/getThemeInfo.js +160 -0
- package/phantom/dispatch/getThreadHistory.js +139 -0
- package/phantom/dispatch/getThreadInfo.js +153 -0
- package/phantom/dispatch/getThreadList.js +132 -0
- package/phantom/dispatch/getThreadPictures.js +93 -0
- package/phantom/dispatch/getUserID.js +147 -0
- package/phantom/dispatch/getUserInfo.js +513 -0
- package/phantom/dispatch/getUserInfoV2.js +146 -0
- package/phantom/dispatch/handleMessageRequest.js +50 -0
- package/phantom/dispatch/httpGet.js +63 -0
- package/phantom/dispatch/httpPost.js +89 -0
- package/phantom/dispatch/httpPostFormData.js +69 -0
- package/phantom/dispatch/listenMqtt.js +1236 -0
- package/phantom/dispatch/listenSpeed.js +179 -0
- package/phantom/dispatch/logout.js +93 -0
- package/phantom/dispatch/markAsDelivered.js +92 -0
- package/phantom/dispatch/markAsRead.js +119 -0
- package/phantom/dispatch/markAsReadAll.js +215 -0
- package/phantom/dispatch/markAsSeen.js +70 -0
- package/phantom/dispatch/mqttDeltaValue.js +278 -0
- package/phantom/dispatch/muteThread.js +253 -0
- package/phantom/dispatch/nickname.js +132 -0
- package/phantom/dispatch/notes.js +263 -0
- package/phantom/dispatch/pinMessage.js +238 -0
- package/phantom/dispatch/produceMetaTheme.js +335 -0
- package/phantom/dispatch/realtime.js +291 -0
- package/phantom/dispatch/removeUserFromGroup.js +248 -0
- package/phantom/dispatch/resolvePhotoUrl.js +217 -0
- package/phantom/dispatch/searchForThread.js +258 -0
- package/phantom/dispatch/sendMessage.js +354 -0
- package/phantom/dispatch/sendMessageMqtt.js +249 -0
- package/phantom/dispatch/sendTypingIndicator.js +206 -0
- package/phantom/dispatch/setMessageReaction.js +188 -0
- package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
- package/phantom/dispatch/setThreadTheme.js +330 -0
- package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
- package/phantom/dispatch/share.js +200 -0
- package/phantom/dispatch/shareContact.js +216 -0
- package/phantom/dispatch/stickers.js +395 -0
- package/phantom/dispatch/story.js +240 -0
- package/phantom/dispatch/theme.js +296 -0
- package/phantom/dispatch/unfriend.js +199 -0
- 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
|
+
};
|