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,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const REACTION_CACHE = new Map();
|
|
7
|
+
const REACTION_HISTORY = [];
|
|
8
|
+
const MAX_HISTORY = 300;
|
|
9
|
+
|
|
10
|
+
const VALID_REACTIONS = new Set([
|
|
11
|
+
'❤️', '❤', '😆', '😮', '😢', '😠', '😡', '😍', '😂', '😭',
|
|
12
|
+
'👍', '👎', '🎉', '💯', '🔥', '🤔', '🥰', '🙏', '🤣', '😊',
|
|
13
|
+
'🥺', '💀', '😎', '🤯', '😴', '🤮', '💔', '🫶', '🤝', '💪',
|
|
14
|
+
'🙌', '👀', '🫠', '🤡', '💩', '👻', '🤖', '🔪', '🐒', '🦊',
|
|
15
|
+
'🐱', '🎁', '💋', '😲', '👏', '',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const MULTI_REACT_DELAY_MS = 250;
|
|
19
|
+
|
|
20
|
+
async function retryOp(fn, retries = 3, base = 400) {
|
|
21
|
+
for (let i = 0; i < retries; i++) {
|
|
22
|
+
try { return await fn(); } catch (err) {
|
|
23
|
+
if (i === retries - 1) throw err;
|
|
24
|
+
const isTransient = /network|timeout|ECONNRESET|5\d\d|429/i.test(String(err?.message || err));
|
|
25
|
+
if (!isTransient) throw err;
|
|
26
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mqttPublishReaction(ctx, reaction, messageID, threadID) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
34
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
35
|
+
|
|
36
|
+
const reqID = ctx.wsReqNumber;
|
|
37
|
+
const taskPayload = {
|
|
38
|
+
thread_key: String(threadID),
|
|
39
|
+
timestamp_ms: Date.now(),
|
|
40
|
+
message_id: String(messageID),
|
|
41
|
+
reaction: reaction || '',
|
|
42
|
+
actor_id: ctx.userID,
|
|
43
|
+
reaction_style: null,
|
|
44
|
+
sync_group: 1,
|
|
45
|
+
send_attribution: Math.random() < 0.5 ? 65537 : 524289,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const content = {
|
|
49
|
+
app_id: '2220391788200892',
|
|
50
|
+
payload: JSON.stringify({
|
|
51
|
+
data_trace_id: null,
|
|
52
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
53
|
+
tasks: [{
|
|
54
|
+
failure_count: null,
|
|
55
|
+
label: '29',
|
|
56
|
+
payload: JSON.stringify(taskPayload),
|
|
57
|
+
queue_name: JSON.stringify(['reaction', String(messageID)]),
|
|
58
|
+
task_id: ctx.wsTaskNumber,
|
|
59
|
+
}],
|
|
60
|
+
version_id: '7158486590867448',
|
|
61
|
+
}),
|
|
62
|
+
request_id: reqID,
|
|
63
|
+
type: 3,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let handled = false;
|
|
67
|
+
const onResp = (topic, message) => {
|
|
68
|
+
if (topic !== '/ls_resp' || handled) return;
|
|
69
|
+
let j;
|
|
70
|
+
try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
|
|
71
|
+
if (j.request_id !== reqID) return;
|
|
72
|
+
handled = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
75
|
+
resolve({ success: true, method: 'mqtt_ack', requestID: reqID });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const timer = setTimeout(() => {
|
|
79
|
+
if (!handled) {
|
|
80
|
+
handled = true;
|
|
81
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
82
|
+
resolve({ success: true, method: 'mqtt_noack', requestID: reqID });
|
|
83
|
+
}
|
|
84
|
+
}, 10000);
|
|
85
|
+
|
|
86
|
+
ctx.mqttClient.on('message', onResp);
|
|
87
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, (err) => {
|
|
88
|
+
if (err && !handled) {
|
|
89
|
+
handled = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
92
|
+
reject(err);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function httpSetReaction(defaultFuncs, ctx, reaction, messageID, threadID) {
|
|
99
|
+
const form = {
|
|
100
|
+
message_id: String(messageID),
|
|
101
|
+
thread_fbid: String(threadID),
|
|
102
|
+
reaction: reaction || '',
|
|
103
|
+
viewer_fbid: ctx.userID,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const res = await defaultFuncs.post(
|
|
107
|
+
'https://www.facebook.com/reactions/setmessagereaction',
|
|
108
|
+
ctx.jar,
|
|
109
|
+
form
|
|
110
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
111
|
+
|
|
112
|
+
if (res && res.error) throw res;
|
|
113
|
+
return { success: true, method: 'http', messageID: String(messageID), threadID: String(threadID), reaction };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function recordHistory(op) {
|
|
117
|
+
REACTION_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
118
|
+
if (REACTION_HISTORY.length > MAX_HISTORY) REACTION_HISTORY.length = MAX_HISTORY;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function sendOneEmoji(defaultFuncs, ctx, emoji, messageID, threadID, opts) {
|
|
122
|
+
const { preferMqtt, fallbackHttp } = opts;
|
|
123
|
+
|
|
124
|
+
if (preferMqtt && ctx.mqttClient) {
|
|
125
|
+
try {
|
|
126
|
+
return await retryOp(() => mqttPublishReaction(ctx, emoji, messageID, threadID));
|
|
127
|
+
} catch (mqttErr) {
|
|
128
|
+
utils.warn('setMessageReactionMqtt', `MQTT failed for "${emoji}", trying HTTP: ${mqttErr.message}`);
|
|
129
|
+
if (fallbackHttp) {
|
|
130
|
+
return await retryOp(() => httpSetReaction(defaultFuncs, ctx, emoji, messageID, threadID));
|
|
131
|
+
}
|
|
132
|
+
throw mqttErr;
|
|
133
|
+
}
|
|
134
|
+
} else if (fallbackHttp) {
|
|
135
|
+
return await retryOp(() => httpSetReaction(defaultFuncs, ctx, emoji, messageID, threadID));
|
|
136
|
+
} else {
|
|
137
|
+
throw new Error('setMessageReactionMqtt: MQTT not connected and HTTP fallback disabled');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
142
|
+
|
|
143
|
+
const setMessageReactionMqtt = async function setMessageReactionMqtt(reaction, messageID, threadID, options, callback) {
|
|
144
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
145
|
+
if (!options || typeof options !== 'object') options = {};
|
|
146
|
+
|
|
147
|
+
const {
|
|
148
|
+
skipCache = false,
|
|
149
|
+
preferMqtt = true,
|
|
150
|
+
validate = true,
|
|
151
|
+
fallbackHttp = true,
|
|
152
|
+
delayMs = MULTI_REACT_DELAY_MS,
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
let resolveFunc, rejectFunc;
|
|
156
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
157
|
+
resolveFunc = resolve;
|
|
158
|
+
rejectFunc = reject;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (typeof callback !== 'function') {
|
|
162
|
+
callback = (err, data) => {
|
|
163
|
+
if (err) return rejectFunc(err);
|
|
164
|
+
resolveFunc(data);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (!messageID) throw new Error('setMessageReactionMqtt: messageID is required');
|
|
170
|
+
if (!threadID) throw new Error('setMessageReactionMqtt: threadID is required');
|
|
171
|
+
|
|
172
|
+
const reactions = Array.isArray(reaction) ? reaction : [reaction];
|
|
173
|
+
|
|
174
|
+
if (validate) {
|
|
175
|
+
for (const r of reactions) {
|
|
176
|
+
if (r !== '' && r !== null && !VALID_REACTIONS.has(r)) {
|
|
177
|
+
utils.warn('setMessageReactionMqtt', `Emoji "${r}" is non-standard but will be attempted`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const cacheKey = `react_${messageID}_${ctx.userID}`;
|
|
183
|
+
if (!skipCache && reactions.length === 1) {
|
|
184
|
+
const cached = REACTION_CACHE.get(cacheKey);
|
|
185
|
+
if (cached && cached.reaction === reactions[0] && (Date.now() - cached.ts < 3000)) {
|
|
186
|
+
return callback(null, { success: true, fromCache: true, reaction: reactions[0], messageID: String(messageID) });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await globalShield.addSmartDelay();
|
|
191
|
+
|
|
192
|
+
const opts = { preferMqtt, fallbackHttp };
|
|
193
|
+
const results = [];
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < reactions.length; i++) {
|
|
196
|
+
const emoji = reactions[i];
|
|
197
|
+
const result = await sendOneEmoji(defaultFuncs, ctx, emoji, messageID, threadID, opts);
|
|
198
|
+
results.push({ ...result, reaction: emoji });
|
|
199
|
+
|
|
200
|
+
recordHistory({ reaction: emoji, messageID: String(messageID), threadID: String(threadID), method: result.method });
|
|
201
|
+
utils.log('setMessageReactionMqtt', `Reacted "${emoji}" on message ${messageID}`);
|
|
202
|
+
|
|
203
|
+
if (i < reactions.length - 1) {
|
|
204
|
+
await new Promise(r => setTimeout(r, delayMs + Math.random() * 100));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const lastReaction = reactions[reactions.length - 1];
|
|
209
|
+
REACTION_CACHE.set(cacheKey, { reaction: lastReaction, ts: Date.now() });
|
|
210
|
+
|
|
211
|
+
const out = reactions.length === 1
|
|
212
|
+
? { ...results[0], messageID: String(messageID), threadID: String(threadID) }
|
|
213
|
+
: { success: true, results, messageID: String(messageID), threadID: String(threadID), count: results.length };
|
|
214
|
+
|
|
215
|
+
callback(null, out);
|
|
216
|
+
|
|
217
|
+
} catch (err) {
|
|
218
|
+
utils.error('setMessageReactionMqtt', err);
|
|
219
|
+
callback(err);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return returnPromise;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
setMessageReactionMqtt.batch = async function batchReact(reactions, threadID, options = {}) {
|
|
226
|
+
const results = [];
|
|
227
|
+
const errors = [];
|
|
228
|
+
for (const { reaction, messageID } of reactions) {
|
|
229
|
+
try {
|
|
230
|
+
const r = await setMessageReactionMqtt(reaction, messageID, threadID, options);
|
|
231
|
+
results.push(r);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
errors.push({ messageID, error: err.message });
|
|
234
|
+
}
|
|
235
|
+
await new Promise(r => setTimeout(r, 150 + Math.random() * 100));
|
|
236
|
+
}
|
|
237
|
+
return { success: errors.length === 0, results, errors };
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
setMessageReactionMqtt.remove = function removeReaction(messageID, threadID, options, callback) {
|
|
241
|
+
return setMessageReactionMqtt('', messageID, threadID, options, callback);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
setMessageReactionMqtt.getHistory = (limit = 50) => REACTION_HISTORY.slice(0, limit);
|
|
245
|
+
setMessageReactionMqtt.getValidReactions = () => Array.from(VALID_REACTIONS).filter(r => r !== '');
|
|
246
|
+
|
|
247
|
+
return setMessageReactionMqtt;
|
|
248
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const THEME_CACHE = new Map();
|
|
7
|
+
const THEME_CACHE_TTL = 10 * 60 * 1000;
|
|
8
|
+
const THEME_HISTORY = [];
|
|
9
|
+
const MAX_HISTORY = 200;
|
|
10
|
+
|
|
11
|
+
const COLOR_PALETTE = {
|
|
12
|
+
blue: '196241301102133',
|
|
13
|
+
purple: '370940413392601',
|
|
14
|
+
green: '169463077092846',
|
|
15
|
+
pink: '230032715012014',
|
|
16
|
+
orange: '175615189761153',
|
|
17
|
+
red: '2136751179887052',
|
|
18
|
+
yellow: '2058653964378557',
|
|
19
|
+
teal: '417639218648241',
|
|
20
|
+
black: '539927563794799',
|
|
21
|
+
white: '2873642392710980',
|
|
22
|
+
default: '196241301102133'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
async function retryOp(fn, retries = 4, base = 500) {
|
|
26
|
+
for (let i = 0; i < retries; i++) {
|
|
27
|
+
try { return await fn(); } catch (err) {
|
|
28
|
+
if (i === retries - 1) throw err;
|
|
29
|
+
const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
|
|
30
|
+
if (!transient) throw err;
|
|
31
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function fetchBootloader(defaultFuncs, ctx) {
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
modules: 'LSUpdateThreadTheme,LSUpdateThreadCustomEmoji,LSUpdateThreadThemePayloadCacheKey',
|
|
40
|
+
__aaid: 0,
|
|
41
|
+
__user: ctx.userID,
|
|
42
|
+
__a: 1,
|
|
43
|
+
__req: utils.getSignatureID(),
|
|
44
|
+
__hs: '20352.HYP:comet_pkg.2.1...0',
|
|
45
|
+
dpr: 1,
|
|
46
|
+
__ccg: 'EXCELLENT',
|
|
47
|
+
__rev: '1027396270',
|
|
48
|
+
__s: utils.getSignatureID(),
|
|
49
|
+
fb_dtsg_ag: ctx.fb_dtsg,
|
|
50
|
+
jazoest: ctx.jazoest,
|
|
51
|
+
__spin_r: '1027396270',
|
|
52
|
+
__spin_b: 'trunk',
|
|
53
|
+
__spin_t: now
|
|
54
|
+
});
|
|
55
|
+
await defaultFuncs.get(`https://www.facebook.com/ajax/bootloader-endpoint/?${params}`, ctx.jar)
|
|
56
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchAvailableThemes(defaultFuncs, ctx) {
|
|
60
|
+
const cacheKey = 'available_themes';
|
|
61
|
+
const cached = THEME_CACHE.get(cacheKey);
|
|
62
|
+
if (cached && Date.now() - cached.ts < THEME_CACHE_TTL) return cached.data;
|
|
63
|
+
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const form = {
|
|
66
|
+
av: ctx.userID,
|
|
67
|
+
__user: ctx.userID,
|
|
68
|
+
__a: 1,
|
|
69
|
+
__req: utils.getSignatureID(),
|
|
70
|
+
__hs: '20352.HYP:comet_pkg.2.1...0',
|
|
71
|
+
dpr: 1,
|
|
72
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
73
|
+
jazoest: ctx.jazoest,
|
|
74
|
+
lsd: ctx.fb_dtsg,
|
|
75
|
+
__spin_r: '1027396270',
|
|
76
|
+
__spin_b: 'trunk',
|
|
77
|
+
__spin_t: now,
|
|
78
|
+
fb_api_caller_class: 'RelayModern',
|
|
79
|
+
fb_api_req_friendly_name: 'MWPThreadThemeQuery_AllThemesQuery',
|
|
80
|
+
variables: JSON.stringify({ version: 'default' }),
|
|
81
|
+
server_timestamps: true,
|
|
82
|
+
doc_id: '24474714052117636'
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
|
|
86
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
87
|
+
|
|
88
|
+
const themes = res?.data?.messenger_thread_themes || [];
|
|
89
|
+
THEME_CACHE.set(cacheKey, { data: themes, ts: Date.now() });
|
|
90
|
+
return themes;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function resolveThemeID(themeData, availableThemes) {
|
|
94
|
+
if (typeof themeData === 'string') {
|
|
95
|
+
const s = themeData.trim();
|
|
96
|
+
if (/^[0-9]+$/.test(s)) return { id: s, emoji: '👍' };
|
|
97
|
+
|
|
98
|
+
const found = availableThemes.find(t =>
|
|
99
|
+
t.accessibility_label && t.accessibility_label.toLowerCase().includes(s.toLowerCase())
|
|
100
|
+
);
|
|
101
|
+
if (found) return { id: found.id, emoji: found.default_emoji || '👍' };
|
|
102
|
+
|
|
103
|
+
const paletteID = COLOR_PALETTE[s.toLowerCase()];
|
|
104
|
+
if (paletteID) return { id: paletteID, emoji: '👍' };
|
|
105
|
+
|
|
106
|
+
return { id: COLOR_PALETTE.default, emoji: '👍' };
|
|
107
|
+
} else if (typeof themeData === 'object' && themeData !== null) {
|
|
108
|
+
return {
|
|
109
|
+
id: themeData.themeId || themeData.theme_id || themeData.id || COLOR_PALETTE.default,
|
|
110
|
+
emoji: themeData.emoji || themeData.customEmoji || '👍'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { id: COLOR_PALETTE.default, emoji: '👍' };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function legacySetTheme(defaultFuncs, ctx, threadID, themeId) {
|
|
117
|
+
const legacyBody = {
|
|
118
|
+
dpr: 1,
|
|
119
|
+
queries: JSON.stringify({
|
|
120
|
+
o0: {
|
|
121
|
+
doc_id: '1727493033983591',
|
|
122
|
+
query_params: {
|
|
123
|
+
data: {
|
|
124
|
+
actor_id: ctx.userID,
|
|
125
|
+
client_mutation_id: '0',
|
|
126
|
+
source: 'SETTINGS',
|
|
127
|
+
theme_id: themeId,
|
|
128
|
+
thread_id: threadID
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const res = await defaultFuncs.post('https://www.facebook.com/api/graphqlbatch/', ctx.jar, legacyBody)
|
|
136
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
137
|
+
|
|
138
|
+
if (res && !res[0]?.o0?.errors) return { success: true, method: 'legacy', themeId };
|
|
139
|
+
throw new Error('Legacy theme set failed: ' + JSON.stringify(res?.[0]?.o0?.errors));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function graphqlSetTheme(defaultFuncs, ctx, threadID, themeId, emoji) {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const form = {
|
|
145
|
+
av: ctx.userID,
|
|
146
|
+
__user: ctx.userID,
|
|
147
|
+
__a: 1,
|
|
148
|
+
__req: utils.getSignatureID(),
|
|
149
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
150
|
+
jazoest: ctx.jazoest,
|
|
151
|
+
lsd: ctx.fb_dtsg,
|
|
152
|
+
__spin_r: '1027396270',
|
|
153
|
+
__spin_b: 'trunk',
|
|
154
|
+
__spin_t: now,
|
|
155
|
+
fb_api_caller_class: 'RelayModern',
|
|
156
|
+
fb_api_req_friendly_name: 'MessengerThreadThemeUpdateMutation',
|
|
157
|
+
variables: JSON.stringify({
|
|
158
|
+
input: {
|
|
159
|
+
actor_id: ctx.userID,
|
|
160
|
+
client_mutation_id: String(Math.floor(Math.random() * 10000)),
|
|
161
|
+
source: 'SETTINGS',
|
|
162
|
+
thread_id: threadID.toString(),
|
|
163
|
+
theme_id: themeId.toString(),
|
|
164
|
+
custom_emoji: emoji
|
|
165
|
+
}
|
|
166
|
+
}),
|
|
167
|
+
server_timestamps: true,
|
|
168
|
+
doc_id: '9734829906576883'
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
|
|
172
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
173
|
+
|
|
174
|
+
if (res?.errors?.length) throw new Error('GraphQL theme error: ' + JSON.stringify(res.errors));
|
|
175
|
+
if (res?.data?.messenger_thread_theme_update?.errors?.length) {
|
|
176
|
+
throw new Error('Theme update error: ' + JSON.stringify(res.data.messenger_thread_theme_update.errors));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { success: true, method: 'graphql', themeId };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function mqttSetTheme(ctx, threadID, themeId) {
|
|
183
|
+
if (!ctx.mqttClient) throw new Error('MQTT not connected');
|
|
184
|
+
|
|
185
|
+
const tasks = [
|
|
186
|
+
{ label: 1013, queue: ['ai_generated_theme', String(threadID)] },
|
|
187
|
+
{ label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)] },
|
|
188
|
+
{ label: 1028, queue: ['thread_theme_writer', String(threadID)] },
|
|
189
|
+
{ label: 43, queue: 'thread_theme', extra: { source: null, payload: null } }
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const results = [];
|
|
193
|
+
for (const { label, queue, extra } of tasks) {
|
|
194
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
195
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
196
|
+
|
|
197
|
+
const msg = {
|
|
198
|
+
app_id: '772021112871879',
|
|
199
|
+
payload: JSON.stringify({
|
|
200
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
201
|
+
tasks: [{
|
|
202
|
+
failure_count: null,
|
|
203
|
+
label: String(label),
|
|
204
|
+
payload: JSON.stringify({ thread_key: threadID, theme_fbid: themeId, sync_group: 1, ...(extra || {}) }),
|
|
205
|
+
queue_name: typeof queue === 'string' ? queue : JSON.stringify(queue),
|
|
206
|
+
task_id: ctx.wsTaskNumber
|
|
207
|
+
}],
|
|
208
|
+
version_id: '24227364673632991'
|
|
209
|
+
}),
|
|
210
|
+
request_id: ctx.wsReqNumber,
|
|
211
|
+
type: 3
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(msg), { qos: 1, retain: false }, (err) => {
|
|
216
|
+
if (err) reject(err); else resolve();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
results.push({ label, queue });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { success: true, method: 'mqtt', themeId, tasks: results.length };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function recordHistory(op) {
|
|
226
|
+
THEME_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
227
|
+
if (THEME_HISTORY.length > MAX_HISTORY) THEME_HISTORY.length = MAX_HISTORY;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
231
|
+
return function setThreadTheme(threadID, themeData, options, callback) {
|
|
232
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
233
|
+
if (!options || typeof options !== 'object') options = {};
|
|
234
|
+
const {
|
|
235
|
+
preferMqtt = !!ctx.mqttClient,
|
|
236
|
+
preferLegacy = true,
|
|
237
|
+
skipBootloader = true,
|
|
238
|
+
fetchThemes = true
|
|
239
|
+
} = options;
|
|
240
|
+
|
|
241
|
+
let resolveFunc, rejectFunc;
|
|
242
|
+
const promise = new Promise((resolve, reject) => {
|
|
243
|
+
resolveFunc = resolve;
|
|
244
|
+
rejectFunc = reject;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (typeof callback !== 'function') {
|
|
248
|
+
callback = (err, data) => {
|
|
249
|
+
if (err) return rejectFunc(err);
|
|
250
|
+
resolveFunc(data);
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!threadID) return callback({ error: 'setThreadTheme: threadID is required' });
|
|
255
|
+
|
|
256
|
+
(async () => {
|
|
257
|
+
try {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
|
|
260
|
+
if (!skipBootloader) {
|
|
261
|
+
try { await retryOp(() => fetchBootloader(defaultFuncs, ctx)); } catch (_) {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let availableThemes = [];
|
|
265
|
+
if (fetchThemes) {
|
|
266
|
+
try { availableThemes = await retryOp(() => fetchAvailableThemes(defaultFuncs, ctx)); } catch (_) {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { id: chosenThemeId, emoji: chosenEmoji } = await resolveThemeID(themeData, availableThemes);
|
|
270
|
+
|
|
271
|
+
await globalShield.addSmartDelay();
|
|
272
|
+
|
|
273
|
+
let result;
|
|
274
|
+
|
|
275
|
+
if (preferMqtt && ctx.mqttClient) {
|
|
276
|
+
try {
|
|
277
|
+
result = await retryOp(() => mqttSetTheme(ctx, threadID, chosenThemeId));
|
|
278
|
+
} catch (mqttErr) {
|
|
279
|
+
utils.warn('setThreadTheme', 'MQTT failed, using HTTP:', mqttErr.message);
|
|
280
|
+
if (preferLegacy) {
|
|
281
|
+
try { result = await retryOp(() => legacySetTheme(defaultFuncs, ctx, threadID, chosenThemeId)); }
|
|
282
|
+
catch (_) { result = await retryOp(() => graphqlSetTheme(defaultFuncs, ctx, threadID, chosenThemeId, chosenEmoji)); }
|
|
283
|
+
} else {
|
|
284
|
+
result = await retryOp(() => graphqlSetTheme(defaultFuncs, ctx, threadID, chosenThemeId, chosenEmoji));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} else if (preferLegacy) {
|
|
288
|
+
try { result = await retryOp(() => legacySetTheme(defaultFuncs, ctx, threadID, chosenThemeId)); }
|
|
289
|
+
catch (legacyErr) {
|
|
290
|
+
utils.warn('setThreadTheme', 'Legacy failed, using GraphQL:', legacyErr.message);
|
|
291
|
+
result = await retryOp(() => graphqlSetTheme(defaultFuncs, ctx, threadID, chosenThemeId, chosenEmoji));
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
result = await retryOp(() => graphqlSetTheme(defaultFuncs, ctx, threadID, chosenThemeId, chosenEmoji));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const themeList = availableThemes.length
|
|
298
|
+
? availableThemes.map(t => ({ id: t.id, name: t.accessibility_label, description: t.description }))
|
|
299
|
+
: null;
|
|
300
|
+
|
|
301
|
+
const finalResult = {
|
|
302
|
+
threadID: String(threadID),
|
|
303
|
+
themeId: chosenThemeId,
|
|
304
|
+
customEmoji: chosenEmoji,
|
|
305
|
+
method: result.method,
|
|
306
|
+
success: true,
|
|
307
|
+
timestamp: now,
|
|
308
|
+
availableThemes: themeList
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
THEME_CACHE.set(`thread_theme_${threadID}`, { result: finalResult, ts: Date.now() });
|
|
312
|
+
recordHistory({ threadID: String(threadID), themeId: chosenThemeId, method: result.method });
|
|
313
|
+
utils.log('setThreadTheme', `Set theme ${chosenThemeId} on thread ${threadID} via ${result.method}`);
|
|
314
|
+
|
|
315
|
+
callback(null, finalResult);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
utils.error('setThreadTheme', err);
|
|
318
|
+
callback(err);
|
|
319
|
+
}
|
|
320
|
+
})();
|
|
321
|
+
|
|
322
|
+
return promise;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
Object.assign(module.exports, {
|
|
326
|
+
getColorPalette: () => ({ ...COLOR_PALETTE }),
|
|
327
|
+
getHistory: (limit = 30) => THEME_HISTORY.slice(0, limit),
|
|
328
|
+
clearCache: () => { THEME_CACHE.clear(); return { success: true }; }
|
|
329
|
+
});
|
|
330
|
+
};
|