nexus-fca 3.1.1 β 3.1.3
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/README.md +7 -3
- package/index.js +137 -4
- package/lib/safety/CookieRefresher.js +12 -1
- package/lib/safety/FacebookSafety.js +40 -32
- package/lib/safety/StealthMode.js +16 -7
- package/package.json +5 -3
- package/src/listenMqtt.js +130 -42
- package/src/listenNotification.js +22 -56
- package/src/listenRealtime.js +161 -0
- package/src/listenSpeed.js +197 -0
- package/utils.js +224 -102
- package/src/changeAvatarV2.js +0 -75
- package/src/getThreadHistoryDeprecated.js +0 -55
- package/src/getThreadInfoDeprecated.js +0 -49
- package/src/getThreadListDeprecated.js +0 -54
package/README.md
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
<img src="https://i.ibb.co/Sk61FGg/Dragon-Fruit-1.jpg" alt="Nexus-FCA" width="520" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
# Nexus-FCA v3.1.
|
|
5
|
+
# Nexus-FCA v3.1.1 β π THE BEST, SAFEST, MOST STABLE FCA
|
|
6
6
|
|
|
7
7
|
Modern, safe, productionβready Messenger (Facebook Chat) API layer with **email/password + appState login**, **proxy support**, **random user agent**, adaptive session & connection resilience, proactive cookie refresh, MQTT stability enhancements, delivery reliability safeguards, memory protection, and rich runtime metrics. Promise + callback compatible, TypeScript typed, minimal friction.
|
|
8
8
|
|
|
9
|
-
## π NEW in 3.1.
|
|
9
|
+
## π NEW in 3.1.1 - Industry Leading Features!
|
|
10
|
+
- β
**Smart MQTT Recovery** - Auto-refreshes Sequence ID on errors to prevent loops
|
|
11
|
+
- β
**Proactive Lifecycle Management** - Randomized reconnects (26-60m) to mimic human behavior
|
|
10
12
|
- β
**Email/Password Login** - Login with Facebook credentials (not just cookies!)
|
|
11
13
|
- β
**Advanced Proxy Support** - HTTP/HTTPS/SOCKS5 proxy for all connections
|
|
12
14
|
- β
**Random User Agent** - 14+ realistic user agents to avoid detection
|
|
@@ -139,7 +141,9 @@ await login({ appState }, { disablePreflight: true });
|
|
|
139
141
|
```
|
|
140
142
|
|
|
141
143
|
---
|
|
142
|
-
## π°οΈ MQTT Enhancements (Since
|
|
144
|
+
## π°οΈ MQTT Enhancements (Since 3.1.x)
|
|
145
|
+
- **Smart Recovery**: Fetches fresh Sequence ID before reconnecting on errors (prevents stale token loops)
|
|
146
|
+
- **Lifecycle Management**: Proactive randomized reconnects (26-60m) to avoid long-session forced disconnects
|
|
143
147
|
- Adaptive reconnect curve (caps 5m)
|
|
144
148
|
- Layered post-refresh probes (1s / 10s / 30s)
|
|
145
149
|
- Synthetic randomized keepalives (55β75s)
|
package/index.js
CHANGED
|
@@ -209,9 +209,75 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
209
209
|
if (process.env.NEXUS_REGION) {
|
|
210
210
|
try { region = process.env.NEXUS_REGION.toUpperCase(); } catch(_) {}
|
|
211
211
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
|
|
213
|
+
// Robust fb_dtsg extraction
|
|
214
|
+
const dtsgRegexes = [
|
|
215
|
+
/DTSGInitialData.*?token":"(.*?)"/,
|
|
216
|
+
/"DTSGInitData",\[\],{"token":"(.*?)"/,
|
|
217
|
+
/\["DTSGInitData",\[\],{"token":"(.*?)"/,
|
|
218
|
+
/name="fb_dtsg" value="(.*?)"/,
|
|
219
|
+
/name="dtsg_ag" value="(.*?)"/
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
for (const regex of dtsgRegexes) {
|
|
223
|
+
const match = html.match(regex);
|
|
224
|
+
if (match && match[1]) {
|
|
225
|
+
fb_dtsg = match[1];
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// NEW: JSON Parsing Fallback (inspired by ws3-fca)
|
|
231
|
+
if (!fb_dtsg) {
|
|
232
|
+
try {
|
|
233
|
+
const extractNetData = (html) => {
|
|
234
|
+
const allScriptsData = [];
|
|
235
|
+
const scriptRegex = /<script type="application\/json"[^>]*>(.*?)<\/script>/g;
|
|
236
|
+
let match;
|
|
237
|
+
while ((match = scriptRegex.exec(html)) !== null) {
|
|
238
|
+
try {
|
|
239
|
+
allScriptsData.push(JSON.parse(match[1]));
|
|
240
|
+
} catch (e) { }
|
|
241
|
+
}
|
|
242
|
+
return allScriptsData;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const netData = extractNetData(html);
|
|
246
|
+
|
|
247
|
+
const findConfig = (key) => {
|
|
248
|
+
for (const scriptData of netData) {
|
|
249
|
+
if (scriptData.require) {
|
|
250
|
+
for (const req of scriptData.require) {
|
|
251
|
+
if (Array.isArray(req) && req[0] === key && req[2]) {
|
|
252
|
+
return req[2];
|
|
253
|
+
}
|
|
254
|
+
if (Array.isArray(req) && req[3] && req[3][0] && req[3][0].__bbox && req[3][0].__bbox.define) {
|
|
255
|
+
for (const def of req[3][0].__bbox.define) {
|
|
256
|
+
if (Array.isArray(def) && def[0].endsWith(key) && def[2]) {
|
|
257
|
+
return def[2];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const dtsgData = findConfig("DTSGInitialData");
|
|
268
|
+
if (dtsgData && dtsgData.token) {
|
|
269
|
+
fb_dtsg = dtsgData.token;
|
|
270
|
+
log.verbose("login", "Found fb_dtsg via JSON parsing");
|
|
271
|
+
}
|
|
272
|
+
} catch (e) {
|
|
273
|
+
log.verbose("login", "JSON parsing for fb_dtsg failed: " + e.message);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!fb_dtsg) {
|
|
278
|
+
// log.warn("login", "Could not find fb_dtsg in HTML. Session might be limited.");
|
|
279
|
+
} else {
|
|
280
|
+
log.verbose("login", "Found fb_dtsg: " + fb_dtsg.substring(0, 10) + "...");
|
|
215
281
|
}
|
|
216
282
|
|
|
217
283
|
|
|
@@ -585,9 +651,76 @@ function loginHelper(appState, email, password, globalOptions, callback, prCallb
|
|
|
585
651
|
return res;
|
|
586
652
|
})
|
|
587
653
|
.then(handleRedirect)
|
|
588
|
-
.then(res => {
|
|
654
|
+
.then(async res => {
|
|
589
655
|
const html = res.body;
|
|
590
656
|
const Obj = buildAPI(globalOptions, html, jar);
|
|
657
|
+
|
|
658
|
+
// Fallback: Try mbasic.facebook.com if fb_dtsg is missing
|
|
659
|
+
if (!Obj.ctx.fb_dtsg) {
|
|
660
|
+
logger("Attempting to fetch fb_dtsg from mbasic.facebook.com...", "info");
|
|
661
|
+
try {
|
|
662
|
+
const mobileOptions = {
|
|
663
|
+
...globalOptions,
|
|
664
|
+
userAgent: "Mozilla/5.0 (Linux; Android 12; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
|
|
665
|
+
};
|
|
666
|
+
const mRes = await utils.get("https://mbasic.facebook.com/", jar, null, mobileOptions);
|
|
667
|
+
const mHtml = mRes.body;
|
|
668
|
+
const mMatch = mHtml.match(/name="fb_dtsg" value="(.*?)"/);
|
|
669
|
+
if (mMatch && mMatch[1]) {
|
|
670
|
+
Obj.ctx.fb_dtsg = mMatch[1];
|
|
671
|
+
Obj.ctx.ttstamp = "2" + Obj.ctx.fb_dtsg.split("").map(c => c.charCodeAt(0)).join("");
|
|
672
|
+
logger("Found fb_dtsg from mbasic.facebook.com", "success");
|
|
673
|
+
|
|
674
|
+
// Re-create defaultFuncs with the new token
|
|
675
|
+
Obj.defaultFuncs = utils.makeDefaults(html, Obj.ctx.userID, Obj.ctx);
|
|
676
|
+
} else {
|
|
677
|
+
// Try one more: dtsg_ag
|
|
678
|
+
const agMatch = mHtml.match(/name="dtsg_ag" value="(.*?)"/);
|
|
679
|
+
if (agMatch && agMatch[1]) {
|
|
680
|
+
Obj.ctx.fb_dtsg = agMatch[1];
|
|
681
|
+
Obj.ctx.ttstamp = "2" + Obj.ctx.fb_dtsg.split("").map(c => c.charCodeAt(0)).join("");
|
|
682
|
+
logger("Found fb_dtsg (ag) from mbasic.facebook.com", "success");
|
|
683
|
+
Obj.defaultFuncs = utils.makeDefaults(html, Obj.ctx.userID, Obj.ctx);
|
|
684
|
+
} else {
|
|
685
|
+
logger("Failed to find fb_dtsg in mbasic response", "warn");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Second Fallback: Try business.facebook.com
|
|
690
|
+
if (!Obj.ctx.fb_dtsg) {
|
|
691
|
+
logger("Attempting to fetch fb_dtsg from business.facebook.com...", "info");
|
|
692
|
+
try {
|
|
693
|
+
const bRes = await utils.get("https://business.facebook.com/business_locations", jar, null, globalOptions);
|
|
694
|
+
const bHtml = bRes.body;
|
|
695
|
+
const bMatch = bHtml.match(/name="fb_dtsg" value="(.*?)"/) || bHtml.match(/DTSGInitialData.*?token":"(.*?)"/);
|
|
696
|
+
if (bMatch && bMatch[1]) {
|
|
697
|
+
Obj.ctx.fb_dtsg = bMatch[1];
|
|
698
|
+
Obj.ctx.ttstamp = "2" + Obj.ctx.fb_dtsg.split("").map(c => c.charCodeAt(0)).join("");
|
|
699
|
+
logger("Found fb_dtsg from business.facebook.com", "success");
|
|
700
|
+
Obj.defaultFuncs = utils.makeDefaults(html, Obj.ctx.userID, Obj.ctx);
|
|
701
|
+
} else {
|
|
702
|
+
logger("Failed to find fb_dtsg in business response", "warn");
|
|
703
|
+
// Debug: Print what we got
|
|
704
|
+
if (bHtml.includes("login_form") || bHtml.includes("checkpoint")) {
|
|
705
|
+
logger("Response indicates login/checkpoint page", "error");
|
|
706
|
+
} else {
|
|
707
|
+
logger("Response start: " + bHtml.substring(0, 200), "verbose");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch (e) {
|
|
711
|
+
logger("Failed to fetch fallback fb_dtsg from business: " + e.message, "warn");
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
} catch (e) {
|
|
716
|
+
logger("Failed to fetch fallback fb_dtsg: " + e.message, "warn");
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (!Obj.ctx.fb_dtsg) {
|
|
721
|
+
log.warn("login", "Could not find fb_dtsg in HTML or fallbacks. Session might be limited.");
|
|
722
|
+
}
|
|
723
|
+
|
|
591
724
|
ctx = Obj.ctx;
|
|
592
725
|
api = Obj.api;
|
|
593
726
|
return res;
|
|
@@ -101,7 +101,12 @@ class CookieRefresher {
|
|
|
101
101
|
try {
|
|
102
102
|
// 1. First do a simple fetch to keep session alive
|
|
103
103
|
logger('CookieRefresher', 'Refreshing cookies to extend session...', 'info');
|
|
104
|
+
|
|
105
|
+
// Enhanced URL list to simulate real user activity and keep session alive
|
|
104
106
|
const urls = [
|
|
107
|
+
'https://www.facebook.com/',
|
|
108
|
+
'https://www.facebook.com/notifications',
|
|
109
|
+
'https://www.facebook.com/messages/t/',
|
|
105
110
|
'https://www.facebook.com/me',
|
|
106
111
|
'https://www.facebook.com/ajax/haste-response/?__a=1',
|
|
107
112
|
'https://www.facebook.com/ajax/bootloader-endpoint/?__a=1'
|
|
@@ -110,6 +115,11 @@ class CookieRefresher {
|
|
|
110
115
|
let success = false;
|
|
111
116
|
for (const url of urls) {
|
|
112
117
|
try {
|
|
118
|
+
// Add random delay between requests to simulate human browsing
|
|
119
|
+
if (success) {
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 2000 + Math.random() * 3000));
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
// Try multiple URLs in case some are blocked
|
|
114
124
|
const res = await this.defaultFuncs.get(url, this.jar, {});
|
|
115
125
|
|
|
@@ -117,7 +127,8 @@ class CookieRefresher {
|
|
|
117
127
|
if (res && res.headers && res.headers["set-cookie"]) {
|
|
118
128
|
this.utils.saveCookies(this.jar)(res);
|
|
119
129
|
success = true;
|
|
120
|
-
break
|
|
130
|
+
// Don't break immediately, hit a few more to look like a real session
|
|
131
|
+
if (Math.random() > 0.7) break;
|
|
121
132
|
}
|
|
122
133
|
} catch (err) {
|
|
123
134
|
logger('CookieRefresher', `Failed to refresh with ${url}: ${err.message}`, 'debug');
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
|
|
9
|
+
// StealthMode module has been removed to avoid global human-like pauses
|
|
10
|
+
// that can harm perceived stability. FacebookSafety now relies on
|
|
11
|
+
// lightweight adaptive delays and backoff only.
|
|
12
|
+
// const StealthMode = require('./StealthMode');
|
|
10
13
|
|
|
11
14
|
class FacebookSafety {
|
|
12
15
|
constructor(options = {}) {
|
|
@@ -24,14 +27,9 @@ class FacebookSafety {
|
|
|
24
27
|
...options
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
enableRandomPauses: true,
|
|
31
|
-
pauseProbability: this.options.ultraLowBanMode ? 0.08 : 0.02,
|
|
32
|
-
minPauseMinutes: 3,
|
|
33
|
-
maxPauseMinutes: 10
|
|
34
|
-
});
|
|
30
|
+
// StealthMode has been fully removed; keep a placeholder field
|
|
31
|
+
// for backward compatibility but do not perform any throttling.
|
|
32
|
+
this.stealthMode = null;
|
|
35
33
|
|
|
36
34
|
// ULTRA-SAFE user agents - Most common real browsers (Nov 2025)
|
|
37
35
|
// These are the MOST COMMON UAs to blend in with real users
|
|
@@ -58,13 +56,13 @@ class FacebookSafety {
|
|
|
58
56
|
this.regions = ['ASH', 'ATL', 'DFW', 'ORD', 'PHX', 'SJC', 'IAD'];
|
|
59
57
|
this.currentRegion = this.regions[Math.floor(Math.random() * this.regions.length)];
|
|
60
58
|
|
|
61
|
-
// ULTRA-SAFE human delay patterns -
|
|
59
|
+
// ULTRA-SAFE human delay patterns - Relaxed for usability
|
|
62
60
|
this.humanDelayPatterns = {
|
|
63
|
-
typing: { min:
|
|
64
|
-
reading: { min:
|
|
65
|
-
thinking: { min:
|
|
66
|
-
browsing: { min:
|
|
67
|
-
messageDelay: { min:
|
|
61
|
+
typing: { min: 500, max: 1500 }, // Normal typing (0.5-1.5s)
|
|
62
|
+
reading: { min: 1000, max: 3000 }, // Normal reading (1-3s)
|
|
63
|
+
thinking: { min: 1000, max: 4000 }, // Normal thinking (1-4s)
|
|
64
|
+
browsing: { min: 500, max: 2000 }, // Normal browsing (0.5-2s)
|
|
65
|
+
messageDelay: { min: 1000, max: 3000 } // 1-3s between messages (much faster)
|
|
68
66
|
};
|
|
69
67
|
|
|
70
68
|
this.sessionMetrics = {
|
|
@@ -225,9 +223,21 @@ class FacebookSafety {
|
|
|
225
223
|
|
|
226
224
|
// Check cookie age (older than 30 days might be risky)
|
|
227
225
|
const oldCookies = parsed.filter(cookie => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
let expVal = cookie.expires || cookie.expirationDate;
|
|
227
|
+
// Handle Unix timestamp in seconds (common in FB cookies)
|
|
228
|
+
if (typeof expVal === 'number' && expVal < 10000000000) {
|
|
229
|
+
expVal *= 1000;
|
|
230
|
+
}
|
|
231
|
+
const expires = new Date(expVal);
|
|
232
|
+
// If invalid date, assume valid to be safe
|
|
233
|
+
if (isNaN(expires.getTime())) return false;
|
|
234
|
+
|
|
235
|
+
// Check if expired
|
|
236
|
+
const timeUntilExpiry = expires.getTime() - Date.now();
|
|
237
|
+
// If expired more than 30 days ago, it's "old"
|
|
238
|
+
// If timeUntilExpiry is negative, it's expired.
|
|
239
|
+
// We want to flag cookies that are LONG expired.
|
|
240
|
+
return timeUntilExpiry < -(30 * 24 * 60 * 60 * 1000);
|
|
231
241
|
});
|
|
232
242
|
|
|
233
243
|
if (oldCookies.length > parsed.length * 0.5) {
|
|
@@ -313,16 +323,16 @@ class FacebookSafety {
|
|
|
313
323
|
minMs = 2 * 60 * 60 * 1000; // 2h
|
|
314
324
|
maxMs = 3 * 60 * 60 * 1000; // 3h
|
|
315
325
|
} else if (this.sessionMetrics.riskLevel === 'medium') {
|
|
316
|
-
// Medium risk:
|
|
317
|
-
minMs =
|
|
318
|
-
maxMs = 5 * 60 * 60 * 1000;
|
|
326
|
+
// Medium risk: 1.5-2.5 hours
|
|
327
|
+
minMs = 1.5 * 60 * 60 * 1000; // 1.5h
|
|
328
|
+
maxMs = 2.5 * 60 * 60 * 1000; // 2.5h
|
|
319
329
|
} else {
|
|
320
|
-
// Low risk:
|
|
321
|
-
minMs =
|
|
322
|
-
maxMs =
|
|
330
|
+
// Low risk: 50-90 minutes (Keep session alive!)
|
|
331
|
+
minMs = 50 * 60 * 1000; // 50m
|
|
332
|
+
maxMs = 90 * 60 * 1000; // 90m
|
|
323
333
|
}
|
|
324
334
|
const interval = minMs + Math.random() * (maxMs - minMs);
|
|
325
|
-
console.log(`π Next token refresh in ${Math.floor(interval / (60 *
|
|
335
|
+
console.log(`π Next token refresh in ${Math.floor(interval / (60 * 1000))}m (keep-alive mode)`);
|
|
326
336
|
const t = setTimeout(async () => {
|
|
327
337
|
await this.refreshSafeSession();
|
|
328
338
|
schedule();
|
|
@@ -637,8 +647,8 @@ class FacebookSafety {
|
|
|
637
647
|
*/
|
|
638
648
|
scheduleLightPoke() {
|
|
639
649
|
if (this._lightPokeTimer || this._destroyed) return;
|
|
640
|
-
const base =
|
|
641
|
-
const jitter = (Math.random()*
|
|
650
|
+
const base = 45 * 60 * 1000; // 45m (Reduced from 6h to keep session alive)
|
|
651
|
+
const jitter = (Math.random()*20 - 10) * 60 * 1000; // Β±10m
|
|
642
652
|
const schedule = () => {
|
|
643
653
|
if (this._destroyed) return;
|
|
644
654
|
const t = setTimeout(async () => {
|
|
@@ -657,7 +667,7 @@ class FacebookSafety {
|
|
|
657
667
|
}
|
|
658
668
|
} catch(_) {}
|
|
659
669
|
schedule();
|
|
660
|
-
}, base + (Math.random()*
|
|
670
|
+
}, base + (Math.random()*20 - 10) * 60 * 1000);
|
|
661
671
|
this._registerTimer(t);
|
|
662
672
|
this._lightPokeTimer = t;
|
|
663
673
|
};
|
|
@@ -808,10 +818,8 @@ class FacebookSafety {
|
|
|
808
818
|
}
|
|
809
819
|
|
|
810
820
|
async applyAdaptiveSendDelay(){
|
|
811
|
-
//
|
|
812
|
-
|
|
813
|
-
await this.stealthMode.waitIfNeeded();
|
|
814
|
-
}
|
|
821
|
+
// StealthMode throttling has been removed; rely on lightweight
|
|
822
|
+
// adaptive delays and backoff logic elsewhere in FacebookSafety.
|
|
815
823
|
|
|
816
824
|
// Then apply adaptive delay based on risk
|
|
817
825
|
const d = this.computeAdaptiveSendDelay();
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
class StealthMode {
|
|
7
7
|
constructor(options = {}) {
|
|
8
8
|
this.options = {
|
|
9
|
-
maxRequestsPerMinute:
|
|
9
|
+
maxRequestsPerMinute: 1000, // Relaxed from 30 to 1000 based on user feedback
|
|
10
10
|
enableRandomPauses: true,
|
|
11
|
-
pauseProbability: 0.
|
|
12
|
-
minPauseMinutes:
|
|
13
|
-
maxPauseMinutes:
|
|
14
|
-
dailyRequestLimit:
|
|
11
|
+
pauseProbability: 0.0001, // Reduced from 1% to 0.01%
|
|
12
|
+
minPauseMinutes: 0.1,
|
|
13
|
+
maxPauseMinutes: 0.5,
|
|
14
|
+
dailyRequestLimit: 500000, // Increased from 2000 to 500k
|
|
15
15
|
...options
|
|
16
16
|
};
|
|
17
17
|
|
|
@@ -99,13 +99,17 @@ class StealthMode {
|
|
|
99
99
|
this.inPause = true;
|
|
100
100
|
this.pauseUntil = Date.now() + duration;
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
const minutes = Math.floor(duration / 60000);
|
|
103
|
+
const seconds = Math.floor((duration % 60000) / 1000);
|
|
104
|
+
const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
105
|
+
console.log(`β Stealth Mode: Taking a human break for ${timeStr}...`);
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
/**
|
|
106
109
|
* Wait until it's safe to proceed
|
|
107
110
|
*/
|
|
108
111
|
async waitIfNeeded() {
|
|
112
|
+
let logged = false;
|
|
109
113
|
while (true) {
|
|
110
114
|
const status = this.canProceed();
|
|
111
115
|
if (status.allowed) {
|
|
@@ -113,7 +117,12 @@ class StealthMode {
|
|
|
113
117
|
return;
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
|
|
120
|
+
// Only log once per wait cycle to avoid spamming console from parallel requests
|
|
121
|
+
if (!logged && status.waitMs > 2000) {
|
|
122
|
+
console.log(`π‘οΈ Stealth Mode: Waiting ${Math.ceil(status.waitMs / 1000)}s (${status.reason})`);
|
|
123
|
+
logged = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
await new Promise(resolve => setTimeout(resolve, status.waitMs));
|
|
118
127
|
}
|
|
119
128
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"description": "Nexus-FCA 3.1 β THE BEST, SAFEST, MOST STABLE Facebook Messenger API! Email/password + appState login, proxy support (HTTP/HTTPS/SOCKS5), random user agent, proactive cookie refresh, MQTT stability, session protection, and TypeScript support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
@@ -15,20 +15,22 @@
|
|
|
15
15
|
"chalk": "^4.1.2",
|
|
16
16
|
"cheerio": "^1.0.0-rc.10",
|
|
17
17
|
"duplexify": "^4.1.3",
|
|
18
|
+
"form-data": "^4.0.5",
|
|
18
19
|
"got": "^11.8.6",
|
|
19
20
|
"gradient-string": "^2.0.2",
|
|
21
|
+
"http-proxy-agent": "^7.0.2",
|
|
20
22
|
"https-proxy-agent": "^7.0.5",
|
|
21
23
|
"lodash": "^4.17.21",
|
|
22
24
|
"mqtt": "^4.3.8",
|
|
23
|
-
"socks-proxy-agent": "^8.0.4",
|
|
24
25
|
"node-cache": "^5.1.2",
|
|
25
26
|
"npmlog": "^7.0.1",
|
|
26
|
-
"request": "^2.88.2",
|
|
27
27
|
"semver": "^7.5.0",
|
|
28
28
|
"sequelize": "^6.37.6",
|
|
29
|
+
"socks-proxy-agent": "^8.0.4",
|
|
29
30
|
"sqlite": "^5.1.1",
|
|
30
31
|
"sqlite3": "^5.1.7",
|
|
31
32
|
"totp-generator": "^1.0.0",
|
|
33
|
+
"tough-cookie": "^6.0.0",
|
|
32
34
|
"uuid": "^9.0.1",
|
|
33
35
|
"ws": "^8.18.1"
|
|
34
36
|
},
|
package/src/listenMqtt.js
CHANGED
|
@@ -39,6 +39,58 @@ const topics = [
|
|
|
39
39
|
"/webrtc_response",
|
|
40
40
|
];
|
|
41
41
|
let WebSocket_Global;
|
|
42
|
+
|
|
43
|
+
function getRandomReconnectTime() {
|
|
44
|
+
const min = 26 * 60 * 1000;
|
|
45
|
+
const max = 60 * 60 * 1000;
|
|
46
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fetchSeqID(defaultFuncs, api, ctx, callback) {
|
|
50
|
+
const form = {
|
|
51
|
+
av: ctx.globalOptions.pageID,
|
|
52
|
+
queries: JSON.stringify({
|
|
53
|
+
o0: {
|
|
54
|
+
doc_id: "3336396659757871",
|
|
55
|
+
query_params: {
|
|
56
|
+
limit: 1,
|
|
57
|
+
before: null,
|
|
58
|
+
tags: ["INBOX"],
|
|
59
|
+
includeDeliveryReceipts: false,
|
|
60
|
+
includeSeqID: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
};
|
|
65
|
+
defaultFuncs
|
|
66
|
+
.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
67
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
68
|
+
.then((resData) => {
|
|
69
|
+
if (utils.getType(resData) !== "Array") throw { error: "Not logged in", res: resData };
|
|
70
|
+
if (resData && resData[resData.length - 1].error_results > 0)
|
|
71
|
+
throw resData[0].o0.errors;
|
|
72
|
+
if (resData[resData.length - 1].successful_results === 0)
|
|
73
|
+
throw {
|
|
74
|
+
error: "getSeqId: there was no successful_results",
|
|
75
|
+
res: resData,
|
|
76
|
+
};
|
|
77
|
+
if (resData[0].o0.data.viewer.message_threads.sync_sequence_id) {
|
|
78
|
+
ctx.lastSeqId = resData[0].o0.data.viewer.message_threads.sync_sequence_id;
|
|
79
|
+
callback(null);
|
|
80
|
+
} else
|
|
81
|
+
throw {
|
|
82
|
+
error: "getSeqId: no sync_sequence_id found.",
|
|
83
|
+
res: resData,
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
.catch((err) => {
|
|
87
|
+
log.error("getSeqId", err);
|
|
88
|
+
if (utils.getType(err) === "Object" && err.error === "Not logged in")
|
|
89
|
+
ctx.loggedIn = false;
|
|
90
|
+
callback(err);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
42
94
|
// Adaptive backoff state (per-process singleton like) - tie to ctx
|
|
43
95
|
function getBackoffState(ctx){
|
|
44
96
|
if(!ctx._adaptiveReconnect){
|
|
@@ -163,15 +215,6 @@ function buildStream(options, WebSocket, Proxy) {
|
|
|
163
215
|
}
|
|
164
216
|
}
|
|
165
217
|
}, pingMs);
|
|
166
|
-
// Extended timeout for long-running stability (5 minutes instead of 1 minute)
|
|
167
|
-
const reconnectMs = parseInt(process.env.NEXUS_MQTT_RECONNECT_TIMEOUT, 10) || 300000;
|
|
168
|
-
reconnectTimeout = setTimeout(() => {
|
|
169
|
-
if (WebSocket.readyState === WebSocket.OPEN) {
|
|
170
|
-
WebSocket.close();
|
|
171
|
-
Stream.end();
|
|
172
|
-
Stream.destroy();
|
|
173
|
-
}
|
|
174
|
-
}, reconnectMs);
|
|
175
218
|
};
|
|
176
219
|
WebSocket_Global = WebSocket;
|
|
177
220
|
Proxy.on("close", () => {
|
|
@@ -341,7 +384,15 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
341
384
|
return globalCallback({ type: "not_logged_in", error: errMsg });
|
|
342
385
|
}
|
|
343
386
|
if (ctx.globalOptions.autoReconnect) {
|
|
344
|
-
|
|
387
|
+
// WS3-style: fetch SeqID then reconnect to ensure fresh state
|
|
388
|
+
fetchSeqID(defaultFuncs, api, ctx, (err) => {
|
|
389
|
+
if (err) {
|
|
390
|
+
log.warn("listenMqtt", "Failed to refresh SeqID on error, falling back to adaptive reconnect...");
|
|
391
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
392
|
+
} else {
|
|
393
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
345
396
|
} else {
|
|
346
397
|
utils.checkLiveCookie(ctx, defaultFuncs)
|
|
347
398
|
.then(() => globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }))
|
|
@@ -351,26 +402,86 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
351
402
|
// Ensure reconnection also triggers on unexpected close without prior error
|
|
352
403
|
mqttClient.on('close', function () {
|
|
353
404
|
ctx.health.onDisconnect();
|
|
354
|
-
|
|
355
|
-
|
|
405
|
+
if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
|
|
406
|
+
|
|
407
|
+
const duration = Date.now() - attemptStartTs;
|
|
408
|
+
const seconds = Math.floor(duration / 1000);
|
|
409
|
+
|
|
410
|
+
// Treat long-lived connections as normal lifecycle, keep logs calm
|
|
411
|
+
if (duration >= 30 * 60 * 1000) { // >= 30 minutes
|
|
412
|
+
log.info('listenMqtt', `MQTT connection closed after ${seconds}s (normal lifecycle). Reconnecting...`);
|
|
413
|
+
} else if (duration >= 5 * 60 * 1000) { // 5-30 minutes
|
|
414
|
+
log.info('listenMqtt', `MQTT connection closed after ${seconds}s (remote close). Reconnecting with backoff...`);
|
|
415
|
+
} else {
|
|
416
|
+
log.warn('listenMqtt', `MQTT bridge socket closed quickly after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).`);
|
|
417
|
+
}
|
|
418
|
+
|
|
356
419
|
if (!ctx.loggedIn) return; // avoid loops if logged out
|
|
357
420
|
if (ctx.globalOptions.autoReconnect) {
|
|
358
421
|
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
359
422
|
}
|
|
360
423
|
});
|
|
424
|
+
|
|
361
425
|
mqttClient.on('disconnect', function(){
|
|
362
426
|
ctx.health.onDisconnect();
|
|
363
|
-
|
|
364
|
-
|
|
427
|
+
if (ctx.health && typeof ctx.health.incFailure === 'function') { ctx.health.incFailure(); }
|
|
428
|
+
|
|
429
|
+
const duration = Date.now() - attemptStartTs;
|
|
430
|
+
const seconds = Math.floor(duration / 1000);
|
|
431
|
+
|
|
432
|
+
if (duration >= 30 * 60 * 1000) {
|
|
433
|
+
log.info('listenMqtt', `MQTT disconnected after ${seconds}s (normal lifecycle). Reconnecting...`);
|
|
434
|
+
} else if (duration >= 5 * 60 * 1000) {
|
|
435
|
+
log.info('listenMqtt', `MQTT disconnected after ${seconds}s (remote close). Reconnecting with backoff...`);
|
|
436
|
+
} else {
|
|
437
|
+
log.warn('listenMqtt', `MQTT bridge disconnect event after ${duration}ms (attempt=${ctx._mqttDiag.attempts}).`);
|
|
438
|
+
}
|
|
439
|
+
|
|
365
440
|
if (!ctx.loggedIn) return;
|
|
366
441
|
if (ctx.globalOptions.autoReconnect) {
|
|
367
442
|
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
368
443
|
}
|
|
369
444
|
});
|
|
445
|
+
|
|
370
446
|
mqttClient.on("connect", function () {
|
|
371
447
|
resetBackoff(backoff);
|
|
372
448
|
backoff.consecutiveFails = 0; // Reset consecutive failures on successful connect
|
|
449
|
+
// Reset or wrap MQTT attempt counter so long-lived sessions don't look scary
|
|
450
|
+
ctx._mqttDiag = ctx._mqttDiag || {};
|
|
451
|
+
if (typeof ctx._mqttDiag.attempts !== 'number' || ctx._mqttDiag.attempts > 100000) {
|
|
452
|
+
ctx._mqttDiag.attempts = 1;
|
|
453
|
+
} else {
|
|
454
|
+
ctx._mqttDiag.attempts++;
|
|
455
|
+
}
|
|
373
456
|
ctx.health.onConnect();
|
|
457
|
+
|
|
458
|
+
// WS3-style randomized proactive reconnect (tunable window).
|
|
459
|
+
// If globalOptions override is provided, respect that; otherwise
|
|
460
|
+
// default to a slightly longer window to avoid overlapping too much
|
|
461
|
+
// with remote/NAT closes.
|
|
462
|
+
if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer);
|
|
463
|
+
let reconnectTime = null;
|
|
464
|
+
const opts = ctx.globalOptions || {};
|
|
465
|
+
const proactiveEnabled = opts.mqttProactiveReconnectEnabled;
|
|
466
|
+
|
|
467
|
+
if (proactiveEnabled !== false) {
|
|
468
|
+
const minM = Number.isFinite(opts.mqttProactiveReconnectMinMinutes) ? opts.mqttProactiveReconnectMinMinutes : 120; // 2h
|
|
469
|
+
const maxM = Number.isFinite(opts.mqttProactiveReconnectMaxMinutes) ? opts.mqttProactiveReconnectMaxMinutes : 240; // 4h
|
|
470
|
+
const min = Math.min(minM, maxM);
|
|
471
|
+
const max = Math.max(minM, maxM);
|
|
472
|
+
const intervalMinutes = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
473
|
+
reconnectTime = intervalMinutes * 60 * 1000;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (reconnectTime) {
|
|
477
|
+
if (verboseMqtt) log.info('listenMqtt', `Scheduled proactive reconnect in ${Math.floor(reconnectTime / 60000)} minutes.`);
|
|
478
|
+
ctx._reconnectTimer = setTimeout(() => {
|
|
479
|
+
log.info('listenMqtt', `Executing proactive proactive reconnect (timer window).`);
|
|
480
|
+
if (ctx.mqttClient) ctx.mqttClient.end(true);
|
|
481
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
482
|
+
}, reconnectTime);
|
|
483
|
+
}
|
|
484
|
+
|
|
374
485
|
if (verboseMqtt) {
|
|
375
486
|
log.info('listenMqtt', `Nexus MQTT bridge established in ${(Date.now()-attemptStartTs)}ms (attempt=${ctx._mqttDiag.attempts}).`);
|
|
376
487
|
}
|
|
@@ -1089,40 +1200,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
1089
1200
|
let globalCallback = identity;
|
|
1090
1201
|
getSeqID = function getSeqID() {
|
|
1091
1202
|
ctx.t_mqttCalled = false;
|
|
1092
|
-
defaultFuncs
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (utils.getType(resData) !== "Array") throw { error: "Not logged in", res: resData };
|
|
1097
|
-
if (resData && resData[resData.length - 1].error_results > 0)
|
|
1098
|
-
throw resData[0].o0.errors;
|
|
1099
|
-
if (resData[resData.length - 1].successful_results === 0)
|
|
1100
|
-
throw {
|
|
1101
|
-
error: "getSeqId: there was no successful_results",
|
|
1102
|
-
res: resData,
|
|
1103
|
-
};
|
|
1104
|
-
if (resData[0].o0.data.viewer.message_threads.sync_sequence_id) {
|
|
1105
|
-
ctx.lastSeqId =
|
|
1106
|
-
resData[0].o0.data.viewer.message_threads.sync_sequence_id;
|
|
1107
|
-
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
1108
|
-
} else
|
|
1109
|
-
throw {
|
|
1110
|
-
error: "getSeqId: no sync_sequence_id found.",
|
|
1111
|
-
res: resData,
|
|
1112
|
-
};
|
|
1113
|
-
})
|
|
1114
|
-
.catch((err) => {
|
|
1115
|
-
log.error("getSeqId", err);
|
|
1116
|
-
if (utils.getType(err) === "Object" && err.error === "Not logged in")
|
|
1117
|
-
ctx.loggedIn = false;
|
|
1118
|
-
return globalCallback(err);
|
|
1119
|
-
});
|
|
1203
|
+
fetchSeqID(defaultFuncs, api, ctx, (err) => {
|
|
1204
|
+
if (err) return globalCallback(err);
|
|
1205
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
1206
|
+
});
|
|
1120
1207
|
};
|
|
1121
1208
|
return function (callback) {
|
|
1122
1209
|
class MessageEmitter extends EventEmitter {
|
|
1123
1210
|
stopListening(callback) {
|
|
1124
1211
|
callback = callback || (() => {});
|
|
1125
1212
|
globalCallback = identity;
|
|
1213
|
+
if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer);
|
|
1126
1214
|
if (ctx.mqttClient) {
|
|
1127
1215
|
ctx.mqttClient.unsubscribe("/webrtc");
|
|
1128
1216
|
ctx.mqttClient.unsubscribe("/rtc_multi");
|