@lazyneoaz/metachat 1.0.6 → 1.0.8
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 -1
- package/src/apis/listenMqtt.js +47 -27
- package/src/apis/refreshFb_dtsg.js +7 -1
- package/src/apis/sendMessage.js +23 -11
- package/src/utils/antiSuspension.js +21 -4
- package/src/utils/autoReLogin.js +22 -31
- package/src/utils/axios.js +15 -1
- package/src/utils/clients.js +87 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -688,23 +688,15 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
688
688
|
timestamp: Date.now()
|
|
689
689
|
}, null);
|
|
690
690
|
}
|
|
691
|
+
// handleSessionExpiry owns the listenMqtt restart after re-login —
|
|
692
|
+
// it captures wasListening from the OLD ctx before calling loginHelper,
|
|
693
|
+
// so it is the only place that reliably knows whether listening was active.
|
|
694
|
+
// DO NOT restart listenMqtt here; doing so races with handleSessionExpiry
|
|
695
|
+
// and creates two simultaneous MQTT connections.
|
|
691
696
|
try {
|
|
692
697
|
if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
693
|
-
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired")
|
|
694
|
-
|
|
695
|
-
try {
|
|
696
|
-
if (typeof api.stopListening === 'function') {
|
|
697
|
-
try { api.stopListening(); } catch (_) {}
|
|
698
|
-
}
|
|
699
|
-
const cb = ctx._lastListenCallback || null;
|
|
700
|
-
if (cb) {
|
|
701
|
-
api.listenMqtt(cb);
|
|
702
|
-
} else {
|
|
703
|
-
api.listenMqtt();
|
|
704
|
-
}
|
|
705
|
-
} catch (_) {}
|
|
706
|
-
}
|
|
707
|
-
}).catch(() => {});
|
|
698
|
+
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired")
|
|
699
|
+
.catch(() => {});
|
|
708
700
|
}
|
|
709
701
|
} catch (_) {}
|
|
710
702
|
}
|
|
@@ -816,16 +808,39 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
816
808
|
} catch (err) {
|
|
817
809
|
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
818
810
|
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
utils.error("MQTT",
|
|
826
|
-
|
|
811
|
+
|
|
812
|
+
const isNotLoggedIn = /Not logged in/i.test(msg);
|
|
813
|
+
const isLoginBlocked = /blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir/i.test(msg);
|
|
814
|
+
|
|
815
|
+
if (isNotLoggedIn || isLoginBlocked) {
|
|
816
|
+
const reason = isLoginBlocked ? "login_blocked" : "not_logged_in";
|
|
817
|
+
utils.error("MQTT", `Auth error in getSeqID: ${reason} — attempting auto re-login`);
|
|
818
|
+
|
|
819
|
+
// Mirror the close-handler re-login pattern: try handleSessionExpiry first.
|
|
820
|
+
// If it succeeds we schedule a reconnect; only fall back to emitAuthError
|
|
821
|
+
// (which kills listening) when re-login is unavailable or already in progress.
|
|
822
|
+
if (!ctx._mqttReauthing && globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
823
|
+
ctx._mqttReauthing = true;
|
|
824
|
+
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', msg)
|
|
825
|
+
.then((ok) => {
|
|
826
|
+
ctx._mqttReauthing = false;
|
|
827
|
+
if (ok && ctx.globalOptions.autoReconnect) {
|
|
828
|
+
ctx._reconnectAttempts = 0;
|
|
829
|
+
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
830
|
+
} else if (!ok) {
|
|
831
|
+
emitAuthError(reason, msg);
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
.catch(() => {
|
|
835
|
+
ctx._mqttReauthing = false;
|
|
836
|
+
emitAuthError(reason, msg);
|
|
837
|
+
});
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
return emitAuthError(reason, msg);
|
|
827
842
|
}
|
|
828
|
-
|
|
843
|
+
|
|
829
844
|
utils.error("MQTT", "getSeqID error:", msg);
|
|
830
845
|
if (ctx.globalOptions.autoReconnect) {
|
|
831
846
|
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
@@ -1020,9 +1035,14 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1020
1035
|
'account_inactive', 'checkpoint_282', 'checkpoint_956', 'error'
|
|
1021
1036
|
];
|
|
1022
1037
|
for (const event of LIFECYCLE_EVENTS) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1038
|
+
// Use listeners() NOT rawListeners() — rawListeners() returns the
|
|
1039
|
+
// internal once-wrapper functions. When re-registered with .on()
|
|
1040
|
+
// those wrappers fire the handler once then remove themselves from
|
|
1041
|
+
// the OLD emitter, but stay on the NEW emitter forever, causing a
|
|
1042
|
+
// memory leak and never-again-firing once handlers.
|
|
1043
|
+
const fns = prevEmitter.listeners ? prevEmitter.listeners(event) : [];
|
|
1044
|
+
for (const fn of fns) {
|
|
1045
|
+
msgEmitter.on(event, fn);
|
|
1026
1046
|
}
|
|
1027
1047
|
}
|
|
1028
1048
|
} catch (_) {}
|
|
@@ -60,7 +60,13 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
60
60
|
|
|
61
61
|
if (!fb_dtsg) throw new Error("Could not find fb_dtsg in HTML. Session may have expired.");
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
// Recalculate ttstamp whenever fb_dtsg changes — ttstamp = "2" followed
|
|
64
|
+
// by the concatenation of each character's char code (NOT their sum).
|
|
65
|
+
// Leaving ttstamp stale after a token refresh causes request signature
|
|
66
|
+
// mismatches that Facebook detects as bot-like behaviour.
|
|
67
|
+
const ttstamp = "2" + Array.from(fb_dtsg).map(c => c.charCodeAt(0)).join("");
|
|
68
|
+
|
|
69
|
+
const updated = { fb_dtsg, ttstamp };
|
|
64
70
|
if (jazoest) updated.jazoest = jazoest;
|
|
65
71
|
|
|
66
72
|
Object.assign(ctx, updated);
|
package/src/apis/sendMessage.js
CHANGED
|
@@ -300,32 +300,44 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
try {
|
|
303
|
-
let result;
|
|
304
303
|
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
305
304
|
const isMultiRecipient = Array.isArray(threadID);
|
|
305
|
+
// Track which transport was used so the catch block can fall back to
|
|
306
|
+
// the OTHER transport — retrying the same one that just failed is useless.
|
|
307
|
+
const usedMqtt = mqttReady && !isMultiRecipient && api.sendMessageMqtt;
|
|
306
308
|
|
|
307
|
-
|
|
309
|
+
let result;
|
|
310
|
+
if (usedMqtt) {
|
|
308
311
|
result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
309
312
|
} else {
|
|
310
313
|
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
311
314
|
}
|
|
312
315
|
callback(null, result);
|
|
313
316
|
} catch (sendErr) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
callback(sendErr);
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
317
|
+
// Fall back to the OTHER transport, not the one that just failed.
|
|
318
|
+
const mqttNowReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
319
|
+
const primaryWasMqtt = mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt;
|
|
320
|
+
|
|
321
|
+
if (primaryWasMqtt) {
|
|
322
|
+
// MQTT was used (or is available) — fall back to HTTP
|
|
323
323
|
try {
|
|
324
324
|
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
325
325
|
callback(null, httpRes);
|
|
326
326
|
} catch (_httpErr) {
|
|
327
327
|
callback(sendErr);
|
|
328
328
|
}
|
|
329
|
+
} else {
|
|
330
|
+
// HTTP was used — try MQTT if it has since become available
|
|
331
|
+
if (mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
332
|
+
try {
|
|
333
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
334
|
+
callback(null, mqttRes);
|
|
335
|
+
} catch (_mqttErr) {
|
|
336
|
+
callback(sendErr);
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
callback(sendErr);
|
|
340
|
+
}
|
|
329
341
|
}
|
|
330
342
|
} finally {
|
|
331
343
|
if (typingTimeout) clearTimeout(typingTimeout);
|
|
@@ -205,6 +205,14 @@ class AntiSuspension {
|
|
|
205
205
|
detectSuspensionSignal(text) {
|
|
206
206
|
if (!text || typeof text !== 'string') return false;
|
|
207
207
|
const lower = text.toLowerCase();
|
|
208
|
+
|
|
209
|
+
// Session expiry is NOT a suspension event — never trip the circuit
|
|
210
|
+
// breaker for it. A false positive here blocks all sends AND re-login
|
|
211
|
+
// attempts for up to 45 minutes, turning a normal logout into a
|
|
212
|
+
// permanent lockout until the cooldown expires.
|
|
213
|
+
const isSessionExpiry = SESSION_EXPIRY_SIGNALS.some(signal => lower.includes(signal));
|
|
214
|
+
if (isSessionExpiry) return false;
|
|
215
|
+
|
|
208
216
|
const found = SUSPENSION_SIGNALS.some(signal => lower.includes(signal));
|
|
209
217
|
if (found) {
|
|
210
218
|
this._onSuspensionSignalDetected();
|
|
@@ -240,11 +248,14 @@ class AntiSuspension {
|
|
|
240
248
|
isCircuitBreakerTripped() {
|
|
241
249
|
const cb = this.suspensionCircuitBreaker;
|
|
242
250
|
if (!cb.tripped) return false;
|
|
251
|
+
// Use per-trip override duration if set, otherwise fall back to default.
|
|
252
|
+
const activeCooldown = cb.overrideCooldownMs || cb.cooldownMs;
|
|
243
253
|
const elapsed = Date.now() - cb.trippedAt;
|
|
244
|
-
if (elapsed >=
|
|
254
|
+
if (elapsed >= activeCooldown) {
|
|
245
255
|
cb.tripped = false;
|
|
246
256
|
cb.signalCount = 0;
|
|
247
257
|
cb.trippedAt = null;
|
|
258
|
+
cb.overrideCooldownMs = null;
|
|
248
259
|
return false;
|
|
249
260
|
}
|
|
250
261
|
return true;
|
|
@@ -253,25 +264,31 @@ class AntiSuspension {
|
|
|
253
264
|
getCircuitBreakerRemainingMs() {
|
|
254
265
|
const cb = this.suspensionCircuitBreaker;
|
|
255
266
|
if (!cb.tripped) return 0;
|
|
256
|
-
|
|
267
|
+
const activeCooldown = cb.overrideCooldownMs || cb.cooldownMs;
|
|
268
|
+
return Math.max(0, activeCooldown - (Date.now() - cb.trippedAt));
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
tripCircuitBreaker(reason, durationMs) {
|
|
260
272
|
const cb = this.suspensionCircuitBreaker;
|
|
261
273
|
cb.tripped = true;
|
|
262
274
|
cb.trippedAt = Date.now();
|
|
263
|
-
|
|
275
|
+
// Store custom duration as a per-trip override so the DEFAULT cooldownMs
|
|
276
|
+
// is never permanently mutated — future organic-signal trips will still
|
|
277
|
+
// use the original 45-minute default.
|
|
278
|
+
cb.overrideCooldownMs = durationMs || null;
|
|
264
279
|
cb.signalCount = cb.maxSignalsBeforeTrip;
|
|
280
|
+
const activeCooldown = durationMs || cb.cooldownMs;
|
|
265
281
|
const { utils } = this._getUtils();
|
|
266
282
|
utils && utils.warn && utils.warn("AntiSuspension",
|
|
267
283
|
`Circuit breaker manually tripped: ${reason || 'manual'}. ` +
|
|
268
|
-
`Cooldown: ${(
|
|
284
|
+
`Cooldown: ${(activeCooldown / 60000).toFixed(1)} min`);
|
|
269
285
|
}
|
|
270
286
|
|
|
271
287
|
resetCircuitBreaker() {
|
|
272
288
|
this.suspensionCircuitBreaker.tripped = false;
|
|
273
289
|
this.suspensionCircuitBreaker.signalCount = 0;
|
|
274
290
|
this.suspensionCircuitBreaker.trippedAt = null;
|
|
291
|
+
this.suspensionCircuitBreaker.overrideCooldownMs = null;
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
async simulateTyping(threadID, messageLength = 50) {
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -110,6 +110,13 @@ class AutoReLoginManager {
|
|
|
110
110
|
|
|
111
111
|
const fbLinkFunc = typeof fbLink === 'function' ? fbLink : () => fbLink;
|
|
112
112
|
|
|
113
|
+
// Capture the listening state from the CURRENT ctx BEFORE loginHelper
|
|
114
|
+
// replaces api.ctx with a fresh context object. By the time the login
|
|
115
|
+
// callback fires, api.ctx already points to the new ctx, so checking
|
|
116
|
+
// api.ctx._listeningActive in the callback would always be undefined.
|
|
117
|
+
const wasListening = !!(api && api.ctx && api.ctx._listeningActive);
|
|
118
|
+
const savedListenCallback = (api && api.ctx && api.ctx._lastListenCallback) || null;
|
|
119
|
+
|
|
113
120
|
await new Promise((resolve, reject) => {
|
|
114
121
|
loginHelperModel(
|
|
115
122
|
this.credentials,
|
|
@@ -121,35 +128,17 @@ class AutoReLoginManager {
|
|
|
121
128
|
}
|
|
122
129
|
|
|
123
130
|
if (api) {
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
'mqttEndpoint', 'lastSeqId', 'appID', 'clientID',
|
|
134
|
-
'userAppID', 'mqttAppID', 'userID', 'region',
|
|
135
|
-
'access_token'
|
|
136
|
-
];
|
|
137
|
-
for (const field of sessionFields) {
|
|
138
|
-
if (newApi.ctx[field] !== undefined) {
|
|
139
|
-
liveCtx[field] = newApi.ctx[field];
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Reset flags that block MQTT reconnection after re-login
|
|
143
|
-
liveCtx.loggedIn = true;
|
|
144
|
-
liveCtx._ending = false;
|
|
145
|
-
liveCtx._cycling = false;
|
|
146
|
-
liveCtx._mqttReauthing = false;
|
|
131
|
+
// loginHelper calls loadApiModules() which recreates all API
|
|
132
|
+
// method closures on the new ctx, so subsequent calls automatically
|
|
133
|
+
// use fresh tokens. Also reset flags on the new ctx so MQTT can
|
|
134
|
+
// reconnect cleanly.
|
|
135
|
+
if (api.ctx) {
|
|
136
|
+
api.ctx.loggedIn = true;
|
|
137
|
+
api.ctx._ending = false;
|
|
138
|
+
api.ctx._cycling = false;
|
|
139
|
+
api.ctx._mqttReauthing = false;
|
|
147
140
|
}
|
|
148
141
|
|
|
149
|
-
// Do NOT replace api.defaultFuncs — the existing closures already
|
|
150
|
-
// reference the live (now-updated) ctx, so they will pick up fresh
|
|
151
|
-
// tokens without needing new function objects.
|
|
152
|
-
|
|
153
142
|
if (api.tokenRefreshManager) {
|
|
154
143
|
api.tokenRefreshManager.resetFailureCount();
|
|
155
144
|
}
|
|
@@ -175,15 +164,17 @@ class AutoReLoginManager {
|
|
|
175
164
|
this.onReLoginSuccess();
|
|
176
165
|
}
|
|
177
166
|
|
|
167
|
+
// Restart MQTT listening if it was active before re-login.
|
|
168
|
+
// wasListening was captured from the OLD ctx before loginHelper replaced
|
|
169
|
+
// api.ctx — this is the only reliable way to know if listening was on.
|
|
178
170
|
try {
|
|
179
|
-
if (
|
|
171
|
+
if (wasListening && api && api.listenMqtt) {
|
|
180
172
|
try {
|
|
181
173
|
if (typeof api.stopListening === 'function') {
|
|
182
174
|
try { api.stopListening(); } catch (_) {}
|
|
183
175
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
api.listenMqtt(cb);
|
|
176
|
+
if (savedListenCallback) {
|
|
177
|
+
api.listenMqtt(savedListenCallback);
|
|
187
178
|
} else {
|
|
188
179
|
api.listenMqtt();
|
|
189
180
|
}
|
package/src/utils/axios.js
CHANGED
|
@@ -77,6 +77,16 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (isScrapingWarning) {
|
|
80
|
+
// Auto-dismiss before propagating the error. Unhandled scraping warnings
|
|
81
|
+
// escalate to permanent suspension. We await the bypass so the checkpoint is
|
|
82
|
+
// gone before the caller's retry (e.g. MQTT reconnect) fires.
|
|
83
|
+
// Guards: only attempt when ctx has jar + globalOptions available.
|
|
84
|
+
if (ctx && ctx.jar && ctx.globalOptions) {
|
|
85
|
+
try {
|
|
86
|
+
const { bypassScrapingWarning } = require('./checkpointBypass');
|
|
87
|
+
await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID || null, null);
|
|
88
|
+
} catch (_) {}
|
|
89
|
+
}
|
|
80
90
|
const err = new Error('Facebook scraping warning checkpoint detected.');
|
|
81
91
|
err.error = 'checkpoint_scraping';
|
|
82
92
|
err.res = body;
|
|
@@ -91,10 +101,14 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
91
101
|
// actual login page — NOT generic strings like '"login.php?"' or
|
|
92
102
|
// '"login_page"' which Facebook embeds in authenticated page payloads as
|
|
93
103
|
// navigation links and client-side route names, causing false positives.
|
|
104
|
+
// `/checkpoint/block/?next` is the URL Facebook redirects to when it forces
|
|
105
|
+
// a logout; it appears in the response body as a redirect target and is a
|
|
106
|
+
// reliable signal that the session is gone.
|
|
94
107
|
const isLoginRedirect =
|
|
95
108
|
bodyStr.includes('<form id="login_form"') ||
|
|
96
109
|
bodyStr.includes('id="loginbutton"') ||
|
|
97
|
-
bodyStr.includes('id="email" name="email"')
|
|
110
|
+
bodyStr.includes('id="email" name="email"') ||
|
|
111
|
+
bodyStr.includes('/checkpoint/block/?next');
|
|
98
112
|
|
|
99
113
|
const isLoginBlocked =
|
|
100
114
|
typeof body === 'object' && body !== null && body.error === 1357001;
|
package/src/utils/clients.js
CHANGED
|
@@ -23,14 +23,33 @@ function formatCookie(arr, url) {
|
|
|
23
23
|
function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
24
24
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Attempt performAutoLogin if available and not already in progress.
|
|
28
|
+
* Returns true if re-login succeeded, false otherwise.
|
|
29
|
+
* Always resets ctx.auto_login in a finally block.
|
|
30
|
+
*/
|
|
31
|
+
async function tryAutoLogin() {
|
|
32
|
+
if (ctx.auto_login || typeof ctx.performAutoLogin !== 'function') return false;
|
|
33
|
+
ctx.auto_login = true;
|
|
34
|
+
try {
|
|
35
|
+
const ok = await ctx.performAutoLogin();
|
|
36
|
+
return !!ok;
|
|
37
|
+
} catch (_) {
|
|
38
|
+
return false;
|
|
39
|
+
} finally {
|
|
40
|
+
ctx.auto_login = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
return async (data) => {
|
|
27
45
|
if (data.statusCode === 401) {
|
|
46
|
+
warn("Session Status", "Session expired. Triggering auto re-login...");
|
|
47
|
+
await tryAutoLogin();
|
|
28
48
|
const err = new Error("Session expired - Authentication required");
|
|
29
49
|
err.error = 401;
|
|
30
50
|
err.errorCode = 401;
|
|
31
51
|
err.errorType = "AUTHENTICATION_REQUIRED";
|
|
32
52
|
err.requiresReLogin = true;
|
|
33
|
-
warn("Session Status", "Session expired. Re-login required.");
|
|
34
53
|
throw err;
|
|
35
54
|
}
|
|
36
55
|
|
|
@@ -49,7 +68,6 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
49
68
|
|
|
50
69
|
await delay(retryTime);
|
|
51
70
|
|
|
52
|
-
// Guard against undefined Content-Type header before splitting
|
|
53
71
|
const contentType = (data.request.headers && data.request.headers["content-type"]) || "";
|
|
54
72
|
if (contentType.split(";")[0].trim() === "multipart/form-data") {
|
|
55
73
|
const newData = await http.postFormData(
|
|
@@ -61,10 +79,6 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
61
79
|
);
|
|
62
80
|
return await parseAndCheckLogin(ctx, http, retryCount)(newData);
|
|
63
81
|
} else {
|
|
64
|
-
// defaultFuncs.post signature: (url, jar, form, ctxx, customHeader)
|
|
65
|
-
// The 4th arg must be ctx (not ctx.globalOptions) — passing globalOptions
|
|
66
|
-
// here caused the retry to be treated as a raw network call without
|
|
67
|
-
// session context, missing auth headers and session inspection.
|
|
68
82
|
const newData = await http.post(
|
|
69
83
|
url,
|
|
70
84
|
ctx.jar,
|
|
@@ -107,8 +121,9 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
107
121
|
if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") {
|
|
108
122
|
res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", "");
|
|
109
123
|
const requireCookie = res.jsmods.require[0][3];
|
|
110
|
-
|
|
111
|
-
ctx.jar.
|
|
124
|
+
// Use setCookieSync to avoid async fire-and-forget that drops rotated session cookies
|
|
125
|
+
try { ctx.jar.setCookieSync(formatCookie(requireCookie, "facebook"), "https://www.facebook.com"); } catch (_) {}
|
|
126
|
+
try { ctx.jar.setCookieSync(formatCookie(requireCookie, "messenger"), "https://www.messenger.com"); } catch (_) {}
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
if (res.jsmods && Array.isArray(res.jsmods.require)) {
|
|
@@ -120,6 +135,9 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
120
135
|
for (let j = 0; j < ctx.fb_dtsg.length; j++) {
|
|
121
136
|
ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
|
|
122
137
|
}
|
|
138
|
+
// jazoest MUST stay in sync with fb_dtsg — stale jazoest causes form-submission
|
|
139
|
+
// failures that Facebook treats as tamper attempts and flags as bot activity.
|
|
140
|
+
ctx.jazoest = `2${Array.from(ctx.fb_dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`;
|
|
123
141
|
}
|
|
124
142
|
}
|
|
125
143
|
}
|
|
@@ -131,12 +149,16 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
131
149
|
};
|
|
132
150
|
|
|
133
151
|
if (res.error && SESSION_EXPIRY_CODES[res.error]) {
|
|
152
|
+
warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error}) — triggering auto re-login`);
|
|
153
|
+
// Fire re-login so the session is refreshed even though this request fails.
|
|
154
|
+
// Awaiting ensures the new session is ready before the error propagates to
|
|
155
|
+
// callers (e.g. MQTT reconnect) that would immediately retry.
|
|
156
|
+
await tryAutoLogin();
|
|
134
157
|
const err = new Error(SESSION_EXPIRY_CODES[res.error]);
|
|
135
158
|
err.error = res.error;
|
|
136
159
|
err.errorCode = res.error;
|
|
137
160
|
err.errorType = "SESSION_EXPIRED";
|
|
138
161
|
err.requiresReLogin = true;
|
|
139
|
-
warn("Session Status", `${SESSION_EXPIRY_CODES[res.error]} (Code: ${res.error})`);
|
|
140
162
|
throw err;
|
|
141
163
|
}
|
|
142
164
|
|
|
@@ -155,6 +177,10 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
155
177
|
err.errorType = res.error === 1357004 ? "CHECKPOINT" : res.error === 1357031 ? "LOCKED" : "BLOCKED";
|
|
156
178
|
err.requiresReLogin = res.error === 1357004 || res.error === 1357031;
|
|
157
179
|
warn("Account Status", `${ACCOUNT_ERROR_CODES[res.error]} (Code: ${res.error})`);
|
|
180
|
+
// For checkpoint and locked states, trigger re-login so the session can recover
|
|
181
|
+
if (err.requiresReLogin) {
|
|
182
|
+
await tryAutoLogin();
|
|
183
|
+
}
|
|
158
184
|
throw err;
|
|
159
185
|
}
|
|
160
186
|
|
|
@@ -166,13 +192,26 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
166
192
|
}
|
|
167
193
|
|
|
168
194
|
const resStr = JSON.stringify(res);
|
|
169
|
-
|
|
195
|
+
|
|
170
196
|
if (resStr.includes("XCheckpointFBScrapingWarningController") || resStr.includes("601051028565049")) {
|
|
171
|
-
warn("Bot Detection", "Facebook checkpoint detected - scraping warning (601051028565049)");
|
|
172
|
-
try {
|
|
173
|
-
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
|
|
197
|
+
warn("Bot Detection", "Facebook checkpoint detected - scraping warning (601051028565049) — auto-dismissing");
|
|
198
|
+
try {
|
|
199
|
+
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 5 * 60 * 1000);
|
|
174
200
|
configureRateLimiter({ maxConcurrentRequests: 2 });
|
|
175
201
|
} catch (_) {}
|
|
202
|
+
// Auto-dismiss the scraping warning — unhandled warnings escalate to permanent
|
|
203
|
+
// account suspension. This path is reached when inspectResponseForSessionIssues
|
|
204
|
+
// was skipped (ctx._skipSessionInspect or null ctx). We still throw after
|
|
205
|
+
// cleanup: `res` at this point is checkpoint JSON, not a valid API response,
|
|
206
|
+
// so returning it would corrupt the caller. The bypass ensures the NEXT
|
|
207
|
+
// retry finds a clean session.
|
|
208
|
+
try {
|
|
209
|
+
const { bypassScrapingWarning } = require('./checkpointBypass');
|
|
210
|
+
await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID, null);
|
|
211
|
+
warn("Bot Detection", "Scraping warning dismissed — checkpoint cleared for next retry");
|
|
212
|
+
} catch (bypassErr) {
|
|
213
|
+
warn("Bot Detection", `Scraping warning bypass failed: ${bypassErr && bypassErr.message ? bypassErr.message : String(bypassErr)}`);
|
|
214
|
+
}
|
|
176
215
|
const err = new Error("Facebook detected automated behavior - Account may be flagged");
|
|
177
216
|
err.error = "Bot checkpoint detected";
|
|
178
217
|
err.errorCode = "CHECKPOINT_SCRAPING";
|
|
@@ -184,8 +223,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
184
223
|
if (resStr.includes("1501092823525282")) {
|
|
185
224
|
warn("Bot Detection", "Critical bot checkpoint 282 detected! Account requires immediate attention!");
|
|
186
225
|
log("Please check your Facebook account in a browser and complete any security checks.");
|
|
187
|
-
try {
|
|
188
|
-
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
|
|
226
|
+
try {
|
|
227
|
+
globalRateLimiter.setEndpointCooldown("__GLOBAL__", 10 * 60 * 1000);
|
|
189
228
|
configureRateLimiter({ maxConcurrentRequests: 1 });
|
|
190
229
|
} catch (_) {}
|
|
191
230
|
const err = new Error("Facebook detected automated behavior - Critical checkpoint required");
|
|
@@ -206,7 +245,8 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
206
245
|
// anywhere in the body JSON, which it does on authenticated pages as a
|
|
207
246
|
// navigation/share link, causing false-positive session expiry errors.
|
|
208
247
|
if (String(res.redirect || "").includes("login.php")) {
|
|
209
|
-
warn("Session Status", "Redirected to login page - Session expired");
|
|
248
|
+
warn("Session Status", "Redirected to login page - Session expired — triggering auto re-login");
|
|
249
|
+
await tryAutoLogin();
|
|
210
250
|
const err = new Error("Session expired - Redirected to login page");
|
|
211
251
|
err.error = 401;
|
|
212
252
|
err.errorCode = 401;
|
|
@@ -215,13 +255,18 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
215
255
|
throw err;
|
|
216
256
|
}
|
|
217
257
|
|
|
218
|
-
if (typeof data.body === 'string' && (
|
|
258
|
+
if (typeof data.body === 'string' && (
|
|
259
|
+
data.body.includes('<title>Facebook - Log In or Sign Up</title>') ||
|
|
260
|
+
data.body.includes('name="login_form"') ||
|
|
261
|
+
data.body.includes('/checkpoint/block/?next')
|
|
262
|
+
)) {
|
|
263
|
+
warn("Session Status", "Detected login page or checkpoint redirect — triggering auto re-login");
|
|
264
|
+
await tryAutoLogin();
|
|
219
265
|
const err = new Error("Session expired - Redirected to login page");
|
|
220
266
|
err.error = 401;
|
|
221
267
|
err.errorCode = 401;
|
|
222
268
|
err.errorType = "LOGIN_REDIRECT";
|
|
223
269
|
err.requiresReLogin = true;
|
|
224
|
-
warn("Session Status", "Detected login page redirect. Session expired.");
|
|
225
270
|
throw err;
|
|
226
271
|
}
|
|
227
272
|
|
|
@@ -231,26 +276,25 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
231
276
|
|
|
232
277
|
/**
|
|
233
278
|
* Saves cookies from a response to the cookie jar.
|
|
279
|
+
* Uses setCookieSync to guarantee cookies are saved before the next request fires.
|
|
280
|
+
* Facebook continuously rotates session cookies (xs, c_user, fr, etc.) — any missed
|
|
281
|
+
* rotation leaves the jar stale and Facebook forces an immediate logout.
|
|
282
|
+
*
|
|
234
283
|
* @param {Object} jar - The cookie jar instance.
|
|
235
284
|
* @returns {function(res: Object): Object} A function that processes the response and returns it.
|
|
236
285
|
*/
|
|
237
286
|
function saveCookies(jar) {
|
|
238
287
|
return function (res) {
|
|
239
|
-
const cookies = res.headers["set-cookie"] || [];
|
|
288
|
+
const cookies = (res.headers && res.headers["set-cookie"]) || [];
|
|
240
289
|
cookies.forEach(function (c) {
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
// session cookies (xs, c_user, fr, etc.) continuously — missing a
|
|
246
|
-
// single update causes the jar to go stale and Facebook forces logout.
|
|
247
|
-
try { jar.setCookie(c, "https://www.facebook.com"); } catch (_) {}
|
|
248
|
-
|
|
249
|
-
// Mirror to messenger.com so MQTT / Messenger API calls stay authed.
|
|
290
|
+
// Save to facebook.com
|
|
291
|
+
try { jar.setCookieSync(c, "https://www.facebook.com"); } catch (_) {}
|
|
292
|
+
|
|
293
|
+
// Mirror to messenger.com so MQTT / Messenger API calls stay authenticated.
|
|
250
294
|
const c2 = c
|
|
251
295
|
.replace(/domain=[^;]*/i, "domain=.messenger.com")
|
|
252
296
|
.replace(/secure;?\s*/i, "");
|
|
253
|
-
try { jar.
|
|
297
|
+
try { jar.setCookieSync(c2, "https://www.messenger.com"); } catch (_) {}
|
|
254
298
|
});
|
|
255
299
|
return res;
|
|
256
300
|
};
|
|
@@ -265,7 +309,6 @@ function saveCookies(jar) {
|
|
|
265
309
|
function getAccessFromBusiness(jar, Options) {
|
|
266
310
|
return async function (res) {
|
|
267
311
|
const html = res ? res.body : null;
|
|
268
|
-
// Use the same axios wrapper used everywhere else — "request" module does not exist
|
|
269
312
|
const { get } = require("./axios");
|
|
270
313
|
try {
|
|
271
314
|
const businessRes = await get("https://business.facebook.com/content_management", jar, null, Options, { noRef: true });
|
|
@@ -278,14 +321,24 @@ function getAccessFromBusiness(jar, Options) {
|
|
|
278
321
|
}
|
|
279
322
|
|
|
280
323
|
/**
|
|
281
|
-
* Retrieves all cookies from the jar for both Facebook and Messenger domains
|
|
324
|
+
* Retrieves all cookies from the jar for both Facebook and Messenger domains,
|
|
325
|
+
* deduplicated by key + domain + path so stale copies don't shadow fresh ones.
|
|
282
326
|
* @param {Object} jar - The cookie jar instance.
|
|
283
327
|
* @returns {Array<Object>} An array of cookie objects.
|
|
284
328
|
*/
|
|
285
329
|
function getAppState(jar) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
330
|
+
const fbCookies = jar.getCookiesSync("https://www.facebook.com");
|
|
331
|
+
const messengerCookies = jar.getCookiesSync("https://www.messenger.com");
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
const all = [];
|
|
334
|
+
for (const cookie of [...fbCookies, ...messengerCookies]) {
|
|
335
|
+
const id = `${cookie.key}|${cookie.domain || ""}|${cookie.path || "/"}`;
|
|
336
|
+
if (!seen.has(id)) {
|
|
337
|
+
seen.add(id);
|
|
338
|
+
all.push(cookie);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return all;
|
|
289
342
|
}
|
|
290
343
|
|
|
291
344
|
module.exports = {
|