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 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.0 – πŸ† THE BEST, SAFEST, MOST STABLE FCA
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.0 - Industry Leading Features!
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 2.1.x)
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
- const tokenMatch = html.match(/DTSGInitialData.*?token":"(.*?)"/);
213
- if (tokenMatch) {
214
- fb_dtsg = tokenMatch[1];
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
- const StealthMode = require('./StealthMode');
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
- // Initialize Stealth Mode
28
- this.stealthMode = new StealthMode({
29
- maxRequestsPerMinute: this.options.ultraLowBanMode ? 3 : 10,
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 - Much slower to avoid detection
59
+ // ULTRA-SAFE human delay patterns - Relaxed for usability
62
60
  this.humanDelayPatterns = {
63
- typing: { min: 1500, max: 3500 }, // Slower typing (1.5-3.5s)
64
- reading: { min: 3000, max: 8000 }, // Longer reading time (3-8s)
65
- thinking: { min: 5000, max: 15000 }, // Much longer thinking (5-15s)
66
- browsing: { min: 2000, max: 5000 }, // Slower browsing (2-5s)
67
- messageDelay: { min: 8000, max: 20000 } // 8-20s between messages!
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
- const expires = new Date(cookie.expires || cookie.expirationDate);
229
- const age = Date.now() - expires.getTime();
230
- return age > (30 * 24 * 60 * 60 * 1000); // 30 days
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: 3-5 hours
317
- minMs = 3 * 60 * 60 * 1000; // 3h
318
- maxMs = 5 * 60 * 60 * 1000; // 5h
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: 4-8 hours (maximum stealth!)
321
- minMs = 4 * 60 * 60 * 1000; // 4h
322
- maxMs = 8 * 60 * 60 * 1000; // 8h
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 * 60 * 1000))}h ${Math.floor((interval % (60 * 60 * 1000)) / (60 * 1000))}m (ultra-safe mode)`);
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 = 6 * 60 * 60 * 1000; // 6h
641
- const jitter = (Math.random()*80 - 40) * 60 * 1000; // Β±40m
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()*80 - 40) * 60 * 1000);
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
- // First, check Stealth Mode limits (rate limiting & pauses)
812
- if (this.stealthMode) {
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: 3, // Very low limit for safety
9
+ maxRequestsPerMinute: 1000, // Relaxed from 30 to 1000 based on user feedback
10
10
  enableRandomPauses: true,
11
- pauseProbability: 0.05, // 5% chance to take a break after each action
12
- minPauseMinutes: 5,
13
- maxPauseMinutes: 15,
14
- dailyRequestLimit: 500, // Hard limit per day
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
- console.log(`β˜• Stealth Mode: Taking a human break for ${Math.floor(duration / 60000)} minutes...`);
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
- console.log(`πŸ›‘οΈ Stealth Mode: Waiting ${Math.ceil(status.waitMs / 1000)}s (${status.reason})`);
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.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
- scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
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
- if(ctx.health && typeof ctx.health.incFailure === 'function'){ ctx.health.incFailure(); }
355
- log.warn('listenMqtt', `MQTT bridge socket closed after ${(Date.now()-attemptStartTs)}ms (attempt=${ctx._mqttDiag.attempts}).`);
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
- if(ctx.health && typeof ctx.health.incFailure === 'function'){ ctx.health.incFailure(); }
364
- log.warn('listenMqtt', `MQTT bridge disconnect event after ${(Date.now()-attemptStartTs)}ms (attempt=${ctx._mqttDiag.attempts}).`);
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
- .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
1094
- .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
1095
- .then((resData) => {
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");