@lazyneoaz/metachat 1.1.3 → 1.1.6
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/package.json +1 -2
- package/src/apis/changeAdminStatus.js +2 -2
- package/src/apis/changeGroupImage.js +1 -1
- package/src/apis/createPoll.js +1 -1
- package/src/apis/deleteMessage.js +9 -90
- package/src/apis/emoji.js +2 -2
- package/src/apis/gcmember.js +87 -131
- package/src/apis/gcname.js +1 -1
- package/src/apis/gcrule.js +24 -20
- package/src/apis/listenMqtt.js +1 -7
- package/src/apis/nickname.js +1 -1
- package/src/apis/pinMessage.js +1 -1
- package/src/apis/sendMessage.js +58 -65
- package/src/apis/sendMessageMqtt.js +3 -21
- package/src/apis/setThreadThemeMqtt.js +6 -7
- package/src/apis/theme.js +2 -2
- package/src/apis/unsendMessage.js +1 -1
- package/src/engine/client.js +1 -1
- package/src/engine/models/buildAPI.js +16 -0
- package/src/utils/antiSuspension.js +0 -29
- package/src/utils/autoReLogin.js +1 -4
- package/src/utils/headers.js +40 -31
- package/src/utils/index.js +13 -2
- package/src/utils/lsRequest.js +1 -1
- package/src/utils/rateLimiter.js +3 -9
- package/src/utils/tokenRefresh.js +9 -1
- package/src/utils/user-agents.js +76 -32
package/src/apis/sendMessage.js
CHANGED
|
@@ -166,7 +166,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
166
166
|
const mention = msg.mentions[i];
|
|
167
167
|
const tag = mention.tag;
|
|
168
168
|
if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
|
|
169
|
-
const offset =
|
|
169
|
+
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
|
170
170
|
if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
|
|
171
171
|
const id = mention.id || 0;
|
|
172
172
|
const emptyChar = '\u200E';
|
|
@@ -285,84 +285,77 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
285
285
|
return callback(new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`"));
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
// Declare typing state here so the finally block never hits a ReferenceError
|
|
289
|
+
// in strict mode. These are set below if simulateTyping is enabled.
|
|
290
|
+
let typingTimeout = null;
|
|
291
|
+
let typingStarted = false;
|
|
292
|
+
|
|
288
293
|
try {
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
// Simulate human typing delay when the option is enabled and there is
|
|
295
|
+
// text to type. The indicator is sent first, we wait the computed delay,
|
|
296
|
+
// then the actual send follows. The finally block stops the indicator.
|
|
297
|
+
if (ctx.globalOptions.simulateTyping && msg.body && typeof api.sendTypingIndicator === 'function') {
|
|
298
|
+
try {
|
|
299
|
+
const typingDelay = await globalAntiSuspension.simulateTyping(threadID, msg.body.length);
|
|
300
|
+
await api.sendTypingIndicator(true, threadID);
|
|
301
|
+
typingStarted = true;
|
|
302
|
+
await new Promise(resolve => {
|
|
303
|
+
typingTimeout = setTimeout(resolve, typingDelay);
|
|
304
|
+
});
|
|
305
|
+
} catch (_) {
|
|
306
|
+
// Typing simulation is best-effort — a failure here must never
|
|
307
|
+
// block the actual message send.
|
|
308
|
+
}
|
|
302
309
|
}
|
|
303
310
|
|
|
304
|
-
const mqttReady = !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
305
|
-
const isMultiRecipient = Array.isArray(threadID);
|
|
306
|
-
// Primary transport: MQTT when connected (faster, lower overhead).
|
|
307
|
-
// Multi-recipient sends always use HTTP (MQTT only handles single thread).
|
|
308
|
-
const preferMqtt = mqttReady && !isMultiRecipient && !!api.sendMessageMqtt;
|
|
309
|
-
|
|
310
|
-
let result;
|
|
311
|
-
let primaryErr;
|
|
312
|
-
let primaryOk = false;
|
|
313
|
-
|
|
314
311
|
try {
|
|
315
|
-
|
|
312
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
313
|
+
const isMultiRecipient = Array.isArray(threadID);
|
|
314
|
+
// Track which transport was used so the catch block can fall back to
|
|
315
|
+
// the OTHER transport — retrying the same one that just failed is useless.
|
|
316
|
+
const usedMqtt = mqttReady && !isMultiRecipient && api.sendMessageMqtt;
|
|
317
|
+
|
|
318
|
+
let result;
|
|
319
|
+
if (usedMqtt) {
|
|
316
320
|
result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
317
321
|
} else {
|
|
318
322
|
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
319
323
|
}
|
|
320
|
-
|
|
321
|
-
} catch (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (primaryOk) {
|
|
326
|
-
return callback(null, result);
|
|
327
|
-
}
|
|
324
|
+
callback(null, result);
|
|
325
|
+
} catch (sendErr) {
|
|
326
|
+
// Fall back to the OTHER transport, not the one that just failed.
|
|
327
|
+
const mqttNowReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
328
|
+
const primaryWasMqtt = mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt;
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
let fallbackErr;
|
|
332
|
-
|
|
333
|
-
if (preferMqtt) {
|
|
334
|
-
// MQTT was primary → fall back to HTTP
|
|
335
|
-
try {
|
|
336
|
-
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
337
|
-
fallbackOk = true;
|
|
338
|
-
} catch (e) {
|
|
339
|
-
fallbackErr = e;
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
// HTTP was primary → fall back to MQTT if available
|
|
343
|
-
const mqttNow = !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
344
|
-
if (mqttNow && !isMultiRecipient && api.sendMessageMqtt) {
|
|
330
|
+
if (primaryWasMqtt) {
|
|
331
|
+
// MQTT was used (or is available) — fall back to HTTP
|
|
345
332
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
} catch (
|
|
349
|
-
|
|
333
|
+
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
334
|
+
callback(null, httpRes);
|
|
335
|
+
} catch (_httpErr) {
|
|
336
|
+
callback(sendErr);
|
|
350
337
|
}
|
|
351
338
|
} else {
|
|
352
|
-
|
|
339
|
+
// HTTP was used — try MQTT if it has since become available
|
|
340
|
+
if (mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
341
|
+
try {
|
|
342
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
343
|
+
callback(null, mqttRes);
|
|
344
|
+
} catch (_mqttErr) {
|
|
345
|
+
callback(sendErr);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
callback(sendErr);
|
|
349
|
+
}
|
|
353
350
|
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const err = primaryErr;
|
|
362
|
-
if (err && fallbackErr && fallbackErr !== primaryErr) {
|
|
363
|
-
err.fallbackError = fallbackErr;
|
|
351
|
+
} finally {
|
|
352
|
+
// Stop typing indicator regardless of success or failure.
|
|
353
|
+
// typingTimeout and typingStarted are always declared above so this
|
|
354
|
+
// block can never throw a ReferenceError in strict mode.
|
|
355
|
+
if (typingTimeout) clearTimeout(typingTimeout);
|
|
356
|
+
if (typingStarted) {
|
|
357
|
+
try { await api.sendTypingIndicator(false, threadID); } catch (_) {}
|
|
364
358
|
}
|
|
365
|
-
return callback(err || fallbackErr);
|
|
366
359
|
}
|
|
367
360
|
} catch (err) {
|
|
368
361
|
callback(err);
|
|
@@ -142,22 +142,10 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
142
142
|
replyToMessage = null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Anti-suspension preparation (circuit-breaker check, smart delay, thread
|
|
146
|
-
// throttling, and daily-stat tracking) is the responsibility of the CALLER.
|
|
147
|
-
// When invoked through api.sendMessage(), preparation is already done there
|
|
148
|
-
// for ALL transport paths (both MQTT and HTTP).
|
|
149
|
-
//
|
|
150
|
-
// If you call api.sendMessageMqtt() directly (bypassing api.sendMessage()),
|
|
151
|
-
// call globalAntiSuspension.prepareBeforeMessage() yourself first and handle
|
|
152
|
-
// the circuit-breaker error it may throw — otherwise your bot has no protection.
|
|
153
|
-
|
|
154
145
|
const normalized = typeof msg === "string" ? { body: msg } : msg;
|
|
155
146
|
const baseBody = normalized.body != null ? String(normalized.body) : "";
|
|
156
147
|
const epoch = (BigInt(Date.now()) << 22n).toString();
|
|
157
|
-
|
|
158
|
-
// request_id, which would cause the ACK listener to match the wrong response.
|
|
159
|
-
if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
|
|
160
|
-
const requestId = ++ctx.wsReqNumber;
|
|
148
|
+
const requestId = Math.floor(100 + Math.random() * 900);
|
|
161
149
|
const otid = utils.generateOfflineThreadingID();
|
|
162
150
|
|
|
163
151
|
const payload0 = {
|
|
@@ -229,12 +217,6 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
229
217
|
payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
|
|
230
218
|
}
|
|
231
219
|
|
|
232
|
-
// Use monotonic task IDs so concurrent sends never reuse the same task_id
|
|
233
|
-
// within a session, which can confuse Facebook's MQTT deduplication.
|
|
234
|
-
if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
|
|
235
|
-
const taskId1 = ++ctx.wsTaskNumber;
|
|
236
|
-
const taskId2 = ++ctx.wsTaskNumber;
|
|
237
|
-
|
|
238
220
|
const content = {
|
|
239
221
|
app_id: "2220391788200892",
|
|
240
222
|
payload: {
|
|
@@ -243,7 +225,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
243
225
|
label: "46",
|
|
244
226
|
payload: payload0,
|
|
245
227
|
queue_name: String(threadID),
|
|
246
|
-
task_id:
|
|
228
|
+
task_id: 400,
|
|
247
229
|
failure_count: null,
|
|
248
230
|
},
|
|
249
231
|
{
|
|
@@ -254,7 +236,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
254
236
|
sync_group: 1,
|
|
255
237
|
},
|
|
256
238
|
queue_name: String(threadID),
|
|
257
|
-
task_id:
|
|
239
|
+
task_id: 401,
|
|
258
240
|
failure_count: null,
|
|
259
241
|
},
|
|
260
242
|
],
|
|
@@ -47,17 +47,16 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const messageDefs = [
|
|
50
|
-
{ label: 1013, queue: ['ai_generated_theme', String(threadID)]
|
|
51
|
-
{ label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)]
|
|
52
|
-
{ label: 1028, queue: ['thread_theme_writer', String(threadID)]
|
|
53
|
-
|
|
54
|
-
{ label: 43, queue: 'thread_theme', extra: { source: null, payload: null }, app_id: '2220391788200892' },
|
|
50
|
+
{ label: 1013, queue: ['ai_generated_theme', String(threadID)] },
|
|
51
|
+
{ label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)] },
|
|
52
|
+
{ label: 1028, queue: ['thread_theme_writer', String(threadID)] },
|
|
53
|
+
{ label: 43, queue: 'thread_theme', extra: { source: null, payload: null } },
|
|
55
54
|
];
|
|
56
55
|
|
|
57
|
-
const messages = messageDefs.map(({ label, queue, extra
|
|
56
|
+
const messages = messageDefs.map(({ label, queue, extra }) => {
|
|
58
57
|
ctx.wsReqNumber += 1;
|
|
59
58
|
return {
|
|
60
|
-
app_id,
|
|
59
|
+
app_id: '772021112871879',
|
|
61
60
|
payload: JSON.stringify({
|
|
62
61
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
63
62
|
tasks: [makeTask(label, queue, extra)],
|
package/src/apis/theme.js
CHANGED
|
@@ -150,11 +150,11 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
const context = {
|
|
153
|
-
app_id:
|
|
153
|
+
app_id: ctx.appID,
|
|
154
154
|
payload: {
|
|
155
155
|
epoch_id: currentEpochId,
|
|
156
156
|
tasks: [query],
|
|
157
|
-
version_id: '
|
|
157
|
+
version_id: '24631415369801570',
|
|
158
158
|
},
|
|
159
159
|
request_id: request_id,
|
|
160
160
|
type: 3,
|
|
@@ -33,7 +33,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
33
33
|
const taskId = ++ctx.wsTaskNumber;
|
|
34
34
|
|
|
35
35
|
const content = {
|
|
36
|
-
app_id: "2220391788200892",
|
|
36
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
37
37
|
payload: JSON.stringify({
|
|
38
38
|
data_trace_id: null,
|
|
39
39
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
package/src/engine/client.js
CHANGED
|
@@ -26,7 +26,7 @@ const DEFAULT_OPTIONS = {
|
|
|
26
26
|
autoReconnect: true,
|
|
27
27
|
online: true,
|
|
28
28
|
emitReady: false,
|
|
29
|
-
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
29
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.182 Safari/537.36",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -74,6 +74,16 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
74
74
|
let region = mqttEndpoint ? new URL(mqttEndpoint).searchParams.get("region")?.toUpperCase() : undefined;
|
|
75
75
|
const irisSeqIDMatch = html.match(/irisSeqID:"(.+?)"/);
|
|
76
76
|
const irisSeqID = irisSeqIDMatch ? irisSeqIDMatch[1] : null;
|
|
77
|
+
|
|
78
|
+
// Extract Facebook client-state tokens that are embedded in every page load.
|
|
79
|
+
// Real browsers include these in every subsequent request to prove the request
|
|
80
|
+
// originated from a genuine page load. Missing them is a strong bot signal.
|
|
81
|
+
const spinRMatch = html.match(/"__spin_r"\s*:\s*(\d+)/);
|
|
82
|
+
const spinTMatch = html.match(/"__spin_t"\s*:\s*(\d+)/);
|
|
83
|
+
const __spin_r = spinRMatch ? parseInt(spinRMatch[1], 10) : null;
|
|
84
|
+
const __spin_t = spinTMatch ? parseInt(spinTMatch[1], 10) : null;
|
|
85
|
+
const __spin_b = 'trunk';
|
|
86
|
+
|
|
77
87
|
if (globalOptions.bypassRegion && mqttEndpoint) {
|
|
78
88
|
const currentEndpoint = new URL(mqttEndpoint);
|
|
79
89
|
currentEndpoint.searchParams.set('region', globalOptions.bypassRegion.toLowerCase());
|
|
@@ -116,6 +126,12 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
|
|
|
116
126
|
validator: globalValidator,
|
|
117
127
|
_emitter: emitter,
|
|
118
128
|
ttstamp,
|
|
129
|
+
// Client-state spin tokens extracted from the page HTML.
|
|
130
|
+
// These are included in every subsequent request body to prove the
|
|
131
|
+
// request originated from a real page load (not a standalone script).
|
|
132
|
+
__spin_r,
|
|
133
|
+
__spin_t,
|
|
134
|
+
__spin_b,
|
|
119
135
|
...dtsgResult,
|
|
120
136
|
};
|
|
121
137
|
const defaultFuncs = utils.makeDefaults(html, userID, ctx);
|
|
@@ -459,35 +459,6 @@ class AntiSuspension {
|
|
|
459
459
|
return results;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/**
|
|
463
|
-
* Prepare before sending — single delay model.
|
|
464
|
-
* Enforces smart delay, thread throttle, and volume limits, respects circuit breaker.
|
|
465
|
-
* If the circuit breaker is tripped (checkpoint/suspension detected) or
|
|
466
|
-
* volume limits are reached, throws to protect the account.
|
|
467
|
-
*/
|
|
468
|
-
async prepareBeforeMessage(threadID, message) {
|
|
469
|
-
if (this.isCircuitBreakerTripped()) {
|
|
470
|
-
const remaining = this.getCircuitBreakerRemainingMs();
|
|
471
|
-
const { utils } = this._getUtils();
|
|
472
|
-
utils && utils.warn && utils.warn("AntiSuspension",
|
|
473
|
-
`Circuit breaker is tripped. Blocking message send to protect account. ` +
|
|
474
|
-
`Cooldown remaining: ${Math.ceil(remaining / 1000)}s`);
|
|
475
|
-
const err = new Error(`Circuit breaker is tripped. Pausing sends to protect account. Retry in ${Math.ceil(remaining / 1000)}s.`);
|
|
476
|
-
err.error = 'circuit_breaker_tripped';
|
|
477
|
-
err.remainingMs = remaining;
|
|
478
|
-
throw err;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const volumeWarning = this.checkVolumeLimit(threadID);
|
|
482
|
-
if (volumeWarning) {
|
|
483
|
-
const { utils } = this._getUtils();
|
|
484
|
-
utils && utils.warn && utils.warn("AntiSuspension", volumeWarning);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
await this.enforceThreadThrottling(threadID);
|
|
488
|
-
this._incrementDailyStats(threadID);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
462
|
getConfig() {
|
|
492
463
|
return {
|
|
493
464
|
messageDelayMs: this.messageDelayMs,
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -205,10 +205,7 @@ class AutoReLoginManager {
|
|
|
205
205
|
|
|
206
206
|
async pauseAPIRequests() {
|
|
207
207
|
utils.log("AutoReLogin", "Pausing API requests during re-login...");
|
|
208
|
-
|
|
209
|
-
// re-logging in. A 1s pause was too short — the session cookies could
|
|
210
|
-
// change mid-request, causing follow-up requests to arrive with stale auth.
|
|
211
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
resolvePendingRequests(success) {
|
package/src/utils/headers.js
CHANGED
|
@@ -28,9 +28,6 @@ function sanitizeHeaders(headers) {
|
|
|
28
28
|
const out = {};
|
|
29
29
|
for (const [key, value] of Object.entries(headers)) {
|
|
30
30
|
if (!key || typeof key !== 'string') continue;
|
|
31
|
-
// Skip undefined/null values — these are intentional omissions (e.g.
|
|
32
|
-
// 'Upgrade-Insecure-Requests' on XHR requests that browsers don't send).
|
|
33
|
-
if (value === undefined || value === null) continue;
|
|
34
31
|
const cleanKey = key.replace(/[^\x21-\x7E]/g, '').trim();
|
|
35
32
|
if (!cleanKey) continue;
|
|
36
33
|
const cleanVal = sanitizeHeaderValue(value);
|
|
@@ -62,16 +59,27 @@ function getRandomTimezone() {
|
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* Generates a comprehensive and realistic set of headers for requests to Facebook.
|
|
62
|
+
*
|
|
63
|
+
* Key fingerprint rules enforced here:
|
|
64
|
+
* - Navigate (page load): Accept = full HTML accept, Upgrade-Insecure-Requests = 1,
|
|
65
|
+
* Sec-Fetch-Dest/Mode/Site = document/navigate/none. NO Origin.
|
|
66
|
+
* - XHR (AJAX/API call): Accept = *\/*, Sec-Fetch-Dest/Mode/Site = empty/cors/same-origin,
|
|
67
|
+
* Origin = host, X-Requested-With = XMLHttpRequest. NO Upgrade-Insecure-Requests.
|
|
68
|
+
*
|
|
69
|
+
* X-Requested-With is sent consistently on all XHR/API calls. Consistency matters —
|
|
70
|
+
* sending it sometimes but not others is itself a detectable pattern.
|
|
71
|
+
*
|
|
65
72
|
* @param {string} url - The target URL.
|
|
66
73
|
* @param {object} options - Global options from context.
|
|
67
74
|
* @param {object} ctx - The application context (containing fb_dtsg, lsd, etc.).
|
|
68
75
|
* @param {object} customHeader - Any extra headers to merge.
|
|
69
|
-
* @param {string} requestType -
|
|
76
|
+
* @param {string} requestType - 'xhr' for GraphQL/AJAX or 'navigate' for page navigation.
|
|
70
77
|
* @returns {object} A complete headers object.
|
|
71
78
|
*/
|
|
72
79
|
function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
73
80
|
const persona = options?.persona || 'desktop';
|
|
74
81
|
const isAndroid = persona === 'android' || persona === 'mobile';
|
|
82
|
+
const isXhr = requestType === 'xhr';
|
|
75
83
|
|
|
76
84
|
let userAgent, secChUa, secChUaFullVersionList, secChUaPlatform, secChUaPlatformVersion;
|
|
77
85
|
let androidData = null;
|
|
@@ -113,8 +121,6 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
113
121
|
const host = new URL(url).hostname;
|
|
114
122
|
const referer = `https://${host}/`;
|
|
115
123
|
|
|
116
|
-
const isXhr = requestType === 'xhr';
|
|
117
|
-
|
|
118
124
|
const locales = options?.cachedLocale || (androidData?.locale ? androidData.locale.replace('_', '-') : getRandomLocale());
|
|
119
125
|
|
|
120
126
|
if (isAndroid) {
|
|
@@ -140,12 +146,9 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
140
146
|
if (ctx.region) {
|
|
141
147
|
headers['X-MSGR-Region'] = ctx.region;
|
|
142
148
|
}
|
|
143
|
-
if (ctx.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (__spin_b) headers['X-Fb-Spin-B'] = String(__spin_b);
|
|
147
|
-
if (__spin_t) headers['X-Fb-Spin-T'] = String(__spin_t);
|
|
148
|
-
}
|
|
149
|
+
if (ctx.__spin_r != null) headers['X-Fb-Spin-R'] = String(ctx.__spin_r);
|
|
150
|
+
if (ctx.__spin_b) headers['X-Fb-Spin-B'] = String(ctx.__spin_b);
|
|
151
|
+
if (ctx.__spin_t != null) headers['X-Fb-Spin-T'] = String(ctx.__spin_t);
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
if (customHeader) {
|
|
@@ -162,10 +165,14 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
162
165
|
const isMac = secChUaPlatform === '"macOS"';
|
|
163
166
|
const isLinux = secChUaPlatform === '"Linux"';
|
|
164
167
|
|
|
168
|
+
// Navigate (page load) Accept mirrors real Chrome — includes avif/webp/apng
|
|
169
|
+
// XHR Accept is simply */* as sent by Chrome's fetch/XHR APIs
|
|
170
|
+
const acceptHeader = isXhr
|
|
171
|
+
? '*/*'
|
|
172
|
+
: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7';
|
|
173
|
+
|
|
165
174
|
const headers = {
|
|
166
|
-
'Accept':
|
|
167
|
-
? 'application/json, text/plain, */*'
|
|
168
|
-
: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
175
|
+
'Accept': acceptHeader,
|
|
169
176
|
'Accept-Language': locales,
|
|
170
177
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
171
178
|
'Cache-Control': 'no-cache',
|
|
@@ -177,26 +184,30 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
177
184
|
'Sec-Ch-Ua': secChUa,
|
|
178
185
|
'Sec-Ch-Ua-Full-Version-List': secChUaFullVersionList,
|
|
179
186
|
'Sec-Ch-Ua-Mobile': '?0',
|
|
187
|
+
'Sec-Ch-Ua-Model': '""',
|
|
180
188
|
'Sec-Ch-Ua-Platform': secChUaPlatform,
|
|
181
189
|
'Sec-Ch-Ua-Platform-Version': secChUaPlatformVersion,
|
|
182
190
|
'Sec-Fetch-Dest': isXhr ? 'empty' : 'document',
|
|
183
191
|
'Sec-Fetch-Mode': isXhr ? 'cors' : 'navigate',
|
|
184
192
|
'Sec-Fetch-Site': isXhr ? 'same-origin' : 'none',
|
|
185
193
|
'User-Agent': userAgent,
|
|
186
|
-
'Upgrade-Insecure-Requests': isXhr ? undefined : '1',
|
|
187
|
-
// X-Requested-With is only sent by XHR calls, NOT by page navigations.
|
|
188
|
-
// Sending it on navigate requests is a clear bot-detection fingerprint
|
|
189
|
-
// because real browsers never include it for document loads.
|
|
190
|
-
...(isXhr ? { 'X-Requested-With': 'XMLHttpRequest' } : {})
|
|
191
194
|
};
|
|
192
195
|
|
|
193
|
-
if (
|
|
194
|
-
headers['
|
|
195
|
-
headers['Sec-Ch-Ua-Bitness'] = '"64"';
|
|
196
|
+
if (!isXhr) {
|
|
197
|
+
headers['Upgrade-Insecure-Requests'] = '1';
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
if (isXhr) {
|
|
199
201
|
headers['Origin'] = `https://${host}`;
|
|
202
|
+
// X-Requested-With is sent consistently on all XHR/API requests.
|
|
203
|
+
// Consistency matters — sending it sometimes but not others is itself
|
|
204
|
+
// a detectable pattern. Keep it always-on for API calls.
|
|
205
|
+
headers['X-Requested-With'] = 'XMLHttpRequest';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (isWindows || isMac || isLinux) {
|
|
209
|
+
headers['Sec-Ch-Ua-Arch'] = '"x86"';
|
|
210
|
+
headers['Sec-Ch-Ua-Bitness'] = '"64"';
|
|
200
211
|
}
|
|
201
212
|
|
|
202
213
|
if (ctx) {
|
|
@@ -206,15 +217,13 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
206
217
|
if (ctx.region) {
|
|
207
218
|
headers['X-MSGR-Region'] = ctx.region;
|
|
208
219
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
220
|
+
// Spin tokens are stored directly on ctx by buildAPI — ctx.master is
|
|
221
|
+
// never populated and reading from it was dead code that never fired.
|
|
222
|
+
if (ctx.__spin_r != null) headers['X-Fb-Spin-R'] = String(ctx.__spin_r);
|
|
223
|
+
if (ctx.__spin_b) headers['X-Fb-Spin-B'] = String(ctx.__spin_b);
|
|
224
|
+
if (ctx.__spin_t != null) headers['X-Fb-Spin-T'] = String(ctx.__spin_t);
|
|
215
225
|
}
|
|
216
226
|
|
|
217
|
-
|
|
218
227
|
if (customHeader) {
|
|
219
228
|
Object.assign(headers, customHeader);
|
|
220
229
|
if (customHeader.noRef) {
|
|
@@ -232,4 +241,4 @@ module.exports = {
|
|
|
232
241
|
meta,
|
|
233
242
|
getRandomLocale,
|
|
234
243
|
getRandomTimezone
|
|
235
|
-
};
|
|
244
|
+
};
|
package/src/utils/index.js
CHANGED
|
@@ -59,7 +59,6 @@ function makeDefaults(html, userID, ctx) {
|
|
|
59
59
|
const network = require("./axios");
|
|
60
60
|
const constants = require("./constants");
|
|
61
61
|
|
|
62
|
-
let reqCounter = 1;
|
|
63
62
|
const revision = constants.getFrom(html, 'revision":', ",");
|
|
64
63
|
|
|
65
64
|
/**
|
|
@@ -68,10 +67,13 @@ function makeDefaults(html, userID, ctx) {
|
|
|
68
67
|
* @returns {object} The merged object.
|
|
69
68
|
*/
|
|
70
69
|
function mergeWithDefaults(obj) {
|
|
70
|
+
// __req must be random, not a sequential counter. A counter that resets to 1
|
|
71
|
+
// on every session start is a textbook bot fingerprint that Facebook's systems
|
|
72
|
+
// detect. Random values match real Chrome behaviour.
|
|
71
73
|
const newObj = {
|
|
72
74
|
av: userID,
|
|
73
75
|
__user: userID,
|
|
74
|
-
__req: (
|
|
76
|
+
__req: Math.floor(Math.random() * 36 ** 2).toString(36),
|
|
75
77
|
__rev: revision,
|
|
76
78
|
__a: 1,
|
|
77
79
|
...(ctx && {
|
|
@@ -80,6 +82,15 @@ function makeDefaults(html, userID, ctx) {
|
|
|
80
82
|
}),
|
|
81
83
|
};
|
|
82
84
|
|
|
85
|
+
// Include Facebook's client-state spin tokens when present. These are
|
|
86
|
+
// extracted from the page HTML at login time and prove each request
|
|
87
|
+
// originated from a real page load, not a standalone script.
|
|
88
|
+
if (ctx) {
|
|
89
|
+
if (ctx.__spin_r != null) newObj.__spin_r = ctx.__spin_r;
|
|
90
|
+
if (ctx.__spin_t != null) newObj.__spin_t = ctx.__spin_t;
|
|
91
|
+
if (ctx.__spin_b) newObj.__spin_b = ctx.__spin_b;
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
if (!obj) return newObj;
|
|
84
95
|
|
|
85
96
|
for (const prop in obj) {
|
package/src/utils/lsRequest.js
CHANGED
|
@@ -122,7 +122,7 @@ function buildLsEnvelope(ctx, tasks, versionId = "25459622483894963") {
|
|
|
122
122
|
const requestId = ++ctx.wsReqNumber;
|
|
123
123
|
|
|
124
124
|
const envelope = {
|
|
125
|
-
app_id: "2220391788200892",
|
|
125
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
126
126
|
payload: JSON.stringify({
|
|
127
127
|
epoch_id: generateEpochId(),
|
|
128
128
|
tasks,
|
package/src/utils/rateLimiter.js
CHANGED
|
@@ -14,12 +14,8 @@ class RateLimiter {
|
|
|
14
14
|
|
|
15
15
|
this.ERROR_CACHE_TTL = 300000;
|
|
16
16
|
this.COOLDOWN_DURATION = 60000;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// etc.) and caused unnecessary delays that looked bot-like when many
|
|
20
|
-
// requests were queued. Facebook's actual rate-limiting kicks in far higher.
|
|
21
|
-
this.MAX_REQUESTS_PER_MINUTE = 120;
|
|
22
|
-
this.MAX_CONCURRENT_REQUESTS = 8;
|
|
17
|
+
this.MAX_REQUESTS_PER_MINUTE = 50;
|
|
18
|
+
this.MAX_CONCURRENT_REQUESTS = 5;
|
|
23
19
|
|
|
24
20
|
this.activeRequests = 0;
|
|
25
21
|
|
|
@@ -29,9 +25,7 @@ class RateLimiter {
|
|
|
29
25
|
|
|
30
26
|
// Per-endpoint sliding windows
|
|
31
27
|
this._endpointWindows = new Map();
|
|
32
|
-
|
|
33
|
-
// burst-write scenarios (e.g. thread hydration after reconnect) unnecessarily.
|
|
34
|
-
this._MAX_PER_ENDPOINT_PER_MINUTE = 40;
|
|
28
|
+
this._MAX_PER_ENDPOINT_PER_MINUTE = 20;
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
configure(opts = {}) {
|
|
@@ -111,7 +111,7 @@ class TokenRefreshManager {
|
|
|
111
111
|
const resp = await utils.get(
|
|
112
112
|
probeUrl,
|
|
113
113
|
ctx.jar,
|
|
114
|
-
{ reason: 'reconnect', __a: 1, __req:
|
|
114
|
+
{ reason: 'reconnect', __a: 1, __req: Math.floor(Math.random() * 36 ** 2).toString(36) },
|
|
115
115
|
ctx.globalOptions,
|
|
116
116
|
probeCtx
|
|
117
117
|
);
|
|
@@ -236,6 +236,14 @@ class TokenRefreshManager {
|
|
|
236
236
|
ctx.__rev = revisionMatch[1];
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
// Refresh spin tokens alongside fb_dtsg — they are page-state values
|
|
240
|
+
// embedded in every HTML response and must stay current so requests
|
|
241
|
+
// continue to pass Facebook's client-state validation.
|
|
242
|
+
const spinRMatch = html.match(/"__spin_r"\s*:\s*(\d+)/);
|
|
243
|
+
const spinTMatch = html.match(/"__spin_t"\s*:\s*(\d+)/);
|
|
244
|
+
if (spinRMatch) ctx.__spin_r = parseInt(spinRMatch[1], 10);
|
|
245
|
+
if (spinTMatch) ctx.__spin_t = parseInt(spinTMatch[1], 10);
|
|
246
|
+
|
|
239
247
|
this.lastRefresh = Date.now();
|
|
240
248
|
this.failureCount = 0;
|
|
241
249
|
|