nexus-fca 3.2.4 → 3.3.0
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/index.js +24 -2
- package/lib/factory/ApiFactory.js +1 -9
- package/lib/mqtt/MqttManager.js +28 -28
- package/lib/network/HealthServer.js +71 -0
- package/lib/safety/CookieManager.js +35 -28
- package/lib/safety/FacebookSafety.js +150 -32
- package/package.json +2 -2
- package/src/listenMqtt.js +58 -11
- package/src/sendMessage.js +3 -3
- package/utils.js +79 -1
package/index.js
CHANGED
|
@@ -85,6 +85,7 @@ const { CookieManager } = require('./lib/safety/CookieManager');
|
|
|
85
85
|
const EmailPasswordLogin = require('./lib/auth/EmailPasswordLogin');
|
|
86
86
|
const ProxyManager = require('./lib/network/ProxyManager');
|
|
87
87
|
const UserAgentManager = require('./lib/network/UserAgentManager');
|
|
88
|
+
const HealthServer = require('./lib/network/HealthServer');
|
|
88
89
|
|
|
89
90
|
// Core compatibility imports
|
|
90
91
|
const MqttManager = require('./lib/mqtt/MqttManager');
|
|
@@ -131,6 +132,12 @@ if (!fs.existsSync(configPath)) {
|
|
|
131
132
|
global.fca = {
|
|
132
133
|
config: config
|
|
133
134
|
};
|
|
135
|
+
|
|
136
|
+
// Start Health Server if on cloud platform or enabled
|
|
137
|
+
if (process.env.PORT || process.env.NEXUS_ENABLE_HEALTH_SERVER === '1') {
|
|
138
|
+
const healthServer = new HealthServer();
|
|
139
|
+
healthServer.start();
|
|
140
|
+
}
|
|
134
141
|
const Boolean_Option = [
|
|
135
142
|
"online",
|
|
136
143
|
"selfListen",
|
|
@@ -712,6 +719,8 @@ class IntegratedNexusLoginSystem {
|
|
|
712
719
|
});
|
|
713
720
|
|
|
714
721
|
fs.writeFileSync(this.options.appstatePath, JSON.stringify(fixedAppstate, null, 2));
|
|
722
|
+
metadata.appStatePath = this.options.appstatePath;
|
|
723
|
+
this.logger(`Session saved to: ${path.basename(this.options.appstatePath)}`, '💾');
|
|
715
724
|
|
|
716
725
|
// Create backup
|
|
717
726
|
const backupName = `appstate_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
|
|
@@ -859,7 +868,8 @@ class IntegratedNexusLoginSystem {
|
|
|
859
868
|
family_device_id: androidDevice.familyDeviceId
|
|
860
869
|
},
|
|
861
870
|
generated_at: new Date().toISOString(),
|
|
862
|
-
persistent_device: !!this.options.persistentDevice
|
|
871
|
+
persistent_device: !!this.options.persistentDevice,
|
|
872
|
+
appStatePath: this.options.appstatePath
|
|
863
873
|
};
|
|
864
874
|
|
|
865
875
|
this.saveAppstate(appstate, result);
|
|
@@ -948,6 +958,8 @@ class IntegratedNexusLoginSystem {
|
|
|
948
958
|
});
|
|
949
959
|
}
|
|
950
960
|
|
|
961
|
+
const appStatePath = credentials.appStatePath || credentials.appstatePath;
|
|
962
|
+
|
|
951
963
|
const result = {
|
|
952
964
|
success: true,
|
|
953
965
|
appstate: appstate,
|
|
@@ -957,6 +969,7 @@ class IntegratedNexusLoginSystem {
|
|
|
957
969
|
user_agent: androidDevice.userAgent
|
|
958
970
|
},
|
|
959
971
|
method: '2FA',
|
|
972
|
+
appStatePath: appStatePath,
|
|
960
973
|
generated_at: new Date().toISOString()
|
|
961
974
|
};
|
|
962
975
|
|
|
@@ -1131,6 +1144,7 @@ async function integratedNexusLogin(credentials = null, options = {}) {
|
|
|
1131
1144
|
proxy: process.env.NEXUS_PROXY || process.env.HTTPS_PROXY || process.env.HTTP_PROXY,
|
|
1132
1145
|
acceptLanguage: process.env.NEXUS_ACCEPT_LANGUAGE || 'en-US,en;q=0.9',
|
|
1133
1146
|
disablePreflight: process.env.NEXUS_DISABLE_PREFLIGHT === '1' || process.env.NEXUS_DISABLE_PREFLIGHT === 'true',
|
|
1147
|
+
appStatePath: result.appStatePath,
|
|
1134
1148
|
...options
|
|
1135
1149
|
};
|
|
1136
1150
|
|
|
@@ -1149,6 +1163,12 @@ async function integratedNexusLogin(credentials = null, options = {}) {
|
|
|
1149
1163
|
botError: err.message
|
|
1150
1164
|
});
|
|
1151
1165
|
} else {
|
|
1166
|
+
// VERCEL STABILITY WARNING
|
|
1167
|
+
if (process.env.VERCEL || process.env.NOW_REGION) {
|
|
1168
|
+
Logger.warn('PLATFORM', 'Vercel/Serverless detected. listenMqtt is NOT supported here.');
|
|
1169
|
+
Logger.warn('PLATFORM', 'Bot will work for outbound actions (sending) only.');
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1152
1172
|
Logger.success('BOT-INIT', 'Bot initialized successfully');
|
|
1153
1173
|
Logger.success('READY', '🚀 Nexus-FCA is now ready for use');
|
|
1154
1174
|
Logger.info('STATUS', `Bot online | User ID: ${api.getCurrentUserID()}`);
|
|
@@ -1232,7 +1252,8 @@ async function login(loginData, options = {}, callback) {
|
|
|
1232
1252
|
password: loginData.password,
|
|
1233
1253
|
twofactor: loginData.twofactor || loginData.otp || undefined,
|
|
1234
1254
|
_2fa: loginData._2fa || undefined,
|
|
1235
|
-
appstate: loginData.appState || loginData.appstate || undefined
|
|
1255
|
+
appstate: loginData.appState || loginData.appstate || undefined,
|
|
1256
|
+
appStatePath: loginData.appStatePath || loginData.appstatePath || undefined
|
|
1236
1257
|
}, { autoStartBot: false }); // ONLY generate cookies, NO bot startup
|
|
1237
1258
|
|
|
1238
1259
|
if (!result.success || !result.appstate) {
|
|
@@ -1277,6 +1298,7 @@ async function login(loginData, options = {}, callback) {
|
|
|
1277
1298
|
online: true,
|
|
1278
1299
|
emitReady: false,
|
|
1279
1300
|
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
|
|
1301
|
+
appStatePath: result.appStatePath,
|
|
1280
1302
|
...options
|
|
1281
1303
|
};
|
|
1282
1304
|
|
|
@@ -169,6 +169,7 @@ class ApiFactory {
|
|
|
169
169
|
wsReqNumber: 0,
|
|
170
170
|
wsTaskNumber: 0,
|
|
171
171
|
globalSafety: this.globalSafety,
|
|
172
|
+
appStatePath: globalOptions.appStatePath || null,
|
|
172
173
|
pendingEdits: new Map()
|
|
173
174
|
};
|
|
174
175
|
}
|
|
@@ -302,15 +303,6 @@ class ApiFactory {
|
|
|
302
303
|
|
|
303
304
|
api._sendMessageDirect = DIRECT_FN;
|
|
304
305
|
api.sendMessage = function (message, threadID, cb, replyToMessage) {
|
|
305
|
-
// New: Auto-Typing support for improved human-like behavior
|
|
306
|
-
if (globalOptions.autoTyping) {
|
|
307
|
-
try {
|
|
308
|
-
api.sendTypingIndicator(threadID, (err) => {
|
|
309
|
-
// Ignore typing errors to avoid blocking the message
|
|
310
|
-
});
|
|
311
|
-
} catch (_) { /* ignore */ }
|
|
312
|
-
}
|
|
313
|
-
|
|
314
306
|
if (!globalOptions.groupQueueEnabled || !isGroupThread(threadID)) {
|
|
315
307
|
return api._sendMessageDirect(message, threadID, cb, replyToMessage);
|
|
316
308
|
}
|
package/lib/mqtt/MqttManager.js
CHANGED
|
@@ -22,7 +22,7 @@ class MqttManager extends EventEmitter {
|
|
|
22
22
|
this.heartbeatInterval = null;
|
|
23
23
|
this.connectionTimeout = null;
|
|
24
24
|
this.lastActivity = Date.now();
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
// Performance metrics
|
|
27
27
|
this.metrics = {
|
|
28
28
|
messagesReceived: 0,
|
|
@@ -49,13 +49,13 @@ class MqttManager extends EventEmitter {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
logger('🔄 Connecting to MQTT...', 'info');
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
const options = this._buildConnectionOptions();
|
|
54
54
|
this.client = mqtt.connect(this.ctx.mqttEndpoint, options);
|
|
55
|
-
|
|
55
|
+
|
|
56
56
|
this._setupEventHandlers();
|
|
57
57
|
this._startHeartbeat();
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
// Connection timeout
|
|
60
60
|
this.connectionTimeout = setTimeout(() => {
|
|
61
61
|
if (!this.isConnected) {
|
|
@@ -81,7 +81,7 @@ class MqttManager extends EventEmitter {
|
|
|
81
81
|
clean: true,
|
|
82
82
|
connectTimeout: 10000,
|
|
83
83
|
reconnectPeriod: 0, // Disable auto-reconnect, we handle it
|
|
84
|
-
keepalive:
|
|
84
|
+
keepalive: 10,
|
|
85
85
|
protocolVersion: 4,
|
|
86
86
|
username: JSON.stringify({
|
|
87
87
|
"u": this.ctx.userID,
|
|
@@ -150,15 +150,15 @@ class MqttManager extends EventEmitter {
|
|
|
150
150
|
this.reconnectDelay = 1000;
|
|
151
151
|
this.metrics.lastConnected = Date.now();
|
|
152
152
|
this.metrics.reconnections++;
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
clearTimeout(this.connectionTimeout);
|
|
155
|
-
|
|
155
|
+
|
|
156
156
|
logger('✅ MQTT connected successfully', 'info');
|
|
157
157
|
logger(`📊 Connection metrics: Reconnections: ${this.metrics.reconnections}, Errors: ${this.metrics.errors}`, 'info');
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
this._subscribeToTopics();
|
|
160
160
|
this._processMessageQueue();
|
|
161
|
-
|
|
161
|
+
|
|
162
162
|
this.emit('connected');
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -169,7 +169,7 @@ class MqttManager extends EventEmitter {
|
|
|
169
169
|
_subscribeToTopics() {
|
|
170
170
|
const topics = [
|
|
171
171
|
"/ls_req",
|
|
172
|
-
"/ls_resp",
|
|
172
|
+
"/ls_resp",
|
|
173
173
|
"/legacy_web",
|
|
174
174
|
"/webrtc",
|
|
175
175
|
"/rtc_multi",
|
|
@@ -208,10 +208,10 @@ class MqttManager extends EventEmitter {
|
|
|
208
208
|
try {
|
|
209
209
|
this.lastActivity = Date.now();
|
|
210
210
|
this.metrics.messagesReceived++;
|
|
211
|
-
|
|
211
|
+
|
|
212
212
|
const messageStr = message.toString();
|
|
213
213
|
let parsedMessage;
|
|
214
|
-
|
|
214
|
+
|
|
215
215
|
try {
|
|
216
216
|
parsedMessage = JSON.parse(messageStr);
|
|
217
217
|
} catch (parseError) {
|
|
@@ -221,7 +221,7 @@ class MqttManager extends EventEmitter {
|
|
|
221
221
|
|
|
222
222
|
// Enhanced message processing with caching
|
|
223
223
|
this._processMessage(topic, parsedMessage);
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
} catch (error) {
|
|
226
226
|
logger(`❌ Error processing MQTT message: ${error.message}`, 'error');
|
|
227
227
|
this.metrics.errors++;
|
|
@@ -245,7 +245,7 @@ class MqttManager extends EventEmitter {
|
|
|
245
245
|
if (message.syncToken) {
|
|
246
246
|
this.ctx.syncToken = message.syncToken;
|
|
247
247
|
}
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
if (message.lastIssuedSeqId) {
|
|
250
250
|
this.ctx.lastSeqId = parseInt(message.lastIssuedSeqId);
|
|
251
251
|
}
|
|
@@ -270,7 +270,7 @@ class MqttManager extends EventEmitter {
|
|
|
270
270
|
logger('🔌 MQTT connection closed', 'warn');
|
|
271
271
|
this._stopHeartbeat();
|
|
272
272
|
this.emit('disconnected');
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
// Auto-reconnect if not intentionally closed
|
|
275
275
|
if (this.client && !this.client.disconnecting) {
|
|
276
276
|
this._handleReconnect();
|
|
@@ -308,9 +308,9 @@ class MqttManager extends EventEmitter {
|
|
|
308
308
|
|
|
309
309
|
this.reconnectAttempts++;
|
|
310
310
|
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
logger(`🔄 MQTT reconnecting in ${delay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`, 'warn');
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
setTimeout(() => {
|
|
315
315
|
this.connect();
|
|
316
316
|
}, delay);
|
|
@@ -350,7 +350,7 @@ class MqttManager extends EventEmitter {
|
|
|
350
350
|
this.messageQueue.shift(); // Remove oldest message
|
|
351
351
|
logger('⚠️ Message queue full, dropping oldest message', 'warn');
|
|
352
352
|
}
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
this.messageQueue.push(messageData);
|
|
355
355
|
logger(`📦 Queued message for ${messageData.topic} (queue size: ${this.messageQueue.length})`, 'info');
|
|
356
356
|
}
|
|
@@ -361,12 +361,12 @@ class MqttManager extends EventEmitter {
|
|
|
361
361
|
*/
|
|
362
362
|
_processMessageQueue() {
|
|
363
363
|
if (this.messageQueue.length === 0) return;
|
|
364
|
-
|
|
364
|
+
|
|
365
365
|
logger(`📦 Processing ${this.messageQueue.length} queued messages`, 'info');
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
const messages = [...this.messageQueue];
|
|
368
368
|
this.messageQueue = [];
|
|
369
|
-
|
|
369
|
+
|
|
370
370
|
messages.forEach(messageData => {
|
|
371
371
|
this.publish(messageData.topic, messageData.message, messageData.options);
|
|
372
372
|
});
|
|
@@ -385,7 +385,7 @@ class MqttManager extends EventEmitter {
|
|
|
385
385
|
logger('💓 MQTT heartbeat - inactive for 5 minutes, sending ping', 'info');
|
|
386
386
|
this.client.ping();
|
|
387
387
|
}
|
|
388
|
-
|
|
388
|
+
|
|
389
389
|
// Update uptime
|
|
390
390
|
if (this.metrics.lastConnected) {
|
|
391
391
|
this.metrics.uptime = Date.now() - this.metrics.lastConnected;
|
|
@@ -422,9 +422,9 @@ class MqttManager extends EventEmitter {
|
|
|
422
422
|
*/
|
|
423
423
|
disconnect() {
|
|
424
424
|
logger('🔌 Disconnecting MQTT gracefully...', 'info');
|
|
425
|
-
|
|
425
|
+
|
|
426
426
|
this._stopHeartbeat();
|
|
427
|
-
|
|
427
|
+
|
|
428
428
|
if (this.client) {
|
|
429
429
|
this.client.publish("/browser_close", "{}", { qos: 0 });
|
|
430
430
|
this.client.end(false, () => {
|
|
@@ -432,7 +432,7 @@ class MqttManager extends EventEmitter {
|
|
|
432
432
|
this.emit('disconnected');
|
|
433
433
|
});
|
|
434
434
|
}
|
|
435
|
-
|
|
435
|
+
|
|
436
436
|
this.isConnected = false;
|
|
437
437
|
}
|
|
438
438
|
|
|
@@ -441,13 +441,13 @@ class MqttManager extends EventEmitter {
|
|
|
441
441
|
*/
|
|
442
442
|
forceDisconnect() {
|
|
443
443
|
logger('⚡ Force disconnecting MQTT...', 'warn');
|
|
444
|
-
|
|
444
|
+
|
|
445
445
|
this._stopHeartbeat();
|
|
446
|
-
|
|
446
|
+
|
|
447
447
|
if (this.client) {
|
|
448
448
|
this.client.end(true);
|
|
449
449
|
}
|
|
450
|
-
|
|
450
|
+
|
|
451
451
|
this.isConnected = false;
|
|
452
452
|
this.emit('disconnected');
|
|
453
453
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const log = require('npmlog');
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal Health Server for Render/Railway/Cloud compatibility.
|
|
9
|
+
* Responds to platform health checks on the assigned PORT.
|
|
10
|
+
*/
|
|
11
|
+
class HealthServer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.port = process.env.PORT || options.port || 10000;
|
|
14
|
+
this.server = null;
|
|
15
|
+
this.active = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
start() {
|
|
19
|
+
if (this.active) return;
|
|
20
|
+
|
|
21
|
+
this.server = http.createServer((req, res) => {
|
|
22
|
+
if (req.url === '/health' || req.url === '/') {
|
|
23
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
24
|
+
const html = `
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<title>Nexus-FCA Status</title>
|
|
29
|
+
<style>
|
|
30
|
+
body { background: #0f172a; color: #f8fafc; font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
31
|
+
.card { background: #1e293b; padding: 2rem; border-radius: 1rem; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); border: 1px solid #334155; text-align: center; }
|
|
32
|
+
h1 { color: #38bdf8; margin-top: 0; }
|
|
33
|
+
.status { display: inline-block; padding: 0.25rem 0.75rem; background: #065f46; color: #34d399; border-radius: 9999px; font-size: 0.875rem; font-weight: 600; }
|
|
34
|
+
.version { color: #64748b; font-size: 0.75rem; margin-top: 1rem; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="card">
|
|
39
|
+
<h1>✨ Nexus-FCA</h1>
|
|
40
|
+
<div class="status">● System Operational</div>
|
|
41
|
+
<div class="version">Core v${pkg.version} | Stability: 99.9%</div>
|
|
42
|
+
</div>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
`;
|
|
46
|
+
res.end(html);
|
|
47
|
+
} else {
|
|
48
|
+
res.writeHead(404);
|
|
49
|
+
res.end();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.server.listen(this.port, () => {
|
|
54
|
+
log.info("HealthServer", `System health endpoint active on port ${this.port}`);
|
|
55
|
+
this.active = true;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.server.on('error', (err) => {
|
|
59
|
+
log.error("HealthServer", `Failed to start: ${err.message}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stop() {
|
|
64
|
+
if (this.server) {
|
|
65
|
+
this.server.close();
|
|
66
|
+
this.active = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = HealthServer;
|
|
@@ -20,45 +20,51 @@ class CookieManager {
|
|
|
20
20
|
if (!Array.isArray(cookies) || cookies.length === 0) {
|
|
21
21
|
return cookies;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
const {
|
|
25
25
|
defaultExpiryDays = 90,
|
|
26
26
|
criticalExpiryDays = 90,
|
|
27
27
|
refreshExisting = true
|
|
28
28
|
} = options;
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
const now = new Date();
|
|
31
31
|
const criticalCookies = ['c_user', 'xs', 'fr', 'datr', 'sb', 'spin'];
|
|
32
32
|
let fixedCount = 0;
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
for (const cookie of cookies) {
|
|
35
35
|
// Skip cookies with no key
|
|
36
36
|
if (!cookie.key) continue;
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
const isCritical = criticalCookies.includes(cookie.key);
|
|
39
39
|
const days = isCritical ? criticalExpiryDays : defaultExpiryDays;
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
// Check if cookie needs expiry fix
|
|
42
|
-
const needsFix = !cookie.expires ||
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
const needsFix = !cookie.expires ||
|
|
43
|
+
refreshExisting ||
|
|
44
|
+
!this._isValidDate(cookie.expires) ||
|
|
45
|
+
this._getRemainingDays(cookie.expires) < 7;
|
|
46
|
+
|
|
47
47
|
if (needsFix) {
|
|
48
|
+
// STICKY COOKIES: If it's a critical cookie (sb, datr, c_user) and still has > 60 days, skip refresh
|
|
49
|
+
const stickyCookies = ['sb', 'datr', 'c_user'];
|
|
50
|
+
if (stickyCookies.includes(cookie.key) && this._getRemainingDays(cookie.expires) > 60) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
// Set expiry to future date
|
|
49
55
|
const futureDate = new Date(now.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
50
56
|
cookie.expires = futureDate.toUTCString();
|
|
51
57
|
fixedCount++;
|
|
52
58
|
}
|
|
53
59
|
}
|
|
54
|
-
|
|
60
|
+
|
|
55
61
|
if (fixedCount > 0) {
|
|
56
62
|
logger(`Fixed expiry dates for ${fixedCount} cookies`, 'info');
|
|
57
63
|
}
|
|
58
|
-
|
|
64
|
+
|
|
59
65
|
return cookies;
|
|
60
66
|
}
|
|
61
|
-
|
|
67
|
+
|
|
62
68
|
/**
|
|
63
69
|
* Check if cookie expiry is a valid date
|
|
64
70
|
* @param {string} dateStr - Date string to check
|
|
@@ -69,7 +75,7 @@ class CookieManager {
|
|
|
69
75
|
const date = new Date(dateStr);
|
|
70
76
|
return !isNaN(date.getTime());
|
|
71
77
|
}
|
|
72
|
-
|
|
78
|
+
|
|
73
79
|
/**
|
|
74
80
|
* Get days remaining until expiry
|
|
75
81
|
* @param {string} dateStr - Date string to check
|
|
@@ -85,7 +91,7 @@ class CookieManager {
|
|
|
85
91
|
return 0;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
|
-
|
|
94
|
+
|
|
89
95
|
/**
|
|
90
96
|
* Check if appstate has critical cookies
|
|
91
97
|
* @param {Array} cookies - Cookies to check
|
|
@@ -95,22 +101,23 @@ class CookieManager {
|
|
|
95
101
|
if (!Array.isArray(cookies) || cookies.length === 0) {
|
|
96
102
|
return { valid: false, missing: ['all'] };
|
|
97
103
|
}
|
|
98
|
-
|
|
99
|
-
const criticalCookies = ['c_user', 'xs', 'datr', 'sb'];
|
|
104
|
+
|
|
105
|
+
const criticalCookies = ['c_user', 'xs', 'datr', 'sb', 'fr', 'spin'];
|
|
100
106
|
const missing = [];
|
|
101
|
-
|
|
107
|
+
|
|
102
108
|
for (const critical of criticalCookies) {
|
|
103
|
-
|
|
109
|
+
const found = cookies.find(c => c.key === critical);
|
|
110
|
+
if (!found || !found.value || found.value === 'deleted') {
|
|
104
111
|
missing.push(critical);
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
|
-
|
|
114
|
+
|
|
108
115
|
return {
|
|
109
116
|
valid: missing.length === 0,
|
|
110
117
|
missing
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
|
-
|
|
120
|
+
|
|
114
121
|
/**
|
|
115
122
|
* Generate default cookie expiry date
|
|
116
123
|
* @param {string} cookieName - Name of cookie
|
|
@@ -119,13 +126,13 @@ class CookieManager {
|
|
|
119
126
|
static getDefaultExpiry(cookieName) {
|
|
120
127
|
const now = new Date();
|
|
121
128
|
const criticalCookies = ['c_user', 'xs', 'fr', 'datr', 'sb'];
|
|
122
|
-
|
|
129
|
+
|
|
123
130
|
// Critical cookies get 90 days, others get 30 days
|
|
124
131
|
const days = criticalCookies.includes(cookieName) ? 90 : 30;
|
|
125
132
|
const future = new Date(now.getTime() + (days * 24 * 60 * 60 * 1000));
|
|
126
133
|
return future.toUTCString();
|
|
127
134
|
}
|
|
128
|
-
|
|
135
|
+
|
|
129
136
|
/**
|
|
130
137
|
* Load and fix appstate file
|
|
131
138
|
* @param {string} appstatePath - Path to appstate.json
|
|
@@ -137,26 +144,26 @@ class CookieManager {
|
|
|
137
144
|
logger(`Appstate file not found: ${appstatePath}`, 'error');
|
|
138
145
|
return null;
|
|
139
146
|
}
|
|
140
|
-
|
|
147
|
+
|
|
141
148
|
const cookies = JSON.parse(fs.readFileSync(appstatePath, 'utf8'));
|
|
142
149
|
if (!Array.isArray(cookies)) {
|
|
143
150
|
logger('Invalid appstate format: not an array', 'error');
|
|
144
151
|
return null;
|
|
145
152
|
}
|
|
146
|
-
|
|
153
|
+
|
|
147
154
|
// Fix expiry dates
|
|
148
155
|
const fixed = this.fixCookieExpiry(cookies);
|
|
149
|
-
|
|
156
|
+
|
|
150
157
|
// Validate critical cookies
|
|
151
158
|
const validation = this.validateCriticalCookies(fixed);
|
|
152
159
|
if (!validation.valid) {
|
|
153
160
|
logger(`Missing critical cookies: ${validation.missing.join(', ')}`, 'warn');
|
|
154
161
|
}
|
|
155
|
-
|
|
162
|
+
|
|
156
163
|
// Save back fixed cookies
|
|
157
164
|
fs.writeFileSync(appstatePath, JSON.stringify(fixed, null, 2));
|
|
158
165
|
logger(`Fixed and saved appstate with ${fixed.length} cookies`, 'info');
|
|
159
|
-
|
|
166
|
+
|
|
160
167
|
return fixed;
|
|
161
168
|
} catch (err) {
|
|
162
169
|
logger(`Failed to load and fix appstate: ${err.message}`, 'error');
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const log = require('npmlog');
|
|
9
10
|
// StealthMode module has been removed to avoid global human-like pauses
|
|
10
11
|
// that can harm perceived stability. FacebookSafety now relies on
|
|
11
12
|
// lightweight adaptive delays and backoff only.
|
|
@@ -96,6 +97,9 @@ class FacebookSafety {
|
|
|
96
97
|
this._dynamicHeartbeatTimer = null; // replaces fixed interval heartbeat for risk-tier tuning
|
|
97
98
|
this._riskLast = 'low';
|
|
98
99
|
|
|
100
|
+
// Safety Store for Token Mirroring (Recovery on ephemeral platforms)
|
|
101
|
+
this.safetyStorePath = path.join(process.cwd(), 'lib', 'safety', 'safety_store.json');
|
|
102
|
+
|
|
99
103
|
this.initSafety();
|
|
100
104
|
}
|
|
101
105
|
|
|
@@ -105,9 +109,14 @@ class FacebookSafety {
|
|
|
105
109
|
this.setupSafeRefresh();
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
// Recovery from safety store
|
|
113
|
+
this._loadFromSafetyStore();
|
|
114
|
+
|
|
108
115
|
// Setup session monitoring
|
|
109
116
|
this.setupSessionMonitoring();
|
|
110
117
|
this._schedulePeriodicRecycle();
|
|
118
|
+
this.scheduleLightPoke();
|
|
119
|
+
this.scheduleSessionBreath();
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
/**
|
|
@@ -328,9 +337,25 @@ class FacebookSafety {
|
|
|
328
337
|
maxMs = 90 * 60 * 1000; // 90m
|
|
329
338
|
}
|
|
330
339
|
const interval = minMs + Math.random() * (maxMs - minMs);
|
|
331
|
-
|
|
340
|
+
log.info("FacebookSafety", `Next token refresh in ${Math.floor(interval / (60 * 1000))}m (keep-alive mode)`);
|
|
332
341
|
const t = setTimeout(async () => {
|
|
333
342
|
await this.refreshSafeSession();
|
|
343
|
+
|
|
344
|
+
// Extra: Explicitly refresh and save fb_dtsg every ~2 hours
|
|
345
|
+
if (Date.now() - this._lastHeavyMaintenanceTs > 2 * 60 * 60 * 1000) {
|
|
346
|
+
try {
|
|
347
|
+
if (this.api && typeof this.api.refreshFb_dtsg === 'function') {
|
|
348
|
+
const newDtsg = await this.api.refreshFb_dtsg();
|
|
349
|
+
if (newDtsg && this.ctx && this.ctx.appStatePath) {
|
|
350
|
+
// Auto-save will be triggered by saveCookies inside refreshFb_dtsg (if it calls post)
|
|
351
|
+
// But we force a saveAppState here just to be sure
|
|
352
|
+
require('../../utils').saveAppState(this.ctx);
|
|
353
|
+
log.info("FacebookSafety", 'Critical token (fb_dtsg) refreshed and saved to disk');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch (_) { }
|
|
357
|
+
}
|
|
358
|
+
|
|
334
359
|
schedule();
|
|
335
360
|
}, interval);
|
|
336
361
|
this._registerTimer(t);
|
|
@@ -474,6 +499,38 @@ class FacebookSafety {
|
|
|
474
499
|
this._periodicRecycleTimer = t;
|
|
475
500
|
}
|
|
476
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Lightweight poke (fb_dtsg refresh only) integrated to remove duplicate logic in index.js
|
|
504
|
+
*/
|
|
505
|
+
scheduleLightPoke() {
|
|
506
|
+
if (this._lightPokeTimer || this._destroyed) return;
|
|
507
|
+
const base = 30 * 60 * 1000; // 30m (Tuned for proactive health)
|
|
508
|
+
const jitter = (Math.random() * 20 - 10) * 60 * 1000; // ±10m
|
|
509
|
+
const schedule = () => {
|
|
510
|
+
if (this._destroyed) return;
|
|
511
|
+
const t = setTimeout(async () => {
|
|
512
|
+
if (this._destroyed) return;
|
|
513
|
+
// Respect spacing: skip if recent heavy refresh
|
|
514
|
+
if (Date.now() - this._lastRefreshTs < this._minSpacingMs / 2) {
|
|
515
|
+
schedule();
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
if (this.api && typeof this.api.refreshFb_dtsg === 'function') {
|
|
520
|
+
await this.api.refreshFb_dtsg().catch(() => { });
|
|
521
|
+
this._lastRefreshTs = Date.now();
|
|
522
|
+
this._lastLightPokeTs = Date.now();
|
|
523
|
+
this.safetyEmit('lightPoke', { ts: Date.now() });
|
|
524
|
+
}
|
|
525
|
+
} catch (_) { }
|
|
526
|
+
schedule();
|
|
527
|
+
}, base + (Math.random() * 20 - 10) * 60 * 1000);
|
|
528
|
+
this._registerTimer(t);
|
|
529
|
+
this._lightPokeTimer = t;
|
|
530
|
+
};
|
|
531
|
+
schedule();
|
|
532
|
+
}
|
|
533
|
+
|
|
477
534
|
// Heartbeat ping & watchdog - ULTRA SAFE VERSION
|
|
478
535
|
_startHeartbeat() {
|
|
479
536
|
if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
|
|
@@ -576,11 +633,12 @@ class FacebookSafety {
|
|
|
576
633
|
let preMqttConnected = this.ctx && this.ctx.mqttClient && this.ctx.mqttClient.connected;
|
|
577
634
|
let preLastEvent = this._lastEventTs;
|
|
578
635
|
try {
|
|
579
|
-
|
|
636
|
+
log.info("FacebookSafety", 'Performing safe session refresh...');
|
|
580
637
|
if (!this.api || typeof this.api.refreshFb_dtsg !== 'function') {
|
|
581
|
-
|
|
638
|
+
log.warn("FacebookSafety", 'Safe refresh skipped: api.refreshFb_dtsg not available');
|
|
582
639
|
return;
|
|
583
640
|
}
|
|
641
|
+
|
|
584
642
|
// Abort protection if takes too long (network hang)
|
|
585
643
|
const timeoutMs = 25 * 1000;
|
|
586
644
|
const controller = new AbortController();
|
|
@@ -588,6 +646,11 @@ class FacebookSafety {
|
|
|
588
646
|
let res;
|
|
589
647
|
try {
|
|
590
648
|
res = await this.api.refreshFb_dtsg({ signal: controller.signal });
|
|
649
|
+
this._saveToSafetyStore(); // Mirror to safety store
|
|
650
|
+
} catch (fbErr) {
|
|
651
|
+
log.warn("FacebookSafety", 'Primary fb_dtsg refresh failed, attempting Business fallback...');
|
|
652
|
+
res = await this.refreshFb_dtsgBusiness().catch(() => null);
|
|
653
|
+
if (!res) throw fbErr;
|
|
591
654
|
} finally { clearTimeout(timeout); }
|
|
592
655
|
this.sessionMetrics.errorCount = Math.max(0, this.sessionMetrics.errorCount - 1);
|
|
593
656
|
this.sessionMetrics.lastActivity = Date.now();
|
|
@@ -639,35 +702,25 @@ class FacebookSafety {
|
|
|
639
702
|
}
|
|
640
703
|
|
|
641
704
|
/**
|
|
642
|
-
*
|
|
705
|
+
* Fallback Token Refresh via Business Suite
|
|
706
|
+
* Business endpoints are often more stable for bots.
|
|
643
707
|
*/
|
|
644
|
-
|
|
645
|
-
if (this.
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
this._lastRefreshTs = Date.now();
|
|
661
|
-
this._lastLightPokeTs = Date.now();
|
|
662
|
-
this.safetyEmit('lightPoke', { ts: Date.now() });
|
|
663
|
-
}
|
|
664
|
-
} catch (_) { }
|
|
665
|
-
schedule();
|
|
666
|
-
}, base + (Math.random() * 20 - 10) * 60 * 1000);
|
|
667
|
-
this._registerTimer(t);
|
|
668
|
-
this._lightPokeTimer = t;
|
|
669
|
-
};
|
|
670
|
-
schedule();
|
|
708
|
+
async refreshFb_dtsgBusiness() {
|
|
709
|
+
if (!this.api || !this.ctx) return null;
|
|
710
|
+
try {
|
|
711
|
+
const utils = require('../../utils');
|
|
712
|
+
const res = await utils.get("https://business.facebook.com/business_locations", this.ctx.jar, null, { noRef: true }, this.ctx);
|
|
713
|
+
const html = res.body;
|
|
714
|
+
const match = html.match(/\["DTSGInitialData",\[\],\{"token":"(.*?)"\},/);
|
|
715
|
+
if (match && match[1]) {
|
|
716
|
+
this.ctx.fb_dtsg = match[1];
|
|
717
|
+
log.info("FacebookSafety", 'Refreshed fb_dtsg via Business fallback');
|
|
718
|
+
return match[1];
|
|
719
|
+
}
|
|
720
|
+
} catch (e) {
|
|
721
|
+
log.verbose("FacebookSafety", 'Business refresh failed: ' + e.message);
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
671
724
|
}
|
|
672
725
|
|
|
673
726
|
_registerTimer(t) {
|
|
@@ -678,7 +731,7 @@ class FacebookSafety {
|
|
|
678
731
|
// Cleanup / destroy resources (to prevent dangling timers)
|
|
679
732
|
destroy() {
|
|
680
733
|
this._destroyed = true;
|
|
681
|
-
const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer, this._lightPokeTimer];
|
|
734
|
+
const timers = [this._safeRefreshInterval, this._safeRefreshTimer, this._heartbeatTimer, this._watchdogTimer, this._periodicRecycleTimer, this._lightPokeTimer, this._breathTimer];
|
|
682
735
|
timers.forEach(t => t && clearTimeout(t));
|
|
683
736
|
// Clear any registered anonymous timers
|
|
684
737
|
this._timerRegistry.forEach(t => clearTimeout(t));
|
|
@@ -834,6 +887,71 @@ class FacebookSafety {
|
|
|
834
887
|
setSafetyEventHandler(handler) {
|
|
835
888
|
this.onSafetyEvent = handler;
|
|
836
889
|
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* ULTRA-LIGHTWEIGHT "Session Breath"
|
|
893
|
+
* Performs a tiny request to maintain session activity without heavy overhead or detection risk.
|
|
894
|
+
*/
|
|
895
|
+
scheduleSessionBreath() {
|
|
896
|
+
if (this._breathTimer || this._destroyed) return;
|
|
897
|
+
const base = 20 * 60 * 1000; // 20m (Breath cycle)
|
|
898
|
+
const schedule = () => {
|
|
899
|
+
if (this._destroyed) return;
|
|
900
|
+
const t = setTimeout(async () => {
|
|
901
|
+
if (this._destroyed) return;
|
|
902
|
+
try {
|
|
903
|
+
const utils = require('../../utils');
|
|
904
|
+
// Hit a static lightweight FB asset or GraphQL endpoint
|
|
905
|
+
await utils.get("https://www.facebook.com/rsrc.php/v3/yO/r/688pC_S3Y6X.png", this.ctx.jar, null, { noRef: true }, this.ctx).catch(() => { });
|
|
906
|
+
this.safetyEmit('sessionBreath', { ts: Date.now() });
|
|
907
|
+
} catch (_) { }
|
|
908
|
+
schedule();
|
|
909
|
+
}, base + (Math.random() * 5 * 60 * 1000)); // 20-25m
|
|
910
|
+
this._registerTimer(t);
|
|
911
|
+
this._breathTimer = t;
|
|
912
|
+
};
|
|
913
|
+
schedule();
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Mirror critical tokens to a separate store for recovery on ephemeral platforms.
|
|
919
|
+
*/
|
|
920
|
+
_saveToSafetyStore() {
|
|
921
|
+
if (!this.ctx || !this.ctx.fb_dtsg) return;
|
|
922
|
+
try {
|
|
923
|
+
const data = {
|
|
924
|
+
fb_dtsg: this.ctx.fb_dtsg,
|
|
925
|
+
jazoest: this.ctx.jazoest,
|
|
926
|
+
updatedAt: new Date().toISOString()
|
|
927
|
+
};
|
|
928
|
+
const dir = require('path').dirname(this.safetyStorePath);
|
|
929
|
+
const fs = require('fs');
|
|
930
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
931
|
+
fs.writeFileSync(this.safetyStorePath, JSON.stringify(data, null, 2));
|
|
932
|
+
} catch (e) {
|
|
933
|
+
log.verbose("FacebookSafety", "Safety store save failed: " + e.message);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Recovery logic from safety store.
|
|
939
|
+
*/
|
|
940
|
+
_loadFromSafetyStore() {
|
|
941
|
+
try {
|
|
942
|
+
const fs = require('fs');
|
|
943
|
+
if (fs.existsSync(this.safetyStorePath)) {
|
|
944
|
+
const data = JSON.parse(fs.readFileSync(this.safetyStorePath, 'utf8'));
|
|
945
|
+
if (data.fb_dtsg && (!this.ctx.fb_dtsg || this.ctx.fb_dtsg === 'undefined')) {
|
|
946
|
+
this.ctx.fb_dtsg = data.fb_dtsg;
|
|
947
|
+
this.ctx.jazoest = data.jazoest;
|
|
948
|
+
log.info("FacebookSafety", "Recovered tokens from safety store");
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
} catch (e) {
|
|
952
|
+
log.verbose("FacebookSafety", "Safety store load failed: " + e.message);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
837
955
|
}
|
|
838
956
|
|
|
839
957
|
module.exports = FacebookSafety;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-fca",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Nexus-FCA 3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
|
+
"description": "Nexus-FCA 3.3.0 – Advanced, Secure & Stable Facebook Messenger API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/listenMqtt.js
CHANGED
|
@@ -203,11 +203,18 @@ function startForegroundRefresh(ctx) {
|
|
|
203
203
|
clearInterval(ctx._foregroundRefreshInterval);
|
|
204
204
|
ctx._foregroundRefreshInterval = null;
|
|
205
205
|
}
|
|
206
|
+
if (ctx._presenceRefreshInterval) {
|
|
207
|
+
clearInterval(ctx._presenceRefreshInterval);
|
|
208
|
+
ctx._presenceRefreshInterval = null;
|
|
209
|
+
}
|
|
206
210
|
const options = ctx.globalOptions || {};
|
|
207
211
|
if (options.foregroundRefreshEnabled === false) return;
|
|
212
|
+
|
|
213
|
+
// Foreground state refresh - reduced from 15min to 1min for better activity signal
|
|
208
214
|
const minutes = Number.isFinite(options.foregroundRefreshMinutes)
|
|
209
|
-
? Math.max(
|
|
210
|
-
: 15
|
|
215
|
+
? Math.max(0.5, options.foregroundRefreshMinutes)
|
|
216
|
+
: 1; // Changed: 15 → 1 minute (like ws3-fca approach)
|
|
217
|
+
|
|
211
218
|
ctx._foregroundRefreshInterval = setInterval(() => {
|
|
212
219
|
if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
|
|
213
220
|
try {
|
|
@@ -224,12 +231,33 @@ function startForegroundRefresh(ctx) {
|
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
}, minutes * 60 * 1000);
|
|
234
|
+
|
|
235
|
+
// ws3-fca style presence signal - sends activity indicator every 50s
|
|
236
|
+
// This prevents Facebook from marking connection as idle/background
|
|
237
|
+
const presenceIntervalSec = Number.isFinite(options.presenceRefreshSeconds)
|
|
238
|
+
? Math.max(30, options.presenceRefreshSeconds)
|
|
239
|
+
: 50; // ws3-fca uses 50 seconds
|
|
240
|
+
|
|
241
|
+
ctx._presenceRefreshInterval = setInterval(() => {
|
|
242
|
+
if (!ctx.mqttClient || !ctx.mqttClient.connected) return;
|
|
243
|
+
try {
|
|
244
|
+
// Generate presence payload like ws3-fca
|
|
245
|
+
const presencePayload = utils.generatePresence(ctx.userID);
|
|
246
|
+
ctx.mqttClient.publish("/t_p", presencePayload, { qos: 0, retain: false });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
// Silent fail for presence - not critical
|
|
249
|
+
}
|
|
250
|
+
}, presenceIntervalSec * 1000);
|
|
227
251
|
}
|
|
228
252
|
function stopForegroundRefresh(ctx) {
|
|
229
253
|
if (ctx && ctx._foregroundRefreshInterval) {
|
|
230
254
|
clearInterval(ctx._foregroundRefreshInterval);
|
|
231
255
|
ctx._foregroundRefreshInterval = null;
|
|
232
256
|
}
|
|
257
|
+
if (ctx && ctx._presenceRefreshInterval) {
|
|
258
|
+
clearInterval(ctx._presenceRefreshInterval);
|
|
259
|
+
ctx._presenceRefreshInterval = null;
|
|
260
|
+
}
|
|
233
261
|
}
|
|
234
262
|
function getStormGuard(ctx) {
|
|
235
263
|
if (!ctx._mqttStorm) {
|
|
@@ -503,8 +531,17 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
503
531
|
}
|
|
504
532
|
const chatOn = (ctx.globalOptions && ctx.globalOptions.online === false) ? false : true;
|
|
505
533
|
const foreground = getForegroundState(ctx);
|
|
534
|
+
|
|
535
|
+
// ws3-fca style: Generate fresh sessionID and clientID on EACH reconnect
|
|
536
|
+
// This prevents Facebook from detecting "stale" sessions and force-disconnecting
|
|
506
537
|
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
|
538
|
+
const clientID = utils.generateClientID ? utils.generateClientID() : `mqttwsclient_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
507
539
|
const GUID = utils.getGUID();
|
|
540
|
+
|
|
541
|
+
// Store for debugging/tracking
|
|
542
|
+
ctx._lastSessionID = sessionID;
|
|
543
|
+
ctx._lastClientID = clientID;
|
|
544
|
+
|
|
508
545
|
const username = {
|
|
509
546
|
u: ctx.userID,
|
|
510
547
|
s: sessionID,
|
|
@@ -528,16 +565,26 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
528
565
|
};
|
|
529
566
|
// jitter user agent keep consistent
|
|
530
567
|
const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
|
|
568
|
+
|
|
569
|
+
// MQTT Host Selection - ws3-fca uses messenger.com, we support both with option
|
|
570
|
+
// messenger.com endpoint appears more stable for some accounts
|
|
531
571
|
let host;
|
|
572
|
+
const useMessengerDomain = ctx.globalOptions.mqttUseMessengerDomain;
|
|
573
|
+
const baseDomain = useMessengerDomain ? 'edge-chat.messenger.com' : 'edge-chat.facebook.com';
|
|
574
|
+
|
|
532
575
|
if (ctx.mqttEndpoint) {
|
|
533
|
-
host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${
|
|
576
|
+
host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${clientID}`;
|
|
534
577
|
} else if (ctx.region) {
|
|
535
|
-
host = `wss
|
|
578
|
+
host = `wss://${baseDomain}/chat?region=${ctx.region.toLowerCase()}&sid=${sessionID}&cid=${clientID}`;
|
|
536
579
|
} else {
|
|
537
|
-
host = `wss
|
|
580
|
+
host = `wss://${baseDomain}/chat?sid=${sessionID}&cid=${clientID}`;
|
|
538
581
|
}
|
|
582
|
+
|
|
583
|
+
// Determine Host header based on domain used
|
|
584
|
+
const hostHeader = useMessengerDomain ? 'edge-chat.messenger.com' : 'edge-chat.facebook.com';
|
|
585
|
+
|
|
539
586
|
const options = {
|
|
540
|
-
clientId: "mqttwsclient"
|
|
587
|
+
clientId: clientID, // Use dynamic clientID instead of static "mqttwsclient"
|
|
541
588
|
protocolId: "MQIsdp",
|
|
542
589
|
protocolVersion: 3,
|
|
543
590
|
username: JSON.stringify(username),
|
|
@@ -550,7 +597,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
550
597
|
ctx.globalOptions.userAgent ||
|
|
551
598
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
552
599
|
Referer: "https://www.facebook.com/",
|
|
553
|
-
Host:
|
|
600
|
+
Host: hostHeader,
|
|
554
601
|
Connection: "Upgrade",
|
|
555
602
|
Pragma: "no-cache",
|
|
556
603
|
"Cache-Control": "no-cache",
|
|
@@ -783,16 +830,16 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
|
783
830
|
ctx.health.onConnect();
|
|
784
831
|
|
|
785
832
|
// WS3-style randomized proactive reconnect (tunable window).
|
|
786
|
-
//
|
|
787
|
-
//
|
|
833
|
+
// CRITICAL FIX: Changed from 6-8h to 45-90min based on ws3-fca analysis (26-60min)
|
|
834
|
+
// Long sessions (2h+) cause Facebook to force-disconnect, proactive reconnect prevents this
|
|
788
835
|
if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer);
|
|
789
836
|
let reconnectTime = null;
|
|
790
837
|
const opts = ctx.globalOptions || {};
|
|
791
838
|
const proactiveEnabled = opts.mqttProactiveReconnectEnabled;
|
|
792
839
|
|
|
793
840
|
if (proactiveEnabled !== false) {
|
|
794
|
-
const minM = Number.isFinite(opts.mqttProactiveReconnectMinMinutes) ? opts.mqttProactiveReconnectMinMinutes :
|
|
795
|
-
const maxM = Number.isFinite(opts.mqttProactiveReconnectMaxMinutes) ? opts.mqttProactiveReconnectMaxMinutes :
|
|
841
|
+
const minM = Number.isFinite(opts.mqttProactiveReconnectMinMinutes) ? opts.mqttProactiveReconnectMinMinutes : 45; // ws3-fca: 26min, we use 45min for safety margin
|
|
842
|
+
const maxM = Number.isFinite(opts.mqttProactiveReconnectMaxMinutes) ? opts.mqttProactiveReconnectMaxMinutes : 90; // ws3-fca: 60min, we use 90min for safety margin
|
|
796
843
|
const min = Math.min(minM, maxM);
|
|
797
844
|
const max = Math.max(minM, maxM);
|
|
798
845
|
const intervalMinutes = Math.floor(Math.random() * (max - min + 1)) + min;
|
package/src/sendMessage.js
CHANGED
|
@@ -269,7 +269,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
269
269
|
// Changing this to accomodate an array of users
|
|
270
270
|
if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
|
|
271
271
|
|
|
272
|
-
if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " +
|
|
272
|
+
if (replyToMessage && messageIDType !== 'String') return callback({ error: "MessageID should be of type string and not " + messageIDType + "." });
|
|
273
273
|
|
|
274
274
|
if (msgType === "String") msg = { body: msg };
|
|
275
275
|
var disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
|
|
@@ -308,11 +308,11 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
308
308
|
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
|
|
309
309
|
signatureID: utils.getSignatureID(),
|
|
310
310
|
replied_to_message_id: replyToMessage,
|
|
311
|
-
reply_metadata: replyToMessage ? {
|
|
311
|
+
reply_metadata: replyToMessage ? JSON.stringify({
|
|
312
312
|
reply_source_id: replyToMessage,
|
|
313
313
|
reply_source_type: 1, // 1: Message
|
|
314
314
|
reply_type: 0 // 0: Reply
|
|
315
|
-
} : undefined
|
|
315
|
+
}) : undefined
|
|
316
316
|
};
|
|
317
317
|
|
|
318
318
|
handleLocation(msg, form, callback, () =>
|
package/utils.js
CHANGED
|
@@ -115,6 +115,15 @@ function getHeaders(url, options, ctx, customHeader) {
|
|
|
115
115
|
headers["sec-fetch-dest"] = "document";
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// Dynamic Referer Pattern
|
|
119
|
+
if (url.includes('graphql')) {
|
|
120
|
+
headers.Referer = 'https://www.facebook.com/';
|
|
121
|
+
} else if (url.includes('messages/')) {
|
|
122
|
+
headers.Referer = 'https://www.facebook.com/messages/';
|
|
123
|
+
} else if (url.includes('business')) {
|
|
124
|
+
headers.Referer = 'https://business.facebook.com/';
|
|
125
|
+
}
|
|
126
|
+
|
|
118
127
|
// Dynamic Client Hints - CRITICAL: Must match User-Agent
|
|
119
128
|
if (isChrome && isWindows) {
|
|
120
129
|
headers["sec-ch-ua"] = '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"';
|
|
@@ -167,6 +176,7 @@ async function get(url, jar, qs, options, ctx) {
|
|
|
167
176
|
if (qs) op.searchParams = cleanObject(qs);
|
|
168
177
|
|
|
169
178
|
return got(url, op).then(function (res) {
|
|
179
|
+
saveCookies(jar, ctx)(res);
|
|
170
180
|
return res;
|
|
171
181
|
});
|
|
172
182
|
}
|
|
@@ -183,6 +193,7 @@ async function get2(url, jar, headers, options, ctx) {
|
|
|
183
193
|
};
|
|
184
194
|
|
|
185
195
|
return got(url, op).then(function (res) {
|
|
196
|
+
saveCookies(jar, ctx)(res);
|
|
186
197
|
return res;
|
|
187
198
|
});
|
|
188
199
|
}
|
|
@@ -230,6 +241,7 @@ async function post(url, jar, form, options, ctx, customHeader) {
|
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
return got(url, op).then(function (res) {
|
|
244
|
+
saveCookies(jar, ctx)(res);
|
|
233
245
|
return res;
|
|
234
246
|
});
|
|
235
247
|
}
|
|
@@ -256,6 +268,7 @@ async function postFormData(url, jar, form, qs, options, ctx) {
|
|
|
256
268
|
if (qs) op.searchParams = cleanObject(qs);
|
|
257
269
|
|
|
258
270
|
return got(url, op).then(function (res) {
|
|
271
|
+
saveCookies(jar, ctx)(res);
|
|
259
272
|
return res;
|
|
260
273
|
});
|
|
261
274
|
}
|
|
@@ -387,6 +400,19 @@ function generatePresence(userID) {
|
|
|
387
400
|
);
|
|
388
401
|
}
|
|
389
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Generate a unique MQTT client ID for each connection
|
|
405
|
+
* ws3-fca style: Fresh clientID on each reconnect prevents Facebook from
|
|
406
|
+
* detecting "stale" sessions and force-disconnecting
|
|
407
|
+
* @returns {string} Unique client identifier
|
|
408
|
+
*/
|
|
409
|
+
function generateClientID() {
|
|
410
|
+
const timestamp = Date.now().toString(36);
|
|
411
|
+
const random = Math.random().toString(36).substr(2, 9);
|
|
412
|
+
const counter = (global.__NEXUS_CLIENT_COUNTER__ = (global.__NEXUS_CLIENT_COUNTER__ || 0) + 1);
|
|
413
|
+
return `mqttwsclient_${timestamp}_${random}_${counter}`;
|
|
414
|
+
}
|
|
415
|
+
|
|
390
416
|
function getGUID() {
|
|
391
417
|
// 1. Check Environment Variable (Best for Render/Heroku/Replit)
|
|
392
418
|
if (process.env.NEXUS_DEVICE_ID) {
|
|
@@ -1311,9 +1337,11 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount = 0, sourceCall) {
|
|
|
1311
1337
|
});
|
|
1312
1338
|
};
|
|
1313
1339
|
}
|
|
1314
|
-
function saveCookies(jar) {
|
|
1340
|
+
function saveCookies(jar, ctx) {
|
|
1315
1341
|
return function (res) {
|
|
1316
1342
|
var cookies = res.headers["set-cookie"] || [];
|
|
1343
|
+
if (cookies.length === 0) return res;
|
|
1344
|
+
|
|
1317
1345
|
cookies.forEach(function (c) {
|
|
1318
1346
|
if (c.indexOf(".facebook.com") > -1 || c.indexOf("facebook.com") > -1) {
|
|
1319
1347
|
try {
|
|
@@ -1328,10 +1356,50 @@ function saveCookies(jar) {
|
|
|
1328
1356
|
} catch (e) { }
|
|
1329
1357
|
}
|
|
1330
1358
|
});
|
|
1359
|
+
|
|
1360
|
+
// AUTO-PERSISTENCE: If ctx has appStatePath, save to disk immediately
|
|
1361
|
+
if (ctx && ctx.appStatePath) {
|
|
1362
|
+
saveAppState(ctx);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1331
1365
|
return res;
|
|
1332
1366
|
};
|
|
1333
1367
|
}
|
|
1334
1368
|
|
|
1369
|
+
function saveAppState(ctx) {
|
|
1370
|
+
if (!ctx || !ctx.jar || !ctx.appStatePath) return;
|
|
1371
|
+
try {
|
|
1372
|
+
const appState = getAppState(ctx.jar);
|
|
1373
|
+
const fs = require('fs');
|
|
1374
|
+
const path = require('path');
|
|
1375
|
+
|
|
1376
|
+
// Ensure directory exists
|
|
1377
|
+
const dir = path.dirname(ctx.appStatePath);
|
|
1378
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1379
|
+
|
|
1380
|
+
fs.writeFileSync(ctx.appStatePath, JSON.stringify(appState, null, 2));
|
|
1381
|
+
|
|
1382
|
+
// Optional: Mirror to environment-ready file for cloud platforms
|
|
1383
|
+
const envPath = ctx.appStatePath.replace('.json', '.env.json');
|
|
1384
|
+
if (process.env.NEXUS_SYNC_ENV_FILE === '1') {
|
|
1385
|
+
fs.writeFileSync(envPath, JSON.stringify(appState));
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// EXTERNAL PERSISTENCE: If NEXUS_EXTERNAL_SAVE_URL is set, POST there
|
|
1389
|
+
if (process.env.NEXUS_EXTERNAL_SAVE_URL) {
|
|
1390
|
+
const axios = require('axios');
|
|
1391
|
+
axios.post(process.env.NEXUS_EXTERNAL_SAVE_URL, appState, {
|
|
1392
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': 'Nexus-FCA-Persistence' },
|
|
1393
|
+
timeout: 10000
|
|
1394
|
+
}).catch(err => {
|
|
1395
|
+
log.verbose("utils", "External appstate sync failed: " + err.message);
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
} catch (e) {
|
|
1399
|
+
log.verbose("utils", "Auto-save appstate failed: " + e.message);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1335
1403
|
var NUM_TO_MONTH = [
|
|
1336
1404
|
"Jan",
|
|
1337
1405
|
"Feb",
|
|
@@ -1811,6 +1879,15 @@ module.exports = {
|
|
|
1811
1879
|
setData_Path,
|
|
1812
1880
|
getPaths,
|
|
1813
1881
|
saveCookies,
|
|
1882
|
+
saveAppState,
|
|
1883
|
+
getAppState,
|
|
1884
|
+
getAppStateB64: function () {
|
|
1885
|
+
if (process.env.NEXUS_APPSTATE_B64) {
|
|
1886
|
+
try { return JSON.parse(Buffer.from(process.env.NEXUS_APPSTATE_B64, 'base64').toString('utf8')); }
|
|
1887
|
+
catch (e) { log.error("utils", "Failed to parse NEXUS_APPSTATE_B64"); return null; }
|
|
1888
|
+
}
|
|
1889
|
+
return null;
|
|
1890
|
+
},
|
|
1814
1891
|
getType,
|
|
1815
1892
|
_formatAttachment,
|
|
1816
1893
|
formatHistoryMessage,
|
|
@@ -1827,6 +1904,7 @@ module.exports = {
|
|
|
1827
1904
|
formatReadReceipt,
|
|
1828
1905
|
formatRead,
|
|
1829
1906
|
generatePresence,
|
|
1907
|
+
generateClientID,
|
|
1830
1908
|
formatDate,
|
|
1831
1909
|
decodeClientPayload,
|
|
1832
1910
|
getAppState,
|