nexus-fca 2.1.2 → 2.1.5
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/CHANGELOG.md +41 -1
- package/README.md +4 -1
- package/index.js +118 -6
- package/lib/health/HealthMetrics.js +100 -0
- package/lib/safety/FacebookSafety.js +71 -57
- package/package.json +1 -1
- package/src/editMessage.js +66 -42
- package/src/listenMqtt.js +128 -83
- package/src/sendMessageMqtt.js +15 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [2.1.
|
|
3
|
+
## [2.1.5] - 2025-08-28 - PendingEdits & ACK Metrics
|
|
4
|
+
### Added
|
|
5
|
+
- PendingEdits buffer with cap (default 200) + TTL (5m) + resend attempts (2) + ACK timeout (12s).
|
|
6
|
+
- Automatic edit resend watchdog with safe limits and metrics (editResends, editFailed, pendingEditsDropped, pendingEditsExpired).
|
|
7
|
+
- API: `api.setEditOptions({ maxPendingEdits, editTTLms, ackTimeoutMs, maxResendAttempts })`.
|
|
8
|
+
- Edit ACK integration: pending edit removed on ACK receipt.
|
|
9
|
+
- Health metrics: p95AckLatencyMs, editResends, editFailed.
|
|
10
|
+
|
|
11
|
+
### Improved
|
|
12
|
+
- Safer edit pipeline: prevents uncontrolled retries, bounds memory, tracks expirations.
|
|
13
|
+
- Enhanced HealthMetrics with percentile latency sample retention (50-sample window).
|
|
14
|
+
|
|
15
|
+
### Notes
|
|
16
|
+
- Durable outbound queue & metrics exporter planned next.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [2.1.4] - Adaptive Backoff & Core Metrics
|
|
21
|
+
### Added
|
|
22
|
+
- Adaptive reconnect backoff with jitter (caps at 5 minutes) for safer, stealthier recovery loops.
|
|
23
|
+
- Lazy preflight session validation (skips heavy validation if a recent successful connect occurred) toggle via `api.enableLazyPreflight()`.
|
|
24
|
+
- Health metrics collector (uptime, idle time, reconnect counts, failures, message/ack counters, synthetic keepalives) accessible with `api.getHealthMetrics()` and included in `api.healthCheck()`.
|
|
25
|
+
- Randomized synthetic keepalive interval (55-75s) to reduce detection patterns.
|
|
26
|
+
- Backoff configuration hook: `api.setBackoffOptions()`.
|
|
27
|
+
|
|
28
|
+
### Improved
|
|
29
|
+
- Reduced noisy session validation on every `listenMqtt` invocation unless needed.
|
|
30
|
+
- More structured error classification feeding metrics (`session_invalid`, `timeout_no_t_ms`, `mqtt_error`, `message_parse`, `not_logged_in`).
|
|
31
|
+
|
|
32
|
+
### Planned (Next)
|
|
33
|
+
- ACK tracking refinement & resend logic.
|
|
34
|
+
- Pending edits buffer with TTL and cap.
|
|
35
|
+
- Durable outbound queue & health exporter.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## [2.1.2] - CONTINUOUS IDLE RECOVERY
|
|
4
40
|
### Added
|
|
5
41
|
- Soft-stale probing at 2 minutes idle (ping + conditional forced reconnect if no events within 5-8s)
|
|
6
42
|
- Wrapper around `listenMqtt` to automatically feed events into safety heartbeat (`recordEvent`) for precise idle detection
|
|
43
|
+
- Ghost connection detection (10m silent but socket connected triggers forced reconnect after probe)
|
|
44
|
+
- Periodic connection recycle every ~6h ±30m to prevent long-lived silent degradation
|
|
45
|
+
- Force reconnect API: `globalSafety.forceReconnect(tag)`
|
|
7
46
|
|
|
8
47
|
### Improved
|
|
9
48
|
- Faster recovery from silent idle states (previously required >5 min or external trigger)
|
|
10
49
|
- Reduced chance of appearing online but unresponsive after short inactivity
|
|
50
|
+
- Added keepalive foreground_state publishes each heartbeat
|
|
11
51
|
|
|
12
52
|
---
|
|
13
53
|
|
package/README.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# Nexus-FCA v2.1.
|
|
1
|
+
# Nexus-FCA v2.1.5
|
|
2
|
+
|
|
3
|
+
<!-- 2.1.5 Highlights (PendingEdits & ACK Metrics) -->
|
|
4
|
+
> New in 2.1.5: PendingEdits buffer (cap + TTL + safe resend), edit ACK watchdog, p95 ACK latency & edit resend/failure metrics, configurable via `api.setEditOptions()`.
|
|
2
5
|
|
|
3
6
|
<p align="center">
|
|
4
7
|
<!-- Preview image wrapped in link (corrected ibb.co domain) -->
|
package/index.js
CHANGED
|
@@ -10,6 +10,33 @@ const path = require('path');
|
|
|
10
10
|
const models = require("./lib/database/models");
|
|
11
11
|
const logger = require("./lib/logger");
|
|
12
12
|
const { safeMode, ultraSafeMode, smartSafetyLimiter, isUserAllowed } = require('./utils'); // Enhanced safety system
|
|
13
|
+
// Minimal aesthetic banner system
|
|
14
|
+
let _fancyBannerPrinted = false;
|
|
15
|
+
const gradient = (() => { try { return require('gradient-string'); } catch(_) { return null; } })();
|
|
16
|
+
const pkgMeta = (() => { try { return require('./package.json'); } catch(_) { return { version: 'dev' }; } })();
|
|
17
|
+
function printFancyStartupBanner() {
|
|
18
|
+
if (_fancyBannerPrinted) return; _fancyBannerPrinted = true;
|
|
19
|
+
const lines = [
|
|
20
|
+
'╔══════════════════════════════════════════════════════╗',
|
|
21
|
+
'║ Nexus-FCA Secure Login ║',
|
|
22
|
+
'║ Advanced Stable Messenger Automation API ║',
|
|
23
|
+
'╚══════════════════════════════════════════════════════╝'
|
|
24
|
+
];
|
|
25
|
+
if (gradient) console.log(gradient.cristal.multiline(lines.join('\n'))); else console.log(lines.join('\n'));
|
|
26
|
+
}
|
|
27
|
+
function printIdentityBanner(uid, name) {
|
|
28
|
+
const cleanName = name || 'Unknown';
|
|
29
|
+
const pad = (txt, len) => (txt.length < len ? txt + ' '.repeat(len - txt.length) : txt.substring(0, len));
|
|
30
|
+
const bodyLen = 54;
|
|
31
|
+
const line = (content) => `║ ${pad(content, bodyLen)} ║`;
|
|
32
|
+
const box = [
|
|
33
|
+
'╔════════════════ LOGGED IN IDENTITY ════════════════╗',
|
|
34
|
+
line(`UID : ${uid}`),
|
|
35
|
+
line(`Name: ${cleanName}`),
|
|
36
|
+
'╚════════════════════════════════════════════════════╝'
|
|
37
|
+
];
|
|
38
|
+
if (gradient) console.log(gradient.atlas.multiline(box.join('\n'))); else console.log(box.join('\n'));
|
|
39
|
+
}
|
|
13
40
|
|
|
14
41
|
// Enhanced imports - All new modules
|
|
15
42
|
const { NexusClient } = require('./lib/compatibility/NexusClient');
|
|
@@ -208,8 +235,21 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
208
235
|
firstListen: true,
|
|
209
236
|
fb_dtsg,
|
|
210
237
|
wsReqNumber: 0,
|
|
211
|
-
wsTaskNumber: 0
|
|
238
|
+
wsTaskNumber: 0,
|
|
239
|
+
// Provide safety module reference to lower layers (listenMqtt)
|
|
240
|
+
globalSafety,
|
|
241
|
+
// Pending edit tracking (Stage 2)
|
|
242
|
+
pendingEdits: new Map()
|
|
212
243
|
};
|
|
244
|
+
// Default edit / resend safety settings
|
|
245
|
+
if(!globalOptions.editSettings){
|
|
246
|
+
globalOptions.editSettings = {
|
|
247
|
+
maxPendingEdits: 200,
|
|
248
|
+
editTTLms: 5*60*1000,
|
|
249
|
+
ackTimeoutMs: 12000,
|
|
250
|
+
maxResendAttempts: 2
|
|
251
|
+
};
|
|
252
|
+
}
|
|
213
253
|
const api = {
|
|
214
254
|
setOptions: setOptions.bind(null, globalOptions),
|
|
215
255
|
getAppState: function getAppState() {
|
|
@@ -222,14 +262,18 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
222
262
|
);
|
|
223
263
|
},
|
|
224
264
|
healthCheck: function(callback) {
|
|
225
|
-
// Simple health check: returns status and safeMode info
|
|
226
265
|
callback(null, {
|
|
227
266
|
status: 'ok',
|
|
228
267
|
safeMode,
|
|
229
268
|
time: new Date().toISOString(),
|
|
230
|
-
userID: ctx.userID || null
|
|
269
|
+
userID: ctx.userID || null,
|
|
270
|
+
metrics: ctx.health ? ctx.health.snapshot() : null
|
|
231
271
|
});
|
|
232
272
|
},
|
|
273
|
+
getHealthMetrics: function(){ return ctx.health ? ctx.health.snapshot() : null; },
|
|
274
|
+
enableLazyPreflight(enable=true){ ctx.globalOptions.disablePreflight = !enable; },
|
|
275
|
+
setBackoffOptions(opts={}){ ctx.globalOptions.backoff = Object.assign(ctx.globalOptions.backoff||{}, opts); },
|
|
276
|
+
setEditOptions(opts={}){ Object.assign(ctx.globalOptions.editSettings, opts); }
|
|
233
277
|
};
|
|
234
278
|
const defaultFuncs = utils.makeDefaults(html, i_userID || userID, ctx);
|
|
235
279
|
require("fs")
|
|
@@ -269,6 +313,59 @@ function buildAPI(globalOptions, html, jar) {
|
|
|
269
313
|
console.error("An error occurred while refreshing fb_dtsg", err);
|
|
270
314
|
});
|
|
271
315
|
}, 1000 * 60 * 60 * 24);
|
|
316
|
+
// === Group Queue (No Cooldown, Sequential per group) ===
|
|
317
|
+
(function initGroupQueue(){
|
|
318
|
+
const groupQueues = new Map(); // threadID -> { q: [], sending: false }
|
|
319
|
+
const isGroupThread = (tid) => typeof tid === 'string' && tid.length >= 15; // simple heuristic
|
|
320
|
+
const DIRECT_FN = api.sendMessage; // original
|
|
321
|
+
|
|
322
|
+
api.enableGroupQueue = function(enable=true){
|
|
323
|
+
globalOptions.groupQueueEnabled = !!enable;
|
|
324
|
+
};
|
|
325
|
+
api.setGroupQueueCapacity = function(n){ globalOptions.groupQueueMax = n; };
|
|
326
|
+
api.enableGroupQueue(true);
|
|
327
|
+
api.setGroupQueueCapacity(100); // allow up to 100 pending per group
|
|
328
|
+
|
|
329
|
+
api._sendMessageDirect = DIRECT_FN;
|
|
330
|
+
api.sendMessage = function(message, threadID, cb){
|
|
331
|
+
if(!globalOptions.groupQueueEnabled || !isGroupThread(threadID)) {
|
|
332
|
+
return api._sendMessageDirect(message, threadID, cb);
|
|
333
|
+
}
|
|
334
|
+
let entry = groupQueues.get(threadID);
|
|
335
|
+
if(!entry){ entry = { q: [], sending: false }; groupQueues.set(threadID, entry); }
|
|
336
|
+
if(entry.q.length >= (globalOptions.groupQueueMax||100)) {
|
|
337
|
+
// drop oldest (keep newest) to avoid unbounded growth
|
|
338
|
+
entry.q.shift();
|
|
339
|
+
}
|
|
340
|
+
entry.q.push({ message, threadID, cb });
|
|
341
|
+
processQueue(threadID, entry);
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
function processQueue(threadID, entry){
|
|
345
|
+
if(entry.sending) return;
|
|
346
|
+
if(!entry.q.length) return;
|
|
347
|
+
entry.sending = true;
|
|
348
|
+
const { message, threadID: tid, cb } = entry.q.shift();
|
|
349
|
+
api._sendMessageDirect(message, tid, function(err, res){
|
|
350
|
+
try { if(!err) globalSafety.recordEvent(); } catch(_) {}
|
|
351
|
+
if(typeof cb === 'function') cb(err, res);
|
|
352
|
+
entry.sending = false;
|
|
353
|
+
// Immediately process next (no cooldown) to keep strict sequence
|
|
354
|
+
setImmediate(()=>processQueue(threadID, entry));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
api._flushGroupQueue = function(threadID){
|
|
359
|
+
const entry = groupQueues.get(threadID);
|
|
360
|
+
if(!entry) return;
|
|
361
|
+
while(entry.q.length) {
|
|
362
|
+
const item = entry.q.shift();
|
|
363
|
+
api._sendMessageDirect(item.message, item.threadID, item.cb);
|
|
364
|
+
}
|
|
365
|
+
entry.sending = false;
|
|
366
|
+
};
|
|
367
|
+
})();
|
|
368
|
+
// === End Group Queue ===
|
|
272
369
|
return {
|
|
273
370
|
ctx,
|
|
274
371
|
defaultFuncs,
|
|
@@ -364,12 +461,25 @@ function loginHelper(appState, email, password, globalOptions, callback, prCallb
|
|
|
364
461
|
if (!safetyStatus.safe) {
|
|
365
462
|
logger(`⚠️ Login safety warning: ${safetyStatus.reason}`, 'warn');
|
|
366
463
|
}
|
|
367
|
-
|
|
368
464
|
logger('✅ Session authenticated successfully', 'info');
|
|
369
|
-
|
|
370
465
|
// Initialize safety monitoring
|
|
371
466
|
globalSafety.startMonitoring(ctx, api);
|
|
372
|
-
|
|
467
|
+
// Post-login identity banner
|
|
468
|
+
try {
|
|
469
|
+
const uid = api.getCurrentUserID && api.getCurrentUserID();
|
|
470
|
+
if (api.getUserInfo && uid) {
|
|
471
|
+
api.getUserInfo(uid, (err, info) => {
|
|
472
|
+
if (!err && info) {
|
|
473
|
+
const userObj = info[uid] || info; // depending on structure
|
|
474
|
+
printIdentityBanner(uid, userObj.name || userObj.firstName || userObj.fullName);
|
|
475
|
+
} else {
|
|
476
|
+
printIdentityBanner(uid || 'N/A');
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
} else {
|
|
480
|
+
printIdentityBanner(uid || 'N/A');
|
|
481
|
+
}
|
|
482
|
+
} catch(_) { /* ignore */ }
|
|
373
483
|
callback(null, api);
|
|
374
484
|
})
|
|
375
485
|
.catch(e => {
|
|
@@ -893,6 +1003,7 @@ class IntegratedNexusLoginSystem {
|
|
|
893
1003
|
|
|
894
1004
|
// Integrated Nexus Login wrapper for easy usage
|
|
895
1005
|
async function integratedNexusLogin(credentials = null, options = {}) {
|
|
1006
|
+
printFancyStartupBanner();
|
|
896
1007
|
const loginSystem = new IntegratedNexusLoginSystem(options);
|
|
897
1008
|
|
|
898
1009
|
// Professional logging system
|
|
@@ -1008,6 +1119,7 @@ async function integratedNexusLogin(credentials = null, options = {}) {
|
|
|
1008
1119
|
* - Appstate only: Uses existing session directly
|
|
1009
1120
|
*/
|
|
1010
1121
|
async function login(loginData, options = {}, callback) {
|
|
1122
|
+
printFancyStartupBanner();
|
|
1011
1123
|
// Support multiple callback signatures
|
|
1012
1124
|
if (typeof options === 'function') {
|
|
1013
1125
|
callback = options;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Lightweight health & metrics tracker for Nexus-FCA (extended Stage 2)
|
|
3
|
+
class HealthMetrics {
|
|
4
|
+
constructor() {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
this.uptimeStart = now;
|
|
7
|
+
this.lastConnectTs = 0;
|
|
8
|
+
this.lastDisconnectTs = 0;
|
|
9
|
+
this.lastMessageTs = 0;
|
|
10
|
+
this.lastErrorTs = 0;
|
|
11
|
+
this.lastErrorType = null;
|
|
12
|
+
this.reconnects = 0;
|
|
13
|
+
this.consecutiveFailures = 0;
|
|
14
|
+
this.messagesReceived = 0;
|
|
15
|
+
this.syntheticKeepAlives = 0;
|
|
16
|
+
this.acksReceived = 0;
|
|
17
|
+
this.pendingEdits = 0;
|
|
18
|
+
this.pendingEditMap = new Map(); // messageID -> { ts, text, attempts }
|
|
19
|
+
this.pendingEditsDropped = 0; // dropped due to cap
|
|
20
|
+
this.pendingEditsExpired = 0; // expired via TTL sweep
|
|
21
|
+
this.outboundQueueDepth = 0;
|
|
22
|
+
this.outboundQueueDropped = 0;
|
|
23
|
+
this.lastAckLatencyMs = null;
|
|
24
|
+
this.avgAckLatencyMs = null;
|
|
25
|
+
this._ackLatencySamples = [];
|
|
26
|
+
this.p95AckLatencyMs = null;
|
|
27
|
+
this.editResends = 0;
|
|
28
|
+
this.editFailed = 0;
|
|
29
|
+
}
|
|
30
|
+
onConnect() { this.lastConnectTs = Date.now(); this.consecutiveFailures = 0; }
|
|
31
|
+
onDisconnect() { this.lastDisconnectTs = Date.now(); }
|
|
32
|
+
onMessage() { this.messagesReceived++; this.lastMessageTs = Date.now(); }
|
|
33
|
+
onSynthetic() { this.syntheticKeepAlives++; }
|
|
34
|
+
onAck(latencyMs){
|
|
35
|
+
this.acksReceived++;
|
|
36
|
+
if(typeof latencyMs === 'number'){
|
|
37
|
+
this.lastAckLatencyMs = latencyMs;
|
|
38
|
+
if(this.avgAckLatencyMs == null) this.avgAckLatencyMs = latencyMs; else this.avgAckLatencyMs = Math.round(this.avgAckLatencyMs*0.8 + latencyMs*0.2);
|
|
39
|
+
// track distribution (cap list length for memory safety)
|
|
40
|
+
this._ackLatencySamples.push(latencyMs);
|
|
41
|
+
if(this._ackLatencySamples.length > 50) this._ackLatencySamples.shift();
|
|
42
|
+
this._recalcP95();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
_recalcP95(){
|
|
46
|
+
if(!this._ackLatencySamples.length){ this.p95AckLatencyMs = null; return; }
|
|
47
|
+
const sorted = [...this._ackLatencySamples].sort((a,b)=>a-b);
|
|
48
|
+
const idx = Math.min(sorted.length-1, Math.floor(sorted.length*0.95));
|
|
49
|
+
this.p95AckLatencyMs = sorted[idx];
|
|
50
|
+
}
|
|
51
|
+
onError(type){ this.lastErrorTs = Date.now(); this.lastErrorType = type || 'unknown'; }
|
|
52
|
+
onReconnectScheduled(delay){ this.reconnects++; this.currentBackoffDelay = delay; if(delay > (this.maxObservedBackoff||0)) this.maxObservedBackoff = delay; }
|
|
53
|
+
trackOutbound(depth){ this.outboundQueueDepth = depth; }
|
|
54
|
+
incOutboundDropped(){ this.outboundQueueDropped++; }
|
|
55
|
+
addPendingEdit(mid, text){ this.pendingEditMap.set(mid, { ts: Date.now(), text, attempts: 0 }); this.pendingEdits = this.pendingEditMap.size; }
|
|
56
|
+
markEditResent(mid){ const rec = this.pendingEditMap.get(mid); if(rec){ rec.attempts++; this.editResends++; } }
|
|
57
|
+
markEditFailed(mid){ if(this.pendingEditMap.delete(mid)) { this.pendingEdits = this.pendingEditMap.size; this.editFailed++; } }
|
|
58
|
+
removePendingEdit(mid){ if(this.pendingEditMap.delete(mid)) this.pendingEdits = this.pendingEditMap.size; }
|
|
59
|
+
incPendingEditDropped(){ this.pendingEditsDropped++; }
|
|
60
|
+
incPendingEditExpired(n=1){ this.pendingEditsExpired += n; }
|
|
61
|
+
sweepPendingEdits(ttlMs){
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
let expired = 0;
|
|
64
|
+
for(const [mid, val] of this.pendingEditMap.entries()){
|
|
65
|
+
if(now - val.ts > ttlMs){ this.pendingEditMap.delete(mid); expired++; }
|
|
66
|
+
}
|
|
67
|
+
if(expired) { this.incPendingEditExpired(expired); }
|
|
68
|
+
this.pendingEdits = this.pendingEditMap.size;
|
|
69
|
+
}
|
|
70
|
+
snapshot(){
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
const idleMs = now - (this.lastMessageTs || this.lastConnectTs || now);
|
|
73
|
+
return {
|
|
74
|
+
uptimeMs: now - this.uptimeStart,
|
|
75
|
+
idleMs,
|
|
76
|
+
reconnects: this.reconnects,
|
|
77
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
78
|
+
lastErrorType: this.lastErrorType,
|
|
79
|
+
lastErrorAgoMs: this.lastErrorTs ? now - this.lastErrorTs : null,
|
|
80
|
+
currentBackoffDelay: this.currentBackoffDelay||0,
|
|
81
|
+
maxObservedBackoff: this.maxObservedBackoff||0,
|
|
82
|
+
messagesReceived: this.messagesReceived,
|
|
83
|
+
syntheticKeepAlives: this.syntheticKeepAlives,
|
|
84
|
+
acksReceived: this.acksReceived,
|
|
85
|
+
lastAckLatencyMs: this.lastAckLatencyMs,
|
|
86
|
+
avgAckLatencyMs: this.avgAckLatencyMs,
|
|
87
|
+
p95AckLatencyMs: this.p95AckLatencyMs,
|
|
88
|
+
pendingEdits: this.pendingEdits,
|
|
89
|
+
pendingEditsDropped: this.pendingEditsDropped,
|
|
90
|
+
pendingEditsExpired: this.pendingEditsExpired,
|
|
91
|
+
editResends: this.editResends,
|
|
92
|
+
editFailed: this.editFailed,
|
|
93
|
+
outboundQueueDepth: this.outboundQueueDepth,
|
|
94
|
+
outboundQueueDropped: this.outboundQueueDropped,
|
|
95
|
+
healthy: this.isHealthy(idleMs)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
isHealthy(idleMs){ if (this.consecutiveFailures >= 10) return false; if (this.messagesReceived < 5 && idleMs > 5*60*1000) return false; return true; }
|
|
99
|
+
}
|
|
100
|
+
module.exports = { HealthMetrics };
|
|
@@ -68,6 +68,10 @@ class FacebookSafety {
|
|
|
68
68
|
this._inFlightRefreshId = 0;
|
|
69
69
|
// New: probing guard to avoid overlapping soft-stale probes
|
|
70
70
|
this._probing = false;
|
|
71
|
+
// Ghost detection guard
|
|
72
|
+
this._ghostChecking = false;
|
|
73
|
+
// Periodic recycle timer
|
|
74
|
+
this._periodicRecycleTimer = null;
|
|
71
75
|
|
|
72
76
|
this.initSafety();
|
|
73
77
|
}
|
|
@@ -80,6 +84,7 @@ class FacebookSafety {
|
|
|
80
84
|
|
|
81
85
|
// Setup session monitoring
|
|
82
86
|
this.setupSessionMonitoring();
|
|
87
|
+
this._schedulePeriodicRecycle();
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -245,18 +250,19 @@ class FacebookSafety {
|
|
|
245
250
|
clearTimeout(this._safeRefreshTimer);
|
|
246
251
|
this._safeRefreshTimer = null;
|
|
247
252
|
}
|
|
248
|
-
//
|
|
253
|
+
// Stealth+Resilient profile refresh policy:
|
|
254
|
+
// risk low: 50-60m, medium: 40-50m, high: 25-35m (random inside band)
|
|
249
255
|
const schedule = () => {
|
|
250
256
|
if (this._destroyed) return;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const interval =
|
|
257
|
+
let minM, maxM;
|
|
258
|
+
if (this.sessionMetrics.riskLevel === 'high') { minM = 25; maxM = 35; }
|
|
259
|
+
else if (this.sessionMetrics.riskLevel === 'medium') { minM = 40; maxM = 50; }
|
|
260
|
+
else { minM = 50; maxM = 60; }
|
|
261
|
+
const interval = (minM * 60 * 1000) + Math.random() * ((maxM - minM) * 60 * 1000);
|
|
256
262
|
this._safeRefreshTimer = setTimeout(async () => {
|
|
257
263
|
await this.refreshSafeSession();
|
|
258
264
|
schedule();
|
|
259
|
-
},
|
|
265
|
+
}, interval);
|
|
260
266
|
};
|
|
261
267
|
schedule();
|
|
262
268
|
}
|
|
@@ -311,40 +317,27 @@ class FacebookSafety {
|
|
|
311
317
|
const now = Date.now();
|
|
312
318
|
const disconnected = !this.ctx || !this.ctx.mqttClient || !this.ctx.mqttClient.connected;
|
|
313
319
|
const idle = now - this._lastEventTs;
|
|
314
|
-
const softStale = idle > 2 * 60 * 1000; //
|
|
315
|
-
const hardStale = idle >
|
|
316
|
-
const stale = hardStale;
|
|
317
|
-
|
|
318
|
-
// If totally disconnected or hard stale -> reconnect immediately
|
|
320
|
+
const softStale = idle > (2.5 * 60 * 1000); // Stealth profile: 2m30s
|
|
321
|
+
const hardStale = idle > 8 * 60 * 1000; // escalate earlier than watchdog hard (8m)
|
|
322
|
+
const stale = hardStale;
|
|
319
323
|
if (disconnected || stale) {
|
|
320
324
|
await this._reconnectMqttWithBackoff(disconnected ? 'disconnected' : 'hard-stale');
|
|
321
325
|
return;
|
|
322
326
|
}
|
|
323
|
-
|
|
324
|
-
// Soft-stale probing: connection claims to be open but no events for 2-5 minutes.
|
|
325
|
-
// We issue a ping and if still no events after probe window, we force a reconnect.
|
|
326
327
|
if (softStale && !this._probing) {
|
|
327
328
|
this._probing = true;
|
|
328
329
|
const prevTs = this._lastEventTs;
|
|
329
|
-
try {
|
|
330
|
-
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
331
|
-
if (typeof this.ctx.mqttClient.ping === 'function') {
|
|
332
|
-
try { this.ctx.mqttClient.ping(); } catch(_) {}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
} catch(_) {}
|
|
330
|
+
try { if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected && typeof this.ctx.mqttClient.ping === 'function') this.ctx.mqttClient.ping(); } catch(_) {}
|
|
336
331
|
setTimeout(() => {
|
|
337
332
|
if (this._destroyed) return;
|
|
338
|
-
// If no new events arrived since probe start, treat as latent-dead connection
|
|
339
333
|
if (this._lastEventTs <= prevTs) {
|
|
340
|
-
// Reset backoff to allow immediate reconnect (latency sensitive)
|
|
341
334
|
this._backoff.attempt = 0;
|
|
342
335
|
this._reconnectMqttWithBackoff('soft-stale');
|
|
343
336
|
}
|
|
344
337
|
this._probing = false;
|
|
345
|
-
},
|
|
338
|
+
}, 6000 + Math.random() * 2000); // 6-8s probe window (Stealth+Resilient)
|
|
346
339
|
}
|
|
347
|
-
} catch
|
|
340
|
+
} catch(_) {}
|
|
348
341
|
}
|
|
349
342
|
|
|
350
343
|
// Progressive backoff + jitter reconnect
|
|
@@ -353,38 +346,47 @@ class FacebookSafety {
|
|
|
353
346
|
this._reconnecting = true;
|
|
354
347
|
try {
|
|
355
348
|
const now = Date.now();
|
|
356
|
-
if (now < this._backoff.next) {
|
|
357
|
-
return; // respect backoff window
|
|
358
|
-
}
|
|
349
|
+
if (now < this._backoff.next) { return; }
|
|
359
350
|
const attempt = ++this._backoff.attempt;
|
|
360
|
-
|
|
361
|
-
const
|
|
351
|
+
// Stealth backoff: 1.2s * 1.8^n capped ~20s, add jitter 0-500ms
|
|
352
|
+
const baseDelay = Math.min(20000, 1200 * Math.pow(1.8, Math.min(attempt, 6)));
|
|
353
|
+
const jitter = Math.random() * 500;
|
|
362
354
|
const delay = baseDelay + jitter;
|
|
363
355
|
this._backoff.next = now + delay;
|
|
364
356
|
await new Promise(r => setTimeout(r, delay));
|
|
365
|
-
|
|
366
|
-
if (this._activeListenerStop && typeof this._activeListenerStop === 'function') {
|
|
367
|
-
try { this._activeListenerStop(); } catch (_) {}
|
|
368
|
-
}
|
|
357
|
+
if (this._activeListenerStop && typeof this._activeListenerStop === 'function') { try { this._activeListenerStop(); } catch(_) {} }
|
|
369
358
|
if (this.api && typeof this.api.listenMqtt === 'function' && !this._destroyed) {
|
|
370
|
-
const stop = this.api.listenMqtt((err, event) => {
|
|
371
|
-
if (!err && event) this.recordEvent();
|
|
372
|
-
});
|
|
359
|
+
const stop = this.api.listenMqtt((err, event) => { if (!err && event) this.recordEvent(); });
|
|
373
360
|
this._activeListenerStop = stop;
|
|
374
|
-
|
|
375
|
-
else this.safetyEmit('mqttReconnect', { success: true, reason });
|
|
361
|
+
this.safetyEmit('mqttReconnect', { success: true, reason, attempt, delay });
|
|
376
362
|
}
|
|
377
|
-
// Reset backoff on success detection soon after
|
|
378
363
|
setTimeout(() => {
|
|
379
|
-
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
380
|
-
this._backoff.attempt = 0;
|
|
381
|
-
}
|
|
364
|
+
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) { this._backoff.attempt = 0; }
|
|
382
365
|
}, 5000);
|
|
383
|
-
} catch
|
|
384
|
-
this.safetyEmit('mqttReconnect', { success: false, error: e.message });
|
|
385
|
-
} finally {
|
|
386
|
-
|
|
387
|
-
|
|
366
|
+
} catch(e) {
|
|
367
|
+
this.safetyEmit('mqttReconnect', { success: false, error: e.message, reason });
|
|
368
|
+
} finally { this._reconnecting = false; }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Public force reconnect (bypass backoff)
|
|
372
|
+
forceReconnect(tag = 'manual') {
|
|
373
|
+
if (this._destroyed) return;
|
|
374
|
+
this._backoff.attempt = 0;
|
|
375
|
+
return this._reconnectMqttWithBackoff('force-' + tag);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Schedule periodic recycle (connection rejuvenation) every 6h ±30m jitter
|
|
379
|
+
_schedulePeriodicRecycle() {
|
|
380
|
+
if (this._periodicRecycleTimer) clearTimeout(this._periodicRecycleTimer);
|
|
381
|
+
if (this._destroyed) return;
|
|
382
|
+
const base = 6 * 60 * 60 * 1000; // 6h
|
|
383
|
+
const jitter = (Math.random() * 60 - 30) * 60 * 1000; // ±30m
|
|
384
|
+
const delay = base + jitter;
|
|
385
|
+
this._periodicRecycleTimer = setTimeout(() => {
|
|
386
|
+
if (this._destroyed) return;
|
|
387
|
+
this.forceReconnect('periodic');
|
|
388
|
+
this._schedulePeriodicRecycle();
|
|
389
|
+
}, delay);
|
|
388
390
|
}
|
|
389
391
|
|
|
390
392
|
// Heartbeat ping & watchdog
|
|
@@ -392,26 +394,38 @@ class FacebookSafety {
|
|
|
392
394
|
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
393
395
|
if (this._watchdogTimer) clearInterval(this._watchdogTimer);
|
|
394
396
|
if (this._destroyed) return;
|
|
397
|
+
// Stealth profile heartbeat: 80–100s random
|
|
395
398
|
this._heartbeatTimer = setInterval(() => {
|
|
396
399
|
if (this._destroyed) return;
|
|
397
400
|
try {
|
|
398
401
|
if (this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
399
402
|
if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping();
|
|
403
|
+
try { this.ctx.mqttClient.publish('/foreground_state', JSON.stringify({ foreground: true })); } catch(_) {}
|
|
400
404
|
this.safetyEmit('heartbeat', { ts: Date.now() });
|
|
401
405
|
}
|
|
402
|
-
} catch
|
|
403
|
-
},
|
|
406
|
+
} catch(_) {}
|
|
407
|
+
}, (80 + Math.random()*20) * 1000);
|
|
404
408
|
this._watchdogTimer = setInterval(() => {
|
|
405
409
|
if (this._destroyed) return;
|
|
406
410
|
const idle = Date.now() - this._lastEventTs;
|
|
407
|
-
|
|
408
|
-
|
|
411
|
+
// Soft escalate already handled inside _ensureMqttAlive at 2m30s
|
|
412
|
+
// Ghost detection earlier: 9m
|
|
413
|
+
if (idle > 9 * 60 * 1000 && !this._ghostChecking && this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected) {
|
|
414
|
+
this._ghostChecking = true;
|
|
415
|
+
const before = this._lastEventTs;
|
|
416
|
+
try { if (this.ctx.mqttClient.ping) this.ctx.mqttClient.ping(); } catch(_) {}
|
|
417
|
+
setTimeout(() => {
|
|
418
|
+
if (this._destroyed) return;
|
|
419
|
+
if (this._lastEventTs <= before) { this.forceReconnect('ghost'); }
|
|
420
|
+
setTimeout(() => { this._ghostChecking = false; }, 5 * 60 * 1000);
|
|
421
|
+
}, 6000 + Math.random()*2000);
|
|
409
422
|
}
|
|
410
|
-
|
|
411
|
-
|
|
423
|
+
// Hard watchdog escalate: 12m
|
|
424
|
+
if (idle > 12 * 60 * 1000) {
|
|
425
|
+
this._backoff.attempt = 0;
|
|
412
426
|
this._ensureMqttAlive();
|
|
413
427
|
}
|
|
414
|
-
},
|
|
428
|
+
}, 35 * 1000); // slight change to avoid pattern
|
|
415
429
|
}
|
|
416
430
|
|
|
417
431
|
/**
|
|
@@ -536,7 +550,7 @@ class FacebookSafety {
|
|
|
536
550
|
// Cleanup / destroy resources (to prevent dangling timers)
|
|
537
551
|
destroy() {
|
|
538
552
|
this._destroyed = true;
|
|
539
|
-
const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer];
|
|
553
|
+
const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer];
|
|
540
554
|
timers.forEach(t => t && clearTimeout(t));
|
|
541
555
|
if (this._activeListenerStop) {
|
|
542
556
|
try { this._activeListenerStop(); } catch (_) {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.5",
|
|
4
4
|
"description": "A modern, safe, and advanced Facebook Chat API for Node.js with fully integrated Nexus Login System. NPM-ready with ID/password/2FA support, ultra-low ban rate protection, and zero external dependencies.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
package/src/editMessage.js
CHANGED
|
@@ -1,57 +1,81 @@
|
|
|
1
1
|
// Nexus-FCA: Advanced and Safe Facebook Chat API
|
|
2
|
-
// editMessage.js - Edit a sent message via MQTT
|
|
2
|
+
// editMessage.js - Edit a sent message via MQTT with PendingEdits buffer & safe resend
|
|
3
3
|
|
|
4
4
|
const { generateOfflineThreadingID } = require('../utils');
|
|
5
5
|
|
|
6
|
-
function canBeCalled(func) {
|
|
7
|
-
try {
|
|
8
|
-
Reflect.apply(func, null, []);
|
|
9
|
-
return true;
|
|
10
|
-
} catch (error) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
16
7
|
return function editMessage(text, messageID, callback) {
|
|
8
|
+
callback = callback || function(){};
|
|
17
9
|
if (!ctx.mqttClient) {
|
|
18
|
-
|
|
10
|
+
return callback(new Error('Not connected to MQTT'));
|
|
11
|
+
}
|
|
12
|
+
if(!messageID || typeof text !== 'string') {
|
|
13
|
+
return callback(new Error('Invalid arguments for editMessage'));
|
|
14
|
+
}
|
|
15
|
+
// Safety: manage pending edits buffer with cap & TTL
|
|
16
|
+
const settings = ctx.globalOptions.editSettings || { maxPendingEdits:200, editTTLms:300000, ackTimeoutMs:12000, maxResendAttempts:2 };
|
|
17
|
+
// Drop oldest if capacity reached
|
|
18
|
+
if(ctx.pendingEdits.size >= settings.maxPendingEdits){
|
|
19
|
+
const firstKey = ctx.pendingEdits.keys().next().value;
|
|
20
|
+
if(firstKey){ ctx.pendingEdits.delete(firstKey); if(ctx.health) ctx.health.incPendingEditDropped(); }
|
|
19
21
|
}
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
ctx.pendingEdits.set(messageID, { text, ts: now, attempts: 0 });
|
|
24
|
+
if(ctx.health) ctx.health.addPendingEdit(messageID, text);
|
|
20
25
|
|
|
21
26
|
ctx.wsReqNumber += 1;
|
|
22
27
|
ctx.wsTaskNumber += 1;
|
|
23
28
|
|
|
24
|
-
const queryPayload = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const query = {
|
|
30
|
-
failure_count: null,
|
|
31
|
-
label: '742',
|
|
32
|
-
payload: JSON.stringify(queryPayload),
|
|
33
|
-
queue_name: 'edit_message',
|
|
34
|
-
task_id: ctx.wsTaskNumber
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const context = {
|
|
38
|
-
app_id: '2220391788200892',
|
|
39
|
-
payload: JSON.stringify({
|
|
40
|
-
data_trace_id: null,
|
|
41
|
-
epoch_id: parseInt(generateOfflineThreadingID()),
|
|
42
|
-
tasks: [query],
|
|
43
|
-
version_id: '6903494529735864'
|
|
44
|
-
}),
|
|
45
|
-
request_id: ctx.wsReqNumber,
|
|
46
|
-
type: 3
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// if (canBeCalled(callback)) {
|
|
50
|
-
// ctx.reqCallbacks[ctx.wsReqNumber] = callback;
|
|
51
|
-
// }
|
|
29
|
+
const queryPayload = { message_id: messageID, text };
|
|
30
|
+
const query = { failure_count: null, label: '742', payload: JSON.stringify(queryPayload), queue_name: 'edit_message', task_id: ctx.wsTaskNumber };
|
|
31
|
+
const context = { app_id: '2220391788200892', payload: JSON.stringify({ data_trace_id: null, epoch_id: parseInt(generateOfflineThreadingID()), tasks: [query], version_id: '6903494529735864' }), request_id: ctx.wsReqNumber, type: 3 };
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
qos:
|
|
55
|
-
|
|
33
|
+
try {
|
|
34
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos:1, retain:false }, (err)=>{
|
|
35
|
+
if(err){
|
|
36
|
+
if(ctx.health) ctx.health.onError('edit_publish_fail');
|
|
37
|
+
return callback(err);
|
|
38
|
+
}
|
|
39
|
+
// Schedule ACK / resend watchdog
|
|
40
|
+
scheduleEditAckWatch(messageID, settings, ctx, api, callback);
|
|
41
|
+
callback(null, { queued:true, messageID });
|
|
42
|
+
});
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if(ctx.health) ctx.health.onError('edit_exception');
|
|
45
|
+
return callback(e);
|
|
46
|
+
}
|
|
56
47
|
};
|
|
57
48
|
};
|
|
49
|
+
|
|
50
|
+
function scheduleEditAckWatch(messageID, settings, ctx, api, originalCb){
|
|
51
|
+
if(!settings || !ctx) return;
|
|
52
|
+
const { ackTimeoutMs=12000, maxResendAttempts=2, editTTLms=300000 } = settings;
|
|
53
|
+
setTimeout(()=>{
|
|
54
|
+
const rec = ctx.pendingEdits.get(messageID);
|
|
55
|
+
if(!rec) return; // already acked or removed
|
|
56
|
+
const age = Date.now() - rec.ts;
|
|
57
|
+
if(age > editTTLms){
|
|
58
|
+
ctx.pendingEdits.delete(messageID);
|
|
59
|
+
if(ctx.health){ ctx.health.removePendingEdit(messageID); ctx.health.incPendingEditExpired(); }
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if(rec.attempts >= maxResendAttempts){
|
|
63
|
+
ctx.pendingEdits.delete(messageID);
|
|
64
|
+
if(ctx.health) ctx.health.markEditFailed(messageID);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Resend
|
|
68
|
+
try {
|
|
69
|
+
rec.attempts++;
|
|
70
|
+
if(ctx.health) ctx.health.markEditResent(messageID);
|
|
71
|
+
const queryPayload = { message_id: messageID, text: rec.text };
|
|
72
|
+
const resend = { failure_count:null, label:'742', payload: JSON.stringify(queryPayload), queue_name:'edit_message', task_id: ++ctx.wsTaskNumber };
|
|
73
|
+
const context = { app_id: '2220391788200892', payload: JSON.stringify({ data_trace_id:null, epoch_id: parseInt(generateOfflineThreadingID()), tasks:[resend], version_id:'6903494529735864' }), request_id: ++ctx.wsReqNumber, type:3 };
|
|
74
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos:1, retain:false });
|
|
75
|
+
// Chain another watch if still pending
|
|
76
|
+
scheduleEditAckWatch(messageID, settings, ctx, api, originalCb);
|
|
77
|
+
} catch(e){
|
|
78
|
+
if(ctx.health) ctx.health.onError('edit_resend_exception');
|
|
79
|
+
}
|
|
80
|
+
}, ackTimeoutMs);
|
|
81
|
+
}
|
package/src/listenMqtt.js
CHANGED
|
@@ -11,6 +11,7 @@ var identity = function () {};
|
|
|
11
11
|
var form = {};
|
|
12
12
|
var getSeqID = function () {};
|
|
13
13
|
const logger = require("../lib/logger.js");
|
|
14
|
+
const { HealthMetrics } = require("../lib/health/HealthMetrics");
|
|
14
15
|
|
|
15
16
|
// Enhanced imports
|
|
16
17
|
const MqttManager = require("../lib/mqtt/MqttManager");
|
|
@@ -38,6 +39,43 @@ const topics = [
|
|
|
38
39
|
"/webrtc_response",
|
|
39
40
|
];
|
|
40
41
|
let WebSocket_Global;
|
|
42
|
+
// Adaptive backoff state (per-process singleton like) - tie to ctx
|
|
43
|
+
function getBackoffState(ctx){
|
|
44
|
+
if(!ctx._adaptiveReconnect){
|
|
45
|
+
ctx._adaptiveReconnect = {
|
|
46
|
+
base: 1000, // 1s
|
|
47
|
+
max: 5 * 60 * 1000, // 5m
|
|
48
|
+
factor: 2,
|
|
49
|
+
jitter: 0.25, // 25% random
|
|
50
|
+
current: 0,
|
|
51
|
+
lastResetTs: 0
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return ctx._adaptiveReconnect;
|
|
55
|
+
}
|
|
56
|
+
function computeNextDelay(state){
|
|
57
|
+
if(!state.current) state.current = state.base;
|
|
58
|
+
else state.current = Math.min(state.max, state.current * state.factor);
|
|
59
|
+
// jitter
|
|
60
|
+
const rand = (Math.random() * 2 - 1) * state.jitter; // -j..+j
|
|
61
|
+
const delay = Math.max(500, Math.round(state.current * (1 + rand)));
|
|
62
|
+
return delay;
|
|
63
|
+
}
|
|
64
|
+
function resetBackoff(state){
|
|
65
|
+
state.current = 0;
|
|
66
|
+
state.lastResetTs = Date.now();
|
|
67
|
+
}
|
|
68
|
+
// Build lazy preflight gating
|
|
69
|
+
function shouldRunPreflight(ctx){
|
|
70
|
+
if(ctx.globalOptions.disablePreflight) return false;
|
|
71
|
+
// If we connected successfully within last 10 minutes, skip heavy preflight to reduce surface.
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const metrics = ctx.health;
|
|
74
|
+
if(metrics && metrics.lastConnectTs && (now - metrics.lastConnectTs) < 10*60*1000){
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
41
79
|
function buildProxy() {
|
|
42
80
|
const Proxy = new Transform({
|
|
43
81
|
objectMode: false,
|
|
@@ -128,20 +166,23 @@ function buildStream(options, WebSocket, Proxy) {
|
|
|
128
166
|
return Stream;
|
|
129
167
|
}
|
|
130
168
|
function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
131
|
-
//
|
|
132
|
-
if
|
|
169
|
+
// Attach health metrics container lazily
|
|
170
|
+
if(!ctx.health) ctx.health = new (require('../lib/health/HealthMetrics').HealthMetrics)();
|
|
171
|
+
const backoff = getBackoffState(ctx);
|
|
172
|
+
const runPreflight = shouldRunPreflight(ctx);
|
|
173
|
+
if (runPreflight) {
|
|
133
174
|
(async () => {
|
|
134
175
|
try {
|
|
135
|
-
await utils.validateSession(ctx, defaultFuncs, { retries:
|
|
176
|
+
await utils.validateSession(ctx, defaultFuncs, { retries: 1, delayMs: 1000 });
|
|
136
177
|
} catch (e) {
|
|
137
|
-
// Suppress first failure; only emit if still bad after short grace period
|
|
138
178
|
setTimeout(() => {
|
|
139
179
|
utils.validateSession(ctx, defaultFuncs, { retries: 0 }).catch(err2 => {
|
|
140
180
|
log.error("listenMqtt", "Session invalid after retry: Not logged in.");
|
|
141
181
|
ctx.loggedIn = false;
|
|
182
|
+
ctx.health.onError('session_invalid');
|
|
142
183
|
globalCallback({ type: "not_logged_in", error: "Session invalid (post-retry)." });
|
|
143
184
|
});
|
|
144
|
-
},
|
|
185
|
+
}, 1500);
|
|
145
186
|
}
|
|
146
187
|
})();
|
|
147
188
|
}
|
|
@@ -170,6 +211,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
170
211
|
p: null,
|
|
171
212
|
php_override: ""
|
|
172
213
|
};
|
|
214
|
+
// jitter user agent keep consistent
|
|
173
215
|
const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
|
|
174
216
|
let host;
|
|
175
217
|
if (ctx.mqttEndpoint) {
|
|
@@ -233,33 +275,41 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
233
275
|
const mqttClient = ctx.mqttClient;
|
|
234
276
|
global.mqttClient = mqttClient;
|
|
235
277
|
mqttClient.on('error', function (err) {
|
|
236
|
-
log.error("listenMqtt", err);
|
|
237
|
-
mqttClient.end();
|
|
238
|
-
// classify redirect/login errors surfaced through upstream logic
|
|
239
278
|
const errMsg = (err && (err.error || err.message || "")).toString();
|
|
279
|
+
ctx.health.onError(errMsg.includes('not logged in') ? 'not_logged_in' : 'mqtt_error');
|
|
280
|
+
log.error("listenMqtt", errMsg);
|
|
281
|
+
try { mqttClient.end(true); } catch(_){ }
|
|
240
282
|
if (/not logged in|login_redirect|html_login_page/i.test(errMsg)) {
|
|
241
283
|
ctx.loggedIn = false;
|
|
242
284
|
return globalCallback({ type: "not_logged_in", error: errMsg });
|
|
243
285
|
}
|
|
244
286
|
if (ctx.globalOptions.autoReconnect) {
|
|
245
|
-
|
|
287
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
246
288
|
} else {
|
|
247
289
|
utils.checkLiveCookie(ctx, defaultFuncs)
|
|
248
|
-
.then(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
290
|
+
.then(() => globalCallback({ type: "stop_listen", error: "Connection refused: Server unavailable" }))
|
|
291
|
+
.catch(() => globalCallback({ type: "account_inactive", error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com" }));
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
// Ensure reconnection also triggers on unexpected close without prior error
|
|
295
|
+
mqttClient.on('close', function () {
|
|
296
|
+
ctx.health.onDisconnect();
|
|
297
|
+
if (!ctx.loggedIn) return; // avoid loops if logged out
|
|
298
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
299
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
mqttClient.on('disconnect', function(){
|
|
303
|
+
ctx.health.onDisconnect();
|
|
304
|
+
if (!ctx.loggedIn) return;
|
|
305
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
306
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
260
307
|
}
|
|
261
308
|
});
|
|
262
309
|
mqttClient.on("connect", function () {
|
|
310
|
+
resetBackoff(backoff);
|
|
311
|
+
ctx.health.onConnect();
|
|
312
|
+
if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
|
|
263
313
|
if (process.env.OnStatus === undefined) {
|
|
264
314
|
logger("Nexus-FCA premium features works only with Nexus-Bot framework(Kidding)", "info");
|
|
265
315
|
process.env.OnStatus = true;
|
|
@@ -295,9 +345,11 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
295
345
|
JSON.stringify({ make_user_available_when_in_foreground: true }),
|
|
296
346
|
{ qos: 1 }
|
|
297
347
|
);
|
|
348
|
+
// Replace fixed rTimeout reconnect with health-driven logic
|
|
298
349
|
const rTimeout = setTimeout(function () {
|
|
350
|
+
ctx.health.onError('timeout_no_t_ms');
|
|
299
351
|
mqttClient.end();
|
|
300
|
-
|
|
352
|
+
scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback);
|
|
301
353
|
}, 5000);
|
|
302
354
|
ctx.tmsWait = function () {
|
|
303
355
|
clearTimeout(rTimeout);
|
|
@@ -311,14 +363,35 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
311
363
|
};
|
|
312
364
|
});
|
|
313
365
|
mqttClient.on("message", function (topic, message, _packet) {
|
|
366
|
+
ctx.health.onMessage();
|
|
367
|
+
if (ctx.globalSafety) { try { ctx.globalSafety.recordEvent(); } catch(_) {} }
|
|
314
368
|
try {
|
|
315
369
|
let jsonMessage = Buffer.isBuffer(message)
|
|
316
370
|
? Buffer.from(message).toString()
|
|
317
371
|
: message;
|
|
318
|
-
try {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
372
|
+
try { jsonMessage = JSON.parse(jsonMessage); } catch (e) { jsonMessage = {}; }
|
|
373
|
+
// ACK tracking: detect send acknowledgements with latency hint if present
|
|
374
|
+
if (jsonMessage?.message_ack) {
|
|
375
|
+
const ack = jsonMessage.message_ack;
|
|
376
|
+
const mid = ack.message_id || ack.mid;
|
|
377
|
+
if(mid && ctx._pendingOutbound && ctx._pendingOutbound.has(mid)){
|
|
378
|
+
const started = ctx._pendingOutbound.get(mid);
|
|
379
|
+
ctx._pendingOutbound.delete(mid);
|
|
380
|
+
const latency = Date.now() - started;
|
|
381
|
+
ctx.health.onAck(latency);
|
|
382
|
+
} else {
|
|
383
|
+
ctx.health.onAck();
|
|
384
|
+
}
|
|
385
|
+
// If this ACK corresponds to an edit, clear from pendingEdits
|
|
386
|
+
if(mid && ctx.pendingEdits && ctx.pendingEdits.has(mid)){
|
|
387
|
+
ctx.pendingEdits.delete(mid);
|
|
388
|
+
if(ctx.health) ctx.health.removePendingEdit(mid);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (jsonMessage?.type === 'ack') { ctx.health.onAck(); }
|
|
392
|
+
// lightweight ack detection heuristic
|
|
393
|
+
if (jsonMessage?.type === 'ack' || jsonMessage?.message_ack) {
|
|
394
|
+
ctx.health.onAck();
|
|
322
395
|
}
|
|
323
396
|
if (jsonMessage.type === "jewel_requests_add") {
|
|
324
397
|
globalCallback(null, {
|
|
@@ -333,53 +406,15 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
333
406
|
timestamp: Date.now().toString(),
|
|
334
407
|
});
|
|
335
408
|
} else if (topic === "/t_ms") {
|
|
336
|
-
if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
if (jsonMessage.lastIssuedSeqId) {
|
|
344
|
-
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
|
345
|
-
}
|
|
346
|
-
for (const i in jsonMessage.deltas) {
|
|
347
|
-
const delta = jsonMessage.deltas[i];
|
|
348
|
-
parseDelta(defaultFuncs, api, ctx, globalCallback, {
|
|
349
|
-
delta: delta,
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
} else if (
|
|
353
|
-
topic === "/thread_typing" ||
|
|
354
|
-
topic === "/orca_typing_notifications"
|
|
355
|
-
) {
|
|
356
|
-
const typ = {
|
|
357
|
-
type: "typ",
|
|
358
|
-
isTyping: !!jsonMessage.state,
|
|
359
|
-
from: jsonMessage.sender_fbid.toString(),
|
|
360
|
-
threadID: utils.formatID(
|
|
361
|
-
(jsonMessage.thread || jsonMessage.sender_fbid).toString()
|
|
362
|
-
),
|
|
363
|
-
};
|
|
364
|
-
(function () {
|
|
365
|
-
globalCallback(null, typ);
|
|
366
|
-
})();
|
|
409
|
+
if (ctx.tmsWait && typeof ctx.tmsWait == "function") { ctx.tmsWait(); }
|
|
410
|
+
if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) { ctx.lastSeqId = jsonMessage.firstDeltaSeqId; ctx.syncToken = jsonMessage.syncToken; }
|
|
411
|
+
if (jsonMessage.lastIssuedSeqId) { ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId); }
|
|
412
|
+
for (const i in jsonMessage.deltas) { const delta = jsonMessage.deltas[i]; parseDelta(defaultFuncs, api, ctx, globalCallback, { delta: delta, }); }
|
|
413
|
+
} else if ( topic === "/thread_typing" || topic === "/orca_typing_notifications" ) {
|
|
414
|
+
const typ = { type: "typ", isTyping: !!jsonMessage.state, from: jsonMessage.sender_fbid.toString(), threadID: utils.formatID( (jsonMessage.thread || jsonMessage.sender_fbid).toString() ), };
|
|
415
|
+
(function () { globalCallback(null, typ); })();
|
|
367
416
|
} else if (topic === "/orca_presence") {
|
|
368
|
-
if (!ctx.globalOptions.updatePresence) {
|
|
369
|
-
for (const i in jsonMessage.list) {
|
|
370
|
-
const data = jsonMessage.list[i];
|
|
371
|
-
const userID = data["u"];
|
|
372
|
-
const presence = {
|
|
373
|
-
type: "presence",
|
|
374
|
-
userID: userID.toString(),
|
|
375
|
-
timestamp: data["l"] * 1000,
|
|
376
|
-
statuses: data["p"],
|
|
377
|
-
};
|
|
378
|
-
(function () {
|
|
379
|
-
globalCallback(null, presence);
|
|
380
|
-
})();
|
|
381
|
-
}
|
|
382
|
-
}
|
|
417
|
+
if (!ctx.globalOptions.updatePresence) { for (const i in jsonMessage.list) { const data = jsonMessage.list[i]; const userID = data["u"]; const presence = { type: "presence", userID: userID.toString(), timestamp: data["l"] * 1000, statuses: data["p"], }; (function () { globalCallback(null, presence); })(); } }
|
|
383
418
|
} else if (topic == "/ls_resp") {
|
|
384
419
|
const parsedPayload = JSON.parse(jsonMessage.payload);
|
|
385
420
|
const reqID = jsonMessage.request_id;
|
|
@@ -387,25 +422,35 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
387
422
|
const taskData = ctx["tasks"].get(reqID);
|
|
388
423
|
const { type: taskType, callback: taskCallback } = taskData;
|
|
389
424
|
const taskRespData = getTaskResponseData(taskType, parsedPayload);
|
|
390
|
-
if (taskRespData == null) {
|
|
391
|
-
taskCallback("error", null);
|
|
392
|
-
} else {
|
|
393
|
-
taskCallback(null, {
|
|
394
|
-
type: taskType,
|
|
395
|
-
reqID: reqID,
|
|
396
|
-
...taskRespData,
|
|
397
|
-
});
|
|
398
|
-
}
|
|
425
|
+
if (taskRespData == null) { taskCallback("error", null); } else { taskCallback(null, { type: taskType, reqID: reqID, ...taskRespData, }); }
|
|
399
426
|
}
|
|
400
427
|
}
|
|
401
428
|
} catch (ex) {
|
|
429
|
+
ctx.health.onError('message_parse');
|
|
402
430
|
console.error("Message parsing error:", ex);
|
|
403
431
|
if (ex.stack) console.error(ex.stack);
|
|
404
432
|
return;
|
|
405
433
|
}
|
|
406
434
|
});
|
|
407
|
-
mqttClient.on("close", function () {});
|
|
408
|
-
mqttClient.on("disconnect", () => {});
|
|
435
|
+
mqttClient.on("close", function () { ctx.health.onDisconnect(); if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
|
|
436
|
+
mqttClient.on("disconnect", () => { ctx.health.onDisconnect(); if (ctx.globalSafety) { try { ctx.globalSafety._ensureMqttAlive(); } catch(_) {} } });
|
|
437
|
+
// Synthetic keepalive with randomized cadence (55-75s) to appear human and keep state alive
|
|
438
|
+
if (!ctx._syntheticKeepAliveInterval) {
|
|
439
|
+
ctx._syntheticKeepAliveInterval = setInterval(() => {
|
|
440
|
+
if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
|
|
441
|
+
if (ctx.globalSafety) {
|
|
442
|
+
const idle = Date.now() - ctx.globalSafety._lastEventTs;
|
|
443
|
+
if (idle > 65 * 1000) { ctx.globalSafety.recordEvent(); ctx.health.onSynthetic(); }
|
|
444
|
+
}
|
|
445
|
+
}, 55000 + Math.floor(Math.random()*20000));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function scheduleAdaptiveReconnect(defaultFuncs, api, ctx, globalCallback){
|
|
449
|
+
const state = getBackoffState(ctx);
|
|
450
|
+
const delay = computeNextDelay(state);
|
|
451
|
+
ctx.health.onReconnectScheduled(delay);
|
|
452
|
+
log.warn('listenMqtt', `Reconnecting in ${delay} ms (adaptive backoff)`);
|
|
453
|
+
setTimeout(()=>listenMqtt(defaultFuncs, api, ctx, globalCallback), delay);
|
|
409
454
|
}
|
|
410
455
|
function getTaskResponseData(taskType, payload) {
|
|
411
456
|
try {
|
package/src/sendMessageMqtt.js
CHANGED
|
@@ -205,14 +205,26 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
205
205
|
task.payload = JSON.stringify(task.payload);
|
|
206
206
|
});
|
|
207
207
|
form.payload = JSON.stringify(form.payload);
|
|
208
|
-
console.log(global.jsonStringifyColor(form, null, 2));
|
|
208
|
+
try { console.log(global.jsonStringifyColor(form, null, 2)); } catch (_) { }
|
|
209
|
+
|
|
210
|
+
// Stage 2: outbound tracking for ACK latency
|
|
211
|
+
if (!ctx._pendingOutbound) ctx._pendingOutbound = new Map();
|
|
212
|
+
let midCandidate = null;
|
|
213
|
+
try {
|
|
214
|
+
const parsed = JSON.parse(form.payload);
|
|
215
|
+
const firstTask = parsed.tasks && parsed.tasks[0] && JSON.parse(parsed.tasks[0].payload);
|
|
216
|
+
if (firstTask && firstTask.otid) midCandidate = String(firstTask.otid);
|
|
217
|
+
} catch (_) { }
|
|
218
|
+
if (midCandidate) {
|
|
219
|
+
ctx._pendingOutbound.set(midCandidate, Date.now());
|
|
220
|
+
if (ctx.health) ctx.health.trackOutbound(ctx._pendingOutbound.size);
|
|
221
|
+
}
|
|
209
222
|
|
|
210
223
|
return mqttClient.publish("/ls_req", JSON.stringify(form), function (err, data) {
|
|
211
224
|
if (err) {
|
|
212
|
-
|
|
225
|
+
if (ctx.health) ctx.health.onError('publish_fail');
|
|
213
226
|
callback(err);
|
|
214
227
|
} else {
|
|
215
|
-
console.log('Message published successfully with data: ', data);
|
|
216
228
|
callback(null, data);
|
|
217
229
|
}
|
|
218
230
|
});
|