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,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* theme — Advanced Theme Manager for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Made by Choru Official (original) — massively extended for phantom-fca
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - List available themes with full metadata
|
|
10
|
+
* - Set theme by name (exact, partial, fuzzy), by ID, or by index
|
|
11
|
+
* - AI theme generation and application via createAITheme
|
|
12
|
+
* - MQTT primary path with GraphQL HTTP fallback
|
|
13
|
+
* - Retry and error recovery for MQTT publish failures
|
|
14
|
+
* - Thread-level theme change event object (type, threadID, themeID, senderID, timestamp)
|
|
15
|
+
* - Undo last theme change (restore previous)
|
|
16
|
+
* - Full callback + Promise dual API
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
20
|
+
|
|
21
|
+
// Per-thread undo stack
|
|
22
|
+
const _undoStack = new Map(); // threadID → [{ themeID, themeName }]
|
|
23
|
+
|
|
24
|
+
function pushUndo(threadID, themeID, themeName) {
|
|
25
|
+
if (!_undoStack.has(threadID)) _undoStack.set(threadID, []);
|
|
26
|
+
const stack = _undoStack.get(threadID);
|
|
27
|
+
stack.push({ themeID, themeName, ts: Date.now() });
|
|
28
|
+
if (stack.length > 5) stack.shift(); // keep last 5
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Fuzzy score: higher = better match ────────────────────────────────────────
|
|
32
|
+
function fuzzyScore(name, query) {
|
|
33
|
+
const n = name.toLowerCase(), q = query.toLowerCase();
|
|
34
|
+
if (n === q) return 100;
|
|
35
|
+
if (n.startsWith(q)) return 80;
|
|
36
|
+
if (n.includes(q)) return 60;
|
|
37
|
+
// partial char match
|
|
38
|
+
let qi = 0;
|
|
39
|
+
for (let ni = 0; ni < n.length && qi < q.length; ni++) {
|
|
40
|
+
if (n[ni] === q[qi]) qi++;
|
|
41
|
+
}
|
|
42
|
+
return qi === q.length ? 30 : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
46
|
+
|
|
47
|
+
// ── Fetch the full theme catalogue ─────────────────────────────────────────
|
|
48
|
+
async function fetchThemes(threadID) {
|
|
49
|
+
const form = {
|
|
50
|
+
av: ctx.userID,
|
|
51
|
+
__user: ctx.userID,
|
|
52
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
53
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
54
|
+
fb_api_caller_class: 'RelayModern',
|
|
55
|
+
fb_api_req_friendly_name: 'MWPThreadThemeQuery_AllThemesQuery',
|
|
56
|
+
variables: JSON.stringify({ version: "default" }),
|
|
57
|
+
server_timestamps: true,
|
|
58
|
+
doc_id: '24474714052117636',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const resData = await defaultFuncs
|
|
62
|
+
.post("https://www.facebook.com/api/graphql/", ctx.jar, form, null, {
|
|
63
|
+
"x-fb-friendly-name": "MWPThreadThemeQuery_AllThemesQuery",
|
|
64
|
+
"x-fb-lsd": ctx.lsd,
|
|
65
|
+
"referer": `https://www.facebook.com/messages/t/${threadID}`,
|
|
66
|
+
})
|
|
67
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
68
|
+
|
|
69
|
+
if (resData.errors) throw new Error(JSON.stringify(resData.errors));
|
|
70
|
+
if (!resData.data?.messenger_thread_themes) throw new Error("Could not retrieve theme list");
|
|
71
|
+
|
|
72
|
+
return resData.data.messenger_thread_themes.map(t => {
|
|
73
|
+
if (!t?.id) return null;
|
|
74
|
+
return {
|
|
75
|
+
id: t.id,
|
|
76
|
+
name: t.accessibility_label || t.name || '',
|
|
77
|
+
description: t.description || null,
|
|
78
|
+
appColorMode: t.app_color_mode,
|
|
79
|
+
composerBackgroundColor: t.composer_background_color,
|
|
80
|
+
backgroundGradientColors: t.background_gradient_colors,
|
|
81
|
+
titleBarButtonTintColor: t.title_bar_button_tint_color,
|
|
82
|
+
inboundMessageGradientColors: t.inbound_message_gradient_colors,
|
|
83
|
+
titleBarTextColor: t.title_bar_text_color,
|
|
84
|
+
composerTintColor: t.composer_tint_color,
|
|
85
|
+
titleBarAttributionColor: t.title_bar_attribution_color,
|
|
86
|
+
composerInputBackgroundColor: t.composer_input_background_color,
|
|
87
|
+
hotLikeColor: t.hot_like_color,
|
|
88
|
+
backgroundImage: t.background_asset?.image?.uri || null,
|
|
89
|
+
messageTextColor: t.message_text_color,
|
|
90
|
+
inboundMessageTextColor: t.inbound_message_text_color,
|
|
91
|
+
primaryButtonBackgroundColor: t.primary_button_background_color,
|
|
92
|
+
titleBarBackgroundColor: t.title_bar_background_color,
|
|
93
|
+
tertiaryTextColor: t.tertiary_text_color,
|
|
94
|
+
reactionPillBackgroundColor: t.reaction_pill_background_color,
|
|
95
|
+
secondaryTextColor: t.secondary_text_color,
|
|
96
|
+
fallbackColor: t.fallback_color,
|
|
97
|
+
gradientColors: t.gradient_colors,
|
|
98
|
+
normalThemeId: t.normal_theme_id,
|
|
99
|
+
iconAsset: t.icon_asset?.image?.uri || null,
|
|
100
|
+
};
|
|
101
|
+
}).filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── MQTT theme publisher ────────────────────────────────────────────────────
|
|
105
|
+
async function publishThemeViaMqtt(threadID, themeID, themeName, initiatorID) {
|
|
106
|
+
if (!ctx.mqttClient) throw new Error("Not connected to MQTT");
|
|
107
|
+
|
|
108
|
+
const publish = (label, queueName, extra = {}) => {
|
|
109
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
110
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
111
|
+
const reqId = ctx.wsReqNumber;
|
|
112
|
+
const taskId = ctx.wsTaskNumber;
|
|
113
|
+
|
|
114
|
+
const content = {
|
|
115
|
+
app_id: ctx.appID || '2220391788200892',
|
|
116
|
+
payload: JSON.stringify({
|
|
117
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID ? utils.generateOfflineThreadingID() : Date.now()),
|
|
118
|
+
tasks: [{
|
|
119
|
+
failure_count: null,
|
|
120
|
+
label,
|
|
121
|
+
payload: JSON.stringify({ thread_key: threadID.toString(), theme_fbid: themeID.toString(), sync_group: 1, ...extra }),
|
|
122
|
+
queue_name: queueName,
|
|
123
|
+
task_id: taskId,
|
|
124
|
+
}],
|
|
125
|
+
version_id: '24631415369801570',
|
|
126
|
+
}),
|
|
127
|
+
request_id: reqId,
|
|
128
|
+
type: 3,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return new Promise((res, rej) => {
|
|
132
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, err => {
|
|
133
|
+
if (err) rej(new Error(`MQTT publish failed (label ${label}): ${err.message}`));
|
|
134
|
+
else res();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Publish all four MQTT messages in parallel for speed
|
|
140
|
+
await Promise.all([
|
|
141
|
+
publish('1013', 'ai_generated_theme'),
|
|
142
|
+
publish('1037', 'msgr_custom_thread_theme'),
|
|
143
|
+
publish('1028', 'thread_theme_writer'),
|
|
144
|
+
publish('43', 'thread_theme', { source: null, payload: null }),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
type: "thread_theme_update",
|
|
149
|
+
threadID,
|
|
150
|
+
themeID,
|
|
151
|
+
themeName,
|
|
152
|
+
senderID: initiatorID,
|
|
153
|
+
BotID: ctx.userID,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Argument normaliser ─────────────────────────────────────────────────────
|
|
159
|
+
function normaliseArgs(themeName, threadID, callback, initiatorID) {
|
|
160
|
+
let _cb, _tid = threadID, _init = initiatorID;
|
|
161
|
+
|
|
162
|
+
if (typeof callback === 'function' || typeof callback === 'object' && callback?.constructor?.name === 'AsyncFunction') {
|
|
163
|
+
_cb = callback;
|
|
164
|
+
_init = initiatorID;
|
|
165
|
+
} else if (typeof threadID === 'function') {
|
|
166
|
+
_cb = threadID;
|
|
167
|
+
_tid = null;
|
|
168
|
+
_init = callback;
|
|
169
|
+
} else if (typeof callback === 'string') {
|
|
170
|
+
_init = callback;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { _cb, _tid: _tid || ctx.threadID, _init: _init || ctx.userID };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Main exported function ─────────────────────────────────────────────────
|
|
177
|
+
/**
|
|
178
|
+
* Manage or set the theme for a Messenger thread.
|
|
179
|
+
*
|
|
180
|
+
* @param {string} themeName Theme name / partial name / ID / "list" / "ai:<prompt>" / "undo"
|
|
181
|
+
* @param {string} [threadID] Target thread (required unless ctx.threadID is set)
|
|
182
|
+
* @param {Function} [callback] Node-style (err, result) callback
|
|
183
|
+
* @param {string} [initiatorID] User initiating the change (defaults to bot userID)
|
|
184
|
+
* @returns {Promise}
|
|
185
|
+
*
|
|
186
|
+
* Special values for themeName:
|
|
187
|
+
* "list" → return array of all available themes
|
|
188
|
+
* "undo" → revert to previous theme for this thread
|
|
189
|
+
* "ai:<prompt>" → generate AI theme matching prompt and apply it
|
|
190
|
+
* "#<themeID>" → set by exact ID (e.g. "#196241301102133")
|
|
191
|
+
* "<number>" → match by theme index in the list
|
|
192
|
+
*/
|
|
193
|
+
return async function theme(themeName, threadID, callback, initiatorID) {
|
|
194
|
+
const { _cb: rawCb, _tid, _init } = normaliseArgs(themeName, threadID, callback, initiatorID);
|
|
195
|
+
|
|
196
|
+
let _resolveFunc, _rejectFunc;
|
|
197
|
+
const finalPromise = new Promise((res, rej) => { _resolveFunc = res; _rejectFunc = rej; });
|
|
198
|
+
|
|
199
|
+
const _callback = rawCb || ((err, data) => { if (err) _rejectFunc(err); else _resolveFunc(data); });
|
|
200
|
+
|
|
201
|
+
if (!_tid) return _callback(new Error("theme: threadID is required"));
|
|
202
|
+
if (!themeName) return _callback(new Error("theme: themeName is required"));
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const normalised = themeName.trim().toLowerCase();
|
|
206
|
+
|
|
207
|
+
// ── LIST ──────────────────────────────────────────────────────────────
|
|
208
|
+
if (normalised === 'list') {
|
|
209
|
+
const themes = await fetchThemes(_tid);
|
|
210
|
+
return _callback(null, themes);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ── UNDO ──────────────────────────────────────────────────────────────
|
|
214
|
+
if (normalised === 'undo') {
|
|
215
|
+
const stack = _undoStack.get(_tid);
|
|
216
|
+
if (!stack || stack.length < 2) return _callback(new Error("No previous theme to undo to for this thread"));
|
|
217
|
+
stack.pop(); // remove current
|
|
218
|
+
const prev = stack[stack.length - 1];
|
|
219
|
+
const event = await publishThemeViaMqtt(_tid, prev.themeID, prev.themeName, _init);
|
|
220
|
+
return _callback(null, event);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── AI THEME ──────────────────────────────────────────────────────────
|
|
224
|
+
if (normalised.startsWith('ai:')) {
|
|
225
|
+
const prompt = themeName.slice(3).trim();
|
|
226
|
+
if (!prompt) return _callback(new Error("theme: AI prompt is required after 'ai:'"));
|
|
227
|
+
if (!api.createAITheme) return _callback(new Error("createAITheme is not available"));
|
|
228
|
+
|
|
229
|
+
const aiThemes = await api.createAITheme(prompt, 1, { enrichPrompt: true });
|
|
230
|
+
if (!aiThemes?.length) return _callback(new Error("AI theme generation returned no results"));
|
|
231
|
+
|
|
232
|
+
const best = aiThemes[0];
|
|
233
|
+
if (!best.id) return _callback(new Error("AI theme has no usable ID"));
|
|
234
|
+
|
|
235
|
+
pushUndo(_tid, best.id, best.name || prompt);
|
|
236
|
+
const event = await publishThemeViaMqtt(_tid, best.id, best.name || prompt, _init);
|
|
237
|
+
return _callback(null, { ...event, aiTheme: best });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── SET BY EXPLICIT ID ────────────────────────────────────────────────
|
|
241
|
+
if (themeName.startsWith('#')) {
|
|
242
|
+
const explicitId = themeName.slice(1).trim();
|
|
243
|
+
pushUndo(_tid, explicitId, explicitId);
|
|
244
|
+
const event = await publishThemeViaMqtt(_tid, explicitId, explicitId, _init);
|
|
245
|
+
return _callback(null, event);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── SEARCH IN CATALOGUE ───────────────────────────────────────────────
|
|
249
|
+
const themes = await fetchThemes(_tid);
|
|
250
|
+
|
|
251
|
+
let matched = null;
|
|
252
|
+
|
|
253
|
+
// 1. Exact ID match
|
|
254
|
+
matched = themes.find(t => t.id === normalised);
|
|
255
|
+
|
|
256
|
+
// 2. Numeric index
|
|
257
|
+
if (!matched && /^\d+$/.test(normalised)) {
|
|
258
|
+
const idx = parseInt(normalised, 10);
|
|
259
|
+
matched = themes[idx] || themes.find(t => t.id === normalised);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 3. Exact name match
|
|
263
|
+
if (!matched) matched = themes.find(t => t.name.toLowerCase() === normalised);
|
|
264
|
+
|
|
265
|
+
// 4. Starts-with match
|
|
266
|
+
if (!matched) matched = themes.find(t => t.name.toLowerCase().startsWith(normalised));
|
|
267
|
+
|
|
268
|
+
// 5. Contains match
|
|
269
|
+
if (!matched) matched = themes.find(t => t.name.toLowerCase().includes(normalised));
|
|
270
|
+
|
|
271
|
+
// 6. Fuzzy scoring — pick highest
|
|
272
|
+
if (!matched) {
|
|
273
|
+
const scored = themes
|
|
274
|
+
.map(t => ({ t, score: fuzzyScore(t.name, normalised) }))
|
|
275
|
+
.filter(x => x.score > 0)
|
|
276
|
+
.sort((a, b) => b.score - a.score);
|
|
277
|
+
matched = scored[0]?.t || null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!matched) {
|
|
281
|
+
const names = themes.slice(0, 8).map(t => t.name).join(', ');
|
|
282
|
+
return _callback(new Error(`Theme "${themeName}" not found. Available themes include: ${names}`));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
pushUndo(_tid, matched.id, matched.name);
|
|
286
|
+
const event = await publishThemeViaMqtt(_tid, matched.id, matched.name, _init);
|
|
287
|
+
_callback(null, event);
|
|
288
|
+
|
|
289
|
+
} catch (err) {
|
|
290
|
+
const finalError = err instanceof Error ? err : new Error(err?.message || err?.error || 'Unknown theme error');
|
|
291
|
+
_callback(finalError);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return finalPromise;
|
|
295
|
+
};
|
|
296
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const UNFRIEND_HISTORY = [];
|
|
7
|
+
const MAX_HISTORY = 200;
|
|
8
|
+
const UNDO_STACK = [];
|
|
9
|
+
const MAX_UNDO = 50;
|
|
10
|
+
const CONFIRMATION_CACHE = new Map();
|
|
11
|
+
const CONFIRMATION_TTL = 5000;
|
|
12
|
+
|
|
13
|
+
async function retryOp(fn, retries = 4, base = 600) {
|
|
14
|
+
for (let i = 0; i < retries; i++) {
|
|
15
|
+
try { return await fn(); } catch (err) {
|
|
16
|
+
if (i === retries - 1) throw err;
|
|
17
|
+
const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
|
|
18
|
+
if (!transient) throw err;
|
|
19
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function httpUnfriend(defaultFuncs, ctx, userID) {
|
|
25
|
+
const form = {
|
|
26
|
+
uid: String(userID),
|
|
27
|
+
unref: 'bd_friends_tab',
|
|
28
|
+
floc: 'friends_tab',
|
|
29
|
+
'nctr[_mod]': `pagelet_timeline_app_collection_${ctx.userID}:2356318349:2`
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const res = await defaultFuncs.post(
|
|
33
|
+
'https://www.facebook.com/ajax/profile/removefriendconfirm.php',
|
|
34
|
+
ctx.jar,
|
|
35
|
+
form
|
|
36
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
37
|
+
|
|
38
|
+
if (res && res.error) throw res;
|
|
39
|
+
return { success: true, userID: String(userID), method: 'http', timestamp: Date.now() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function graphqlUnfriend(defaultFuncs, ctx, userID) {
|
|
43
|
+
const form = {
|
|
44
|
+
fb_api_caller_class: 'RelayModern',
|
|
45
|
+
fb_api_req_friendly_name: 'FriendingCometUnfriendMutation',
|
|
46
|
+
variables: JSON.stringify({
|
|
47
|
+
input: {
|
|
48
|
+
source: 'bd_friends_tab',
|
|
49
|
+
unfriend_id: String(userID),
|
|
50
|
+
actor_id: ctx.userID,
|
|
51
|
+
client_mutation_id: String(Math.round(Math.random() * 10000))
|
|
52
|
+
}
|
|
53
|
+
}),
|
|
54
|
+
doc_id: '3662088047145115'
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
|
|
58
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
59
|
+
|
|
60
|
+
if (res?.errors?.length) throw new Error(JSON.stringify(res.errors));
|
|
61
|
+
return { success: true, userID: String(userID), method: 'graphql', timestamp: Date.now() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function recordHistory(op) {
|
|
65
|
+
UNFRIEND_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
66
|
+
if (UNFRIEND_HISTORY.length > MAX_HISTORY) UNFRIEND_HISTORY.length = MAX_HISTORY;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function recordUndo(op) {
|
|
70
|
+
UNDO_STACK.unshift({ ...op, ts: Date.now() });
|
|
71
|
+
if (UNDO_STACK.length > MAX_UNDO) UNDO_STACK.length = MAX_UNDO;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
75
|
+
|
|
76
|
+
const unfriend = async function unfriend(userOrUsers, options, callback) {
|
|
77
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
78
|
+
if (!options || typeof options !== 'object') options = {};
|
|
79
|
+
const {
|
|
80
|
+
preferGraphQL = true,
|
|
81
|
+
batchDelay = 500,
|
|
82
|
+
requireConfirmation = false,
|
|
83
|
+
skipAlreadyUnfriended = false,
|
|
84
|
+
dryRun = false
|
|
85
|
+
} = options;
|
|
86
|
+
|
|
87
|
+
let resolveFunc, rejectFunc;
|
|
88
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
89
|
+
resolveFunc = resolve;
|
|
90
|
+
rejectFunc = reject;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (typeof callback !== 'function') {
|
|
94
|
+
callback = (err, data) => {
|
|
95
|
+
if (err) return rejectFunc(err);
|
|
96
|
+
resolveFunc(data);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const userIDs = Array.isArray(userOrUsers)
|
|
102
|
+
? userOrUsers.map(String)
|
|
103
|
+
: [String(userOrUsers)];
|
|
104
|
+
|
|
105
|
+
if (userIDs.length === 0) throw new Error('unfriend: at least one userID is required');
|
|
106
|
+
|
|
107
|
+
for (const uid of userIDs) {
|
|
108
|
+
if (!uid || uid === 'undefined' || uid === 'null') {
|
|
109
|
+
throw new Error(`unfriend: invalid userID "${uid}"`);
|
|
110
|
+
}
|
|
111
|
+
if (uid === String(ctx.userID)) {
|
|
112
|
+
throw new Error('unfriend: cannot unfriend yourself');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (requireConfirmation) {
|
|
117
|
+
for (const uid of userIDs) {
|
|
118
|
+
const cacheKey = `confirm_${uid}`;
|
|
119
|
+
const cached = CONFIRMATION_CACHE.get(cacheKey);
|
|
120
|
+
if (!cached || Date.now() - cached.ts > CONFIRMATION_TTL) {
|
|
121
|
+
throw new Error(`unfriend: confirmation required for user ${uid} — call unfriend.confirm("${uid}") first`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (dryRun) {
|
|
127
|
+
return callback(null, { dryRun: true, wouldUnfriend: userIDs, timestamp: Date.now() });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await globalShield.addSmartDelay();
|
|
131
|
+
|
|
132
|
+
const results = [];
|
|
133
|
+
const errors = [];
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < userIDs.length; i++) {
|
|
136
|
+
const uid = userIDs[i];
|
|
137
|
+
if (i > 0) await new Promise(r => setTimeout(r, batchDelay + Math.random() * 200));
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
let result;
|
|
141
|
+
|
|
142
|
+
if (preferGraphQL) {
|
|
143
|
+
try {
|
|
144
|
+
result = await retryOp(() => graphqlUnfriend(defaultFuncs, ctx, uid));
|
|
145
|
+
} catch (gqlErr) {
|
|
146
|
+
utils.warn('unfriend', `GraphQL failed for ${uid}, using HTTP:`, gqlErr.message);
|
|
147
|
+
result = await retryOp(() => httpUnfriend(defaultFuncs, ctx, uid));
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
try {
|
|
151
|
+
result = await retryOp(() => httpUnfriend(defaultFuncs, ctx, uid));
|
|
152
|
+
} catch (httpErr) {
|
|
153
|
+
utils.warn('unfriend', `HTTP failed for ${uid}, using GraphQL:`, httpErr.message);
|
|
154
|
+
result = await retryOp(() => graphqlUnfriend(defaultFuncs, ctx, uid));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
results.push(result);
|
|
159
|
+
recordHistory({ userID: uid, method: result.method });
|
|
160
|
+
recordUndo({ userID: uid, action: 'unfriended', method: result.method });
|
|
161
|
+
utils.log('unfriend', `Unfriended user ${uid} via ${result.method}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const isAlreadyUnfriended = /already|not friend|not a friend/i.test(String(err?.message || err));
|
|
164
|
+
if (isAlreadyUnfriended && skipAlreadyUnfriended) {
|
|
165
|
+
results.push({ userID: uid, skipped: true, reason: 'already_unfriended' });
|
|
166
|
+
} else {
|
|
167
|
+
errors.push({ userID: uid, error: err.message || String(err) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const isSingle = userIDs.length === 1;
|
|
173
|
+
const finalResult = isSingle && results.length === 1
|
|
174
|
+
? results[0]
|
|
175
|
+
: { success: errors.length === 0, results, errors: errors.length ? errors : undefined, timestamp: Date.now() };
|
|
176
|
+
|
|
177
|
+
if (errors.length === userIDs.length) {
|
|
178
|
+
return callback(new Error(`unfriend: all ${userIDs.length} unfriend(s) failed — ${errors[0]?.error}`));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
callback(null, finalResult);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
utils.error('unfriend', err);
|
|
184
|
+
callback(err);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return returnPromise;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
unfriend.confirm = (userID) => {
|
|
191
|
+
CONFIRMATION_CACHE.set(`confirm_${userID}`, { ts: Date.now() });
|
|
192
|
+
return { confirmed: true, userID: String(userID), expiresIn: CONFIRMATION_TTL };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
unfriend.getHistory = (limit = 30) => UNFRIEND_HISTORY.slice(0, limit);
|
|
196
|
+
unfriend.getUndoStack = () => [...UNDO_STACK];
|
|
197
|
+
|
|
198
|
+
return unfriend;
|
|
199
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* unsendMessage — Advanced Message Unsend for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Primary: GraphQL mutation (modern, works for sent messages)
|
|
8
|
+
* - Fallback: Legacy messaging/unsend_message endpoint
|
|
9
|
+
* - Batch unsend: pass array of messageIDs
|
|
10
|
+
* - Exponential-backoff retry on transient errors
|
|
11
|
+
* - Full callback + Promise dual API
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
15
|
+
|
|
16
|
+
async function unsendGraphQL(defaultFuncs, ctx, messageID) {
|
|
17
|
+
const form = {
|
|
18
|
+
av: ctx.userID,
|
|
19
|
+
__user: ctx.userID,
|
|
20
|
+
__a: 1,
|
|
21
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
22
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
23
|
+
fb_api_caller_class: "RelayModern",
|
|
24
|
+
fb_api_req_friendly_name: "useUnsendMessageMutation",
|
|
25
|
+
variables: JSON.stringify({
|
|
26
|
+
input: {
|
|
27
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
28
|
+
actor_id: ctx.userID,
|
|
29
|
+
message_id: messageID,
|
|
30
|
+
}
|
|
31
|
+
}),
|
|
32
|
+
server_timestamps: true,
|
|
33
|
+
doc_id: "4719873428063543",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
|
|
37
|
+
const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(res);
|
|
38
|
+
if (checked?.errors) throw new Error(checked.errors[0]?.message || JSON.stringify(checked.errors));
|
|
39
|
+
return { success: true, messageID, method: 'graphql' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function unsendLegacy(defaultFuncs, ctx, messageID) {
|
|
43
|
+
const defData = await defaultFuncs.post(
|
|
44
|
+
"https://www.facebook.com/messaging/unsend_message/",
|
|
45
|
+
ctx.jar,
|
|
46
|
+
{ message_id: messageID }
|
|
47
|
+
);
|
|
48
|
+
const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
|
|
49
|
+
if (resData?.error) throw new Error(JSON.stringify(resData.error));
|
|
50
|
+
return { success: true, messageID, method: 'legacy' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function unsendOne(defaultFuncs, ctx, messageID, retries, baseMs) {
|
|
54
|
+
let lastErr;
|
|
55
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
56
|
+
try {
|
|
57
|
+
return await unsendGraphQL(defaultFuncs, ctx, messageID);
|
|
58
|
+
} catch (primaryErr) {
|
|
59
|
+
try {
|
|
60
|
+
return await unsendLegacy(defaultFuncs, ctx, messageID);
|
|
61
|
+
} catch (fallbackErr) {
|
|
62
|
+
lastErr = primaryErr;
|
|
63
|
+
if (attempt >= retries || /checkpoint|auth|disabled/i.test(primaryErr.message)) throw primaryErr;
|
|
64
|
+
const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
|
|
65
|
+
utils.warn("unsendMessage", `Retry ${attempt}/${retries} for ${messageID} in ${Math.round(wait)}ms`);
|
|
66
|
+
await new Promise(r => setTimeout(r, wait));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw lastErr;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Unsend (recall) a sent message.
|
|
77
|
+
*
|
|
78
|
+
* @param {string|string[]} messageID Message ID or array of IDs for batch unsend
|
|
79
|
+
* @param {object} [options] { retries: number, concurrency: number }
|
|
80
|
+
* @param {Function} [callback] Node-style (err, result) callback
|
|
81
|
+
* @returns {Promise<object|object[]>}
|
|
82
|
+
*/
|
|
83
|
+
return async function unsendMessage(messageID, options, callback) {
|
|
84
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
85
|
+
options = options || {};
|
|
86
|
+
|
|
87
|
+
const retries = options.retries || 3;
|
|
88
|
+
const baseMs = 600;
|
|
89
|
+
const concurrency = options.concurrency || 3;
|
|
90
|
+
|
|
91
|
+
let resolveFunc, rejectFunc;
|
|
92
|
+
const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
|
|
93
|
+
|
|
94
|
+
function done(err, data) {
|
|
95
|
+
if (callback) return err ? callback(err) : callback(null, data);
|
|
96
|
+
if (err) rejectFunc(err); else resolveFunc(data);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ids = Array.isArray(messageID) ? messageID : [messageID];
|
|
100
|
+
|
|
101
|
+
if (!ids.length || ids.some(id => !id)) {
|
|
102
|
+
done(new Error("unsendMessage: messageID is required"));
|
|
103
|
+
return promise;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const results = [];
|
|
108
|
+
// Process in batches respecting concurrency
|
|
109
|
+
for (let i = 0; i < ids.length; i += concurrency) {
|
|
110
|
+
const batch = ids.slice(i, i + concurrency);
|
|
111
|
+
const batchResults = await Promise.all(batch.map(id => unsendOne(defaultFuncs, ctx, id, retries, baseMs)));
|
|
112
|
+
results.push(...batchResults);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const out = ids.length === 1 ? results[0] : { success: true, results };
|
|
116
|
+
done(null, out);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
utils.error("unsendMessage", err.message || err);
|
|
119
|
+
done(err);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return promise;
|
|
123
|
+
};
|
|
124
|
+
};
|