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,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* produceMetaTheme — Advanced Meta AI Theme Producer for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* The highest-level AI theme generation interface, combining:
|
|
7
|
+
* - Smart prompt pre-processing & keyword enrichment
|
|
8
|
+
* - Dual-endpoint resilience (web + www Facebook endpoints)
|
|
9
|
+
* - Exponential-backoff retry with per-error friendly messages
|
|
10
|
+
* - Complete color metadata extraction across all UI surfaces
|
|
11
|
+
* - WCAG accessibility scoring
|
|
12
|
+
* - Dark/light mode detection
|
|
13
|
+
* - In-memory result caching (5 minutes)
|
|
14
|
+
* - Structured output: themes[] + flattened best-theme properties
|
|
15
|
+
* - Full callback + Promise dual API
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
19
|
+
|
|
20
|
+
const _cache = new Map();
|
|
21
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
// ── Prompt keyword enrichment ──────────────────────────────────────────────────
|
|
24
|
+
const ENRICHERS = [
|
|
25
|
+
{ re: /\bnature\b/i, add: ", organic texture, earth tones, lush flora" },
|
|
26
|
+
{ re: /\bocean|sea\b/i, add: ", deep blue gradients, aquamarine shimmer, bioluminescent waves" },
|
|
27
|
+
{ re: /\bspace|galaxy\b/i, add: ", cosmic purple nebula, starfield depth, zero-gravity glow" },
|
|
28
|
+
{ re: /\bneon\b/i, add: ", cyberpunk grid, vivid saturation, high-contrast glow lines" },
|
|
29
|
+
{ re: /\bsunset\b/i, add: ", amber horizon, golden-hour warmth, gradient sky fade" },
|
|
30
|
+
{ re: /\bforest\b/i, add: ", deep canopy greens, dappled light, mossy tranquility" },
|
|
31
|
+
{ re: /\bfire|flame\b/i, add: ", scorched black, molten orange, ember heat shimmer" },
|
|
32
|
+
{ re: /\bminimal\b/i, add: ", clean whitespace, geometric precision, calm neutrals" },
|
|
33
|
+
{ re: /\bvintage\b/i, add: ", film grain sepia, nostalgic warmth, muted retro palette" },
|
|
34
|
+
{ re: /\bpastel\b/i, add: ", soft blush, peachy cloud tones, lavender mist" },
|
|
35
|
+
{ re: /\bwinter|snow\b/i, add: ", frosted crystal blue-white, cold clarity, silent snowfall" },
|
|
36
|
+
{ re: /\bcity|urban\b/i, add: ", metropolitan neon, steel-grey concrete, skyline silhouette" },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function enrichPrompt(raw) {
|
|
40
|
+
let s = raw.trim();
|
|
41
|
+
for (const { re, add } of ENRICHERS) {
|
|
42
|
+
if (re.test(s) && !s.includes(add.slice(2, 12))) { s += add; break; }
|
|
43
|
+
}
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Color helpers ────────────────────────────────────────────────────────────
|
|
48
|
+
function hexToRgb(hex) {
|
|
49
|
+
if (!hex) return null;
|
|
50
|
+
const c = hex.replace(/^#/, '').replace(/^[Ff]{2}/, '');
|
|
51
|
+
if (c.length !== 6) return null;
|
|
52
|
+
return { r: parseInt(c.slice(0, 2), 16), g: parseInt(c.slice(2, 4), 16), b: parseInt(c.slice(4, 6), 16) };
|
|
53
|
+
}
|
|
54
|
+
function lum({ r, g, b }) {
|
|
55
|
+
const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
56
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
57
|
+
}
|
|
58
|
+
function isDark(hex) { const r = hexToRgb(hex); return r ? lum(r) < 0.179 : null; }
|
|
59
|
+
function wcag(h1, h2) {
|
|
60
|
+
const r1 = hexToRgb(h1), r2 = hexToRgb(h2);
|
|
61
|
+
if (!r1 || !r2) return null;
|
|
62
|
+
const [hi, lo] = lum(r1) > lum(r2) ? [lum(r1), lum(r2)] : [lum(r2), lum(r1)];
|
|
63
|
+
return +((hi + 0.05) / (lo + 0.05)).toFixed(2);
|
|
64
|
+
}
|
|
65
|
+
function parseGradient(raw) {
|
|
66
|
+
if (!raw) return [];
|
|
67
|
+
if (Array.isArray(raw)) return raw;
|
|
68
|
+
if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return [raw]; } }
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
function extractUri(obj) {
|
|
72
|
+
if (!obj) return null;
|
|
73
|
+
if (typeof obj === 'string') return obj;
|
|
74
|
+
return obj.uri || obj.url || null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Accessibility score ──────────────────────────────────────────────────────
|
|
78
|
+
function accessibilityScore(t) {
|
|
79
|
+
let s = 40;
|
|
80
|
+
const g = t.colors?.gradient || [];
|
|
81
|
+
if (g.length >= 2) s += 10;
|
|
82
|
+
if (g.length >= 3) s += 5;
|
|
83
|
+
if (t.images?.background) s += 12;
|
|
84
|
+
if (t.images?.icon) s += 5;
|
|
85
|
+
if (t.alternativeThemes?.length) s += 8;
|
|
86
|
+
if (t.preview_image_urls?.light_mode) s += 10;
|
|
87
|
+
if (t.gradient_contrast && t.gradient_contrast > 4.5) s += 10;
|
|
88
|
+
return Math.min(100, s);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Full theme normaliser ────────────────────────────────────────────────────
|
|
92
|
+
function normalizeTheme(raw, idx) {
|
|
93
|
+
const g = parseGradient(raw.gradient_colors || raw.background_gradient_colors);
|
|
94
|
+
const bg = extractUri(raw.background_asset?.image);
|
|
95
|
+
const ic = extractUri(raw.icon_asset?.image);
|
|
96
|
+
|
|
97
|
+
const preview = (() => {
|
|
98
|
+
const src = raw.preview_image_urls || raw.preview_images;
|
|
99
|
+
if (!src) return bg ? { light_mode: bg, dark_mode: bg } : null;
|
|
100
|
+
if (typeof src === 'string') return { light_mode: src, dark_mode: src };
|
|
101
|
+
if (Array.isArray(src)) return { light_mode: extractUri(src[0]), dark_mode: extractUri(src[1]) || extractUri(src[0]) };
|
|
102
|
+
const l = extractUri(src.light_mode || src.light);
|
|
103
|
+
const d = extractUri(src.dark_mode || src.dark) || l;
|
|
104
|
+
return (l || d) ? { light_mode: l || d, dark_mode: d || l } : null;
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
const primary = g[0] || raw.fallback_color || null;
|
|
108
|
+
|
|
109
|
+
const out = {
|
|
110
|
+
success: true,
|
|
111
|
+
themeId: raw.id,
|
|
112
|
+
name: raw.accessibility_label || raw.name || `Theme ${idx + 1}`,
|
|
113
|
+
description: raw.description || null,
|
|
114
|
+
serialNumber: idx + 1,
|
|
115
|
+
rank: idx,
|
|
116
|
+
|
|
117
|
+
colors: {
|
|
118
|
+
primary: primary,
|
|
119
|
+
fallback: raw.fallback_color || null,
|
|
120
|
+
gradient: g,
|
|
121
|
+
backgroundGradient: parseGradient(raw.background_gradient_colors),
|
|
122
|
+
inboundMessageGradient: parseGradient(raw.inbound_message_gradient_colors),
|
|
123
|
+
messageText: raw.message_text_color || null,
|
|
124
|
+
titleBarText: raw.title_bar_text_color || null,
|
|
125
|
+
titleBarButton: raw.title_bar_button_tint_color || null,
|
|
126
|
+
titleBarBackground: raw.title_bar_background_color || null,
|
|
127
|
+
composerBackground: raw.composer_background_color || null,
|
|
128
|
+
composerInputBackground: raw.composer_input_background_color || null,
|
|
129
|
+
composerTint: raw.composer_tint_color || null,
|
|
130
|
+
primaryButton: raw.primary_button_background_color || null,
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
images: { background: bg, icon: ic },
|
|
134
|
+
backgroundImage: bg,
|
|
135
|
+
iconImage: ic,
|
|
136
|
+
preview_image_urls: preview,
|
|
137
|
+
|
|
138
|
+
is_dark: isDark(primary),
|
|
139
|
+
gradient_contrast: g.length >= 2 ? wcag(g[0], g[g.length - 1]) : null,
|
|
140
|
+
|
|
141
|
+
alternativeThemes: Array.isArray(raw.alternative_themes)
|
|
142
|
+
? raw.alternative_themes.map(a => ({
|
|
143
|
+
id: a.id,
|
|
144
|
+
name: a.accessibility_label || a.name || null,
|
|
145
|
+
backgroundImage: extractUri(a.background_asset?.image) || null,
|
|
146
|
+
iconImage: extractUri(a.icon_asset?.image) || null,
|
|
147
|
+
gradient_colors: parseGradient(a.gradient_colors),
|
|
148
|
+
is_dark: isDark(a.fallback_color || null),
|
|
149
|
+
}))
|
|
150
|
+
: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
out.accessibility_score = accessibilityScore(out);
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Build GraphQL form ────────────────────────────────────────────────────────
|
|
158
|
+
function buildForm(ctx, input) {
|
|
159
|
+
return {
|
|
160
|
+
av: ctx.userID,
|
|
161
|
+
__aaid: 0,
|
|
162
|
+
__user: ctx.userID,
|
|
163
|
+
__a: 1,
|
|
164
|
+
__req: utils.getSignatureID ? utils.getSignatureID() : '1',
|
|
165
|
+
__hs: "20358.HYP:comet_pkg.2.1...0",
|
|
166
|
+
dpr: 1,
|
|
167
|
+
__ccg: "EXCELLENT",
|
|
168
|
+
__rev: "1027673511",
|
|
169
|
+
__s: utils.getSignatureID ? utils.getSignatureID() : '1',
|
|
170
|
+
__hsi: "7554561631547849479",
|
|
171
|
+
__comet_req: 15,
|
|
172
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
173
|
+
jazoest: ctx.jazoest,
|
|
174
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
175
|
+
__spin_r: "1027673511",
|
|
176
|
+
__spin_b: "trunk",
|
|
177
|
+
__spin_t: Date.now(),
|
|
178
|
+
__crn: "comet.fbweb.MWInboxHomeRoute",
|
|
179
|
+
qpl_active_flow_ids: "25309433,521485406",
|
|
180
|
+
fb_api_caller_class: "RelayModern",
|
|
181
|
+
fb_api_req_friendly_name: "useGenerateAIThemeMutation",
|
|
182
|
+
variables: JSON.stringify({ input }),
|
|
183
|
+
server_timestamps: true,
|
|
184
|
+
doc_id: "23873748445608673",
|
|
185
|
+
fb_api_analytics_tags: JSON.stringify(["qpl_active_flow_ids=25309433,521485406"]),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Friendly error map ────────────────────────────────────────────────────────
|
|
190
|
+
const ERROR_MAP = [
|
|
191
|
+
{ test: e => /not authorized/i.test(e?.message), msg: "This account doesn't have permission to generate AI themes." },
|
|
192
|
+
{ test: e => /rate.?limit/i.test(e?.message), msg: "Rate limit reached. Please wait a moment before retrying." },
|
|
193
|
+
{ test: e => /invalid/i.test(e?.message), msg: "Invalid parameters. Please review your prompt and options." },
|
|
194
|
+
{ test: e => e?.statusCode === 403, msg: "Access denied. Your account may not support Meta AI themes." },
|
|
195
|
+
{ test: e => e?.statusCode === 429, msg: "Too many requests. Please slow down." },
|
|
196
|
+
{ test: e => /network|timeout|econnreset/i.test(e?.message), msg: "Network error. Please check your connection and retry." },
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
function friendlyError(err) {
|
|
200
|
+
for (const { test, msg } of ERROR_MAP) if (test(err)) return msg;
|
|
201
|
+
return "An unexpected error occurred while generating the theme.";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Retry fetch ───────────────────────────────────────────────────────────────
|
|
205
|
+
async function attemptFetch(defaultFuncs, ctx, form, retries, baseMs) {
|
|
206
|
+
for (let i = 1; i <= retries; i++) {
|
|
207
|
+
try {
|
|
208
|
+
const raw = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
|
|
209
|
+
const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(raw);
|
|
210
|
+
|
|
211
|
+
if (checked.errors) {
|
|
212
|
+
const msg = (Array.isArray(checked.errors) ? checked.errors[0]?.message : null) || JSON.stringify(checked.errors);
|
|
213
|
+
throw Object.assign(new Error(msg), { isFatal: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const payload = checked?.data?.xfb_generate_ai_themes_from_prompt;
|
|
217
|
+
if (!payload) throw Object.assign(new Error("No AI theme payload in response"), { isFatal: true });
|
|
218
|
+
if (!Array.isArray(payload.themes) || !payload.themes.length) throw Object.assign(new Error("No themes generated for prompt"), { isFatal: true });
|
|
219
|
+
|
|
220
|
+
return payload;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.isFatal || i >= retries) throw err;
|
|
223
|
+
const wait = baseMs * Math.pow(2, i - 1) + Math.random() * 200;
|
|
224
|
+
utils.warn("produceMetaTheme", `Retry ${i}/${retries} in ${Math.round(wait)}ms — ${err.message}`);
|
|
225
|
+
await new Promise(r => setTimeout(r, wait));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Module export ─────────────────────────────────────────────────────────────
|
|
231
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Produce a fully-enriched Meta AI theme from a natural-language prompt.
|
|
235
|
+
*
|
|
236
|
+
* @param {string} prompt Natural-language description
|
|
237
|
+
* @param {object} [opts]
|
|
238
|
+
* @param {number} [opts.numThemes=1] Number of themes (1–5)
|
|
239
|
+
* @param {string} [opts.imageUrl] Optional reference image URL
|
|
240
|
+
* @param {boolean} [opts.enrichPrompt=true] Auto-enrich prompt
|
|
241
|
+
* @param {boolean} [opts.useCache=true] Use 5-minute result cache
|
|
242
|
+
* @param {number} [opts.retries=3] Max retry attempts
|
|
243
|
+
* @param {Function} [callback] Node-style (err, result) callback
|
|
244
|
+
* @returns {Promise<object>} { success, count, themes[], ...bestThemeProps }
|
|
245
|
+
*/
|
|
246
|
+
return function produceMetaTheme(prompt, opts, callback) {
|
|
247
|
+
let resolveFunc, rejectFunc;
|
|
248
|
+
const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
|
|
249
|
+
|
|
250
|
+
if (typeof opts === 'function') { callback = opts; opts = {}; }
|
|
251
|
+
opts = opts || {};
|
|
252
|
+
if (typeof callback !== 'function') {
|
|
253
|
+
callback = (err, data) => { if (err) rejectFunc(err); else resolveFunc(data); };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
257
|
+
callback({ error: "Prompt is required and must be a non-empty string" });
|
|
258
|
+
return promise;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const rawPrompt = prompt.trim();
|
|
262
|
+
const finalPrompt = opts.enrichPrompt !== false ? enrichPrompt(rawPrompt) : rawPrompt;
|
|
263
|
+
const numThemes = Math.max(1, Math.min(5, Number.isFinite(+opts.numThemes) ? +opts.numThemes : 1));
|
|
264
|
+
const retries = opts.retries || 3;
|
|
265
|
+
const cacheKey = `meta::${finalPrompt}::${numThemes}`;
|
|
266
|
+
|
|
267
|
+
// Cache hit
|
|
268
|
+
if (opts.useCache !== false) {
|
|
269
|
+
const hit = _cache.get(cacheKey);
|
|
270
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL) {
|
|
271
|
+
utils.log("produceMetaTheme", `Cache hit — "${rawPrompt}"`);
|
|
272
|
+
callback(null, hit.data);
|
|
273
|
+
return promise;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
utils.log("produceMetaTheme", `Generating ${numThemes} theme(s) for: "${rawPrompt}"`);
|
|
278
|
+
|
|
279
|
+
const input = {
|
|
280
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
281
|
+
actor_id: ctx.userID,
|
|
282
|
+
bypass_cache: true,
|
|
283
|
+
caller: "MESSENGER",
|
|
284
|
+
num_themes: numThemes,
|
|
285
|
+
prompt: finalPrompt,
|
|
286
|
+
};
|
|
287
|
+
if (opts.imageUrl) input.image_url = opts.imageUrl;
|
|
288
|
+
|
|
289
|
+
const form = buildForm(ctx, input);
|
|
290
|
+
|
|
291
|
+
(async () => {
|
|
292
|
+
try {
|
|
293
|
+
const payload = await attemptFetch(defaultFuncs, ctx, form, retries, 700);
|
|
294
|
+
const themes = payload.themes.map(normalizeTheme);
|
|
295
|
+
|
|
296
|
+
// Sort best accessibility first, re-rank
|
|
297
|
+
themes.sort((a, b) => (b.accessibility_score || 0) - (a.accessibility_score || 0));
|
|
298
|
+
themes.forEach((t, i) => { t.rank = i; t.serialNumber = i + 1; });
|
|
299
|
+
|
|
300
|
+
const best = themes[0];
|
|
301
|
+
const out = {
|
|
302
|
+
success: true,
|
|
303
|
+
count: themes.length,
|
|
304
|
+
themes,
|
|
305
|
+
// Flattened best-theme convenience props
|
|
306
|
+
themeId: best.themeId,
|
|
307
|
+
name: best.name,
|
|
308
|
+
description: best.description,
|
|
309
|
+
colors: best.colors,
|
|
310
|
+
images: best.images,
|
|
311
|
+
backgroundImage: best.backgroundImage,
|
|
312
|
+
iconImage: best.iconImage,
|
|
313
|
+
preview_image_urls: best.preview_image_urls,
|
|
314
|
+
is_dark: best.is_dark,
|
|
315
|
+
accessibility_score: best.accessibility_score,
|
|
316
|
+
gradient_contrast: best.gradient_contrast,
|
|
317
|
+
alternativeThemes: best.alternativeThemes,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
if (opts.useCache !== false) _cache.set(cacheKey, { data: out, ts: Date.now() });
|
|
321
|
+
utils.log("produceMetaTheme", `Done — ${themes.length} theme(s), best score: ${best.accessibility_score}`);
|
|
322
|
+
callback(null, out);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
utils.error("produceMetaTheme", err.message || err);
|
|
325
|
+
callback({
|
|
326
|
+
error: friendlyError(err),
|
|
327
|
+
originalError: err?.message || String(err),
|
|
328
|
+
statusCode: err?.statusCode || null,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
332
|
+
|
|
333
|
+
return promise;
|
|
334
|
+
};
|
|
335
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { WebSocket } = require('undici');
|
|
4
|
+
const EventEmitter = require('events');
|
|
5
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
6
|
+
const HttpsProxyAgent = require('https-proxy-agent');
|
|
7
|
+
|
|
8
|
+
const REALTIME_METRICS = { connected: 0, disconnected: 0, messagesReceived: 0, errors: 0, reconnects: 0 };
|
|
9
|
+
const REALTIME_LOG = [];
|
|
10
|
+
const MAX_LOG = 200;
|
|
11
|
+
const NOTIF_HANDLERS = new Map();
|
|
12
|
+
const MIDDLEWARE_STACK = [];
|
|
13
|
+
|
|
14
|
+
function logEvent(event, data = {}) {
|
|
15
|
+
const entry = { event, ts: Date.now(), ...data };
|
|
16
|
+
REALTIME_LOG.unshift(entry);
|
|
17
|
+
if (REALTIME_LOG.length > MAX_LOG) REALTIME_LOG.length = MAX_LOG;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatNotification(data) {
|
|
21
|
+
if (!data.data || !data.data.viewer) return null;
|
|
22
|
+
const notifEdge = data.data.viewer.notifications_page?.edges?.[1]?.node?.notif;
|
|
23
|
+
if (!notifEdge) return null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
type: 'notification',
|
|
27
|
+
notifID: notifEdge.notif_id,
|
|
28
|
+
body: notifEdge.body?.text,
|
|
29
|
+
senderID: Object.keys(notifEdge.tracking?.from_uids || {})[0],
|
|
30
|
+
url: notifEdge.url,
|
|
31
|
+
timestamp: notifEdge.creation_time?.timestamp,
|
|
32
|
+
seenState: notifEdge.seen_state,
|
|
33
|
+
rawEdge: notifEdge
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function classifyMessage(jsonData) {
|
|
38
|
+
if (jsonData.code === 200) return 'status';
|
|
39
|
+
if (jsonData.data?.viewer?.notifications_page) return 'notification';
|
|
40
|
+
if (jsonData.type === 'presence') return 'presence';
|
|
41
|
+
if (jsonData.type === 'friend_request') return 'friend_request';
|
|
42
|
+
if (jsonData.data?.xhrPayload) return 'xhr';
|
|
43
|
+
if (jsonData.event === 'friend_request_confirmed') return 'friend_confirmed';
|
|
44
|
+
return 'payload';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function runMiddlewares(message, middlewares) {
|
|
48
|
+
let msg = message;
|
|
49
|
+
for (const mw of middlewares) {
|
|
50
|
+
try { msg = (await mw(msg)) || msg; } catch (err) {
|
|
51
|
+
utils.warn('realtime', `Middleware error: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return msg;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function buildSubscriptions(userID) {
|
|
58
|
+
return [
|
|
59
|
+
'{"x-dgw-app-XRSS-method":"Falco","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
60
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:USER_ACTIVITY_UPDATE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"9525970914181809","x-dgw-app-XRSS-routing_hint":"UserActivitySubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
61
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:ACTOR_GATEWAY_EXPERIENCE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"24191710730466150","x-dgw-app-XRSS-routing_hint":"CometActorGatewayExperienceSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
62
|
+
`{"x-dgw-app-XRSS-method":"FBLQ:comet_notifications_live_query_experimental","x-dgw-app-XRSS-doc_id":"9784489068321501","x-dgw-app-XRSS-actor_id":"${userID}","x-dgw-app-XRSS-page_id":"${userID}","x-dgw-app-XRSS-request_stream_retry":"false","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}`,
|
|
63
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:FRIEND_REQUEST_CONFIRM_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"9687616244672204","x-dgw-app-XRSS-routing_hint":"FriendingCometFriendRequestConfirmSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
64
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:FRIEND_REQUEST_RECEIVE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"24047008371656912","x-dgw-app-XRSS-routing_hint":"FriendingCometFriendRequestReceiveSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
65
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:RTWEB_CALL_BLOCKED_SETTING_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"24429620016626810","x-dgw-app-XRSS-routing_hint":"RTWebCallBlockedSettingSubscription_CallBlockSettingSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
66
|
+
'{"x-dgw-app-XRSS-method":"PresenceUnifiedJSON","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
67
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:MESSENGER_CHAT_TABS_NOTIFICATION_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"23885219097739619","x-dgw-app-XRSS-routing_hint":"MWChatTabsNotificationSubscription_MessengerChatTabsNotificationSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
68
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:BATCH_NOTIFICATION_STATE_CHANGE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"30300156509571373","x-dgw-app-XRSS-routing_hint":"CometBatchNotificationsStateChangeSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
69
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:NOTIFICATION_STATE_CHANGE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"23864641996495578","x-dgw-app-XRSS-routing_hint":"CometNotificationsStateChangeSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}',
|
|
70
|
+
'{"x-dgw-app-XRSS-method":"FBGQLS:NOTIFICATION_STATE_CHANGE_SUBSCRIBE","x-dgw-app-XRSS-doc_id":"9754477301332178","x-dgw-app-XRSS-routing_hint":"CometFriendNotificationsStateChangeSubscription","x-dgw-app-xrs-body":"true","x-dgw-app-XRS-Accept-Ack":"RSAck","x-dgw-app-XRSS-http_referer":"https://www.facebook.com/friends"}'
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
75
|
+
return function listenRealtime(options = {}) {
|
|
76
|
+
const {
|
|
77
|
+
keepAliveMs = 10000,
|
|
78
|
+
maxReconnectDelay = 60000,
|
|
79
|
+
reconnectBaseMs = 1000,
|
|
80
|
+
maxReconnectAttempts = Infinity,
|
|
81
|
+
pingPayload = 'ping'
|
|
82
|
+
} = options;
|
|
83
|
+
|
|
84
|
+
const emitter = new EventEmitter();
|
|
85
|
+
emitter.setMaxListeners(50);
|
|
86
|
+
|
|
87
|
+
let ws = null;
|
|
88
|
+
let reconnectTimeout = null;
|
|
89
|
+
let keepAliveInterval = null;
|
|
90
|
+
let stopped = false;
|
|
91
|
+
let reconnectAttempts = 0;
|
|
92
|
+
let totalMessages = 0;
|
|
93
|
+
let connectionStartTime = null;
|
|
94
|
+
|
|
95
|
+
const subscriptions = buildSubscriptions(ctx.userID);
|
|
96
|
+
|
|
97
|
+
async function handleMessage(data) {
|
|
98
|
+
try {
|
|
99
|
+
const text = typeof data.text === 'function' ? await data.text() : String(data);
|
|
100
|
+
const jsonStart = text.indexOf('{');
|
|
101
|
+
if (jsonStart === -1) return;
|
|
102
|
+
|
|
103
|
+
let jsonData;
|
|
104
|
+
try { jsonData = JSON.parse(text.substring(jsonStart)); } catch { return; }
|
|
105
|
+
|
|
106
|
+
REALTIME_METRICS.messagesReceived++;
|
|
107
|
+
totalMessages++;
|
|
108
|
+
|
|
109
|
+
const processed = MIDDLEWARE_STACK.length ? await runMiddlewares(jsonData, MIDDLEWARE_STACK) : jsonData;
|
|
110
|
+
const msgType = classifyMessage(processed);
|
|
111
|
+
|
|
112
|
+
logEvent(`message:${msgType}`, { size: text.length });
|
|
113
|
+
|
|
114
|
+
if (NOTIF_HANDLERS.has(msgType)) {
|
|
115
|
+
for (const handler of NOTIF_HANDLERS.get(msgType)) {
|
|
116
|
+
try { handler(processed); } catch (err) {
|
|
117
|
+
utils.warn('realtime', `Handler error for ${msgType}:`, err.message);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (processed.code === 200) {
|
|
123
|
+
emitter.emit('success', processed);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const formattedNotif = formatNotification(processed);
|
|
128
|
+
if (formattedNotif) {
|
|
129
|
+
emitter.emit('notification', formattedNotif);
|
|
130
|
+
emitter.emit(msgType, formattedNotif);
|
|
131
|
+
} else {
|
|
132
|
+
emitter.emit('payload', processed);
|
|
133
|
+
emitter.emit(msgType, processed);
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
REALTIME_METRICS.errors++;
|
|
137
|
+
utils.error('realtime', 'Message parse error:', err.message || err);
|
|
138
|
+
emitter.emit('error', err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function scheduleReconnect() {
|
|
143
|
+
if (stopped || reconnectAttempts >= maxReconnectAttempts) return;
|
|
144
|
+
reconnectAttempts++;
|
|
145
|
+
REALTIME_METRICS.reconnects++;
|
|
146
|
+
|
|
147
|
+
const backoff = Math.min(maxReconnectDelay, reconnectBaseMs * Math.pow(2, Math.min(reconnectAttempts, 7)));
|
|
148
|
+
const jitter = Math.floor(Math.random() * 1500);
|
|
149
|
+
const delay = backoff + jitter;
|
|
150
|
+
|
|
151
|
+
clearTimeout(reconnectTimeout);
|
|
152
|
+
reconnectTimeout = setTimeout(connect, delay);
|
|
153
|
+
logEvent('reconnect_scheduled', { attempt: reconnectAttempts, delay });
|
|
154
|
+
utils.warn('realtime', `Reconnect #${reconnectAttempts} in ${delay}ms`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function connect() {
|
|
158
|
+
if (stopped) return;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const queryParams = new URLSearchParams({
|
|
162
|
+
'x-dgw-appid': '2220391788200892',
|
|
163
|
+
'x-dgw-appversion': '0',
|
|
164
|
+
'x-dgw-authtype': '1:0',
|
|
165
|
+
'x-dgw-version': '5',
|
|
166
|
+
'x-dgw-uuid': ctx.userID,
|
|
167
|
+
'x-dgw-tier': 'prod',
|
|
168
|
+
'x-dgw-deviceid': ctx.clientID,
|
|
169
|
+
'x-dgw-app-stream-group': 'group1'
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const url = `wss://gateway.facebook.com/ws/realtime?${queryParams.toString()}`;
|
|
173
|
+
const cookies = ctx.jar.getCookiesSync('https://www.facebook.com').join('; ');
|
|
174
|
+
|
|
175
|
+
const wsOptions = {
|
|
176
|
+
headers: {
|
|
177
|
+
Cookie: cookies,
|
|
178
|
+
Origin: 'https://www.facebook.com',
|
|
179
|
+
'User-Agent': ctx.globalOptions?.userAgent || 'Mozilla/5.0',
|
|
180
|
+
Referer: 'https://www.facebook.com',
|
|
181
|
+
Host: new URL(url).hostname,
|
|
182
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
183
|
+
'Accept-Language': 'en-US,en;q=0.9'
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
if (ctx.globalOptions?.proxy) wsOptions.agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
|
|
188
|
+
|
|
189
|
+
ws = new WebSocket(url, wsOptions);
|
|
190
|
+
|
|
191
|
+
ws.onopen = () => {
|
|
192
|
+
reconnectAttempts = 0;
|
|
193
|
+
connectionStartTime = Date.now();
|
|
194
|
+
REALTIME_METRICS.connected++;
|
|
195
|
+
logEvent('connected');
|
|
196
|
+
utils.log('realtime', 'WebSocket connected');
|
|
197
|
+
|
|
198
|
+
subscriptions.forEach((payload, index) => {
|
|
199
|
+
const prefix = Buffer.from([14, index, 0, payload.length]);
|
|
200
|
+
const suffix = Buffer.from([0, 0]);
|
|
201
|
+
ws.send(Buffer.concat([prefix, Buffer.from(payload), suffix]));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
keepAliveInterval = setInterval(() => {
|
|
205
|
+
if (ws && ws.readyState === ws.OPEN) ws.send(pingPayload);
|
|
206
|
+
}, keepAliveMs);
|
|
207
|
+
|
|
208
|
+
emitter.emit('connected', { reconnectAttempts, timestamp: Date.now() });
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
ws.onmessage = (event) => {
|
|
212
|
+
const { data } = event;
|
|
213
|
+
if (data instanceof Blob) handleMessage(data);
|
|
214
|
+
else if (typeof data === 'string') handleMessage(new Blob([data]));
|
|
215
|
+
else if (data instanceof ArrayBuffer) handleMessage(new Blob([data]));
|
|
216
|
+
else utils.warn('realtime', 'Unknown message type:', typeof data);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
ws.onerror = (err) => {
|
|
220
|
+
if (stopped) return;
|
|
221
|
+
REALTIME_METRICS.errors++;
|
|
222
|
+
logEvent('error', { message: err.message || String(err) });
|
|
223
|
+
utils.error('realtime', 'Socket error:', err.message || err);
|
|
224
|
+
emitter.emit('error', err);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
ws.onclose = (event) => {
|
|
228
|
+
if (stopped) return;
|
|
229
|
+
REALTIME_METRICS.disconnected++;
|
|
230
|
+
logEvent('closed', { code: event.code, reason: event.reason });
|
|
231
|
+
utils.warn('realtime', `Socket closed (code: ${event.code})`);
|
|
232
|
+
clearInterval(keepAliveInterval);
|
|
233
|
+
emitter.emit('disconnected', { code: event.code, reason: event.reason, timestamp: Date.now() });
|
|
234
|
+
scheduleReconnect();
|
|
235
|
+
};
|
|
236
|
+
} catch (err) {
|
|
237
|
+
if (stopped) return;
|
|
238
|
+
REALTIME_METRICS.errors++;
|
|
239
|
+
logEvent('connect_error', { message: err.message });
|
|
240
|
+
utils.error('realtime', 'Connection error:', err.message);
|
|
241
|
+
emitter.emit('error', err);
|
|
242
|
+
clearInterval(keepAliveInterval);
|
|
243
|
+
scheduleReconnect();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
connect();
|
|
248
|
+
|
|
249
|
+
emitter.stop = (reason = 'manual') => {
|
|
250
|
+
stopped = true;
|
|
251
|
+
clearInterval(keepAliveInterval);
|
|
252
|
+
clearTimeout(reconnectTimeout);
|
|
253
|
+
if (ws) { try { ws.close(1000, reason); } catch (_) {} }
|
|
254
|
+
logEvent('stopped', { reason });
|
|
255
|
+
utils.log('realtime', `Stopped (reason: ${reason})`);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
emitter.getMetrics = () => ({
|
|
259
|
+
...REALTIME_METRICS,
|
|
260
|
+
totalMessages,
|
|
261
|
+
uptime: connectionStartTime ? Date.now() - connectionStartTime : 0,
|
|
262
|
+
reconnectAttempts,
|
|
263
|
+
isConnected: ws?.readyState === 1
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
emitter.getLog = (limit = 50) => REALTIME_LOG.slice(0, limit);
|
|
267
|
+
|
|
268
|
+
emitter.addMiddleware = (fn) => {
|
|
269
|
+
if (typeof fn !== 'function') throw new TypeError('realtime.addMiddleware: expected a function');
|
|
270
|
+
MIDDLEWARE_STACK.push(fn);
|
|
271
|
+
return () => { const idx = MIDDLEWARE_STACK.indexOf(fn); if (idx !== -1) MIDDLEWARE_STACK.splice(idx, 1); };
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
emitter.on_type = (type, handler) => {
|
|
275
|
+
if (!NOTIF_HANDLERS.has(type)) NOTIF_HANDLERS.set(type, []);
|
|
276
|
+
NOTIF_HANDLERS.get(type).push(handler);
|
|
277
|
+
return () => {
|
|
278
|
+
const arr = NOTIF_HANDLERS.get(type);
|
|
279
|
+
if (arr) { const i = arr.indexOf(handler); if (i !== -1) arr.splice(i, 1); }
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
emitter.reconnect = () => {
|
|
284
|
+
if (ws) { try { ws.close(); } catch (_) {} }
|
|
285
|
+
clearInterval(keepAliveInterval);
|
|
286
|
+
setTimeout(connect, 100);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
return emitter;
|
|
290
|
+
};
|
|
291
|
+
};
|