nexus-fca 3.2.3 → 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/README.md +1 -0
- package/index.js +24 -2
- package/lib/factory/ApiFactory.js +16 -7
- 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 +166 -47
- package/package.json +2 -2
- package/src/listenMqtt.js +58 -11
- package/src/sendMessage.js +3 -3
- package/utils.js +107 -8
package/README.md
CHANGED
|
@@ -187,6 +187,7 @@ const login = require('nexus-fca');
|
|
|
187
187
|
## 📚 Documentation Map
|
|
188
188
|
| Resource | Location |
|
|
189
189
|
|----------|----------|
|
|
190
|
+
| **Usage Guide (Examples)** | `USAGE-GUIDE.md` |
|
|
190
191
|
| Full API Reference | `DOCS.md` |
|
|
191
192
|
| Feature Guides | `docs/*.md` |
|
|
192
193
|
| Configuration Reference | `docs/configuration-reference.md` |
|
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
|
}
|
|
@@ -183,6 +184,9 @@ class ApiFactory {
|
|
|
183
184
|
globalOptions[key] = options[key];
|
|
184
185
|
});
|
|
185
186
|
},
|
|
187
|
+
autoTyping: (enable = true) => {
|
|
188
|
+
globalOptions.autoTyping = !!enable;
|
|
189
|
+
},
|
|
186
190
|
getAppState: function () {
|
|
187
191
|
const appState = utils.getAppState(ctx.jar);
|
|
188
192
|
return appState.filter((item, index, self) =>
|
|
@@ -217,6 +221,11 @@ class ApiFactory {
|
|
|
217
221
|
}
|
|
218
222
|
};
|
|
219
223
|
|
|
224
|
+
// Default options
|
|
225
|
+
if (typeof globalOptions.autoTyping === 'undefined') {
|
|
226
|
+
globalOptions.autoTyping = true; // Enabled by default for safety
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
// Default edit settings
|
|
221
230
|
if (!globalOptions.editSettings) {
|
|
222
231
|
globalOptions.editSettings = {
|
|
@@ -293,9 +302,9 @@ class ApiFactory {
|
|
|
293
302
|
globalOptions.groupQueueIdleMs = 30 * 60 * 1000;
|
|
294
303
|
|
|
295
304
|
api._sendMessageDirect = DIRECT_FN;
|
|
296
|
-
api.sendMessage = function (message, threadID, cb) {
|
|
305
|
+
api.sendMessage = function (message, threadID, cb, replyToMessage) {
|
|
297
306
|
if (!globalOptions.groupQueueEnabled || !isGroupThread(threadID)) {
|
|
298
|
-
return api._sendMessageDirect(message, threadID, cb);
|
|
307
|
+
return api._sendMessageDirect(message, threadID, cb, replyToMessage);
|
|
299
308
|
}
|
|
300
309
|
let entry = groupQueues.get(threadID);
|
|
301
310
|
if (!entry) { entry = { q: [], sending: false, lastActive: Date.now() }; groupQueues.set(threadID, entry); }
|
|
@@ -304,7 +313,7 @@ class ApiFactory {
|
|
|
304
313
|
entry.q.shift();
|
|
305
314
|
if (ctx.health) ctx.health.recordGroupQueuePrune(0, 0, 1);
|
|
306
315
|
}
|
|
307
|
-
entry.q.push({ message, threadID, cb });
|
|
316
|
+
entry.q.push({ message, threadID, cb, replyToMessage });
|
|
308
317
|
processQueue(threadID, entry);
|
|
309
318
|
};
|
|
310
319
|
|
|
@@ -312,13 +321,13 @@ class ApiFactory {
|
|
|
312
321
|
if (entry.sending) return;
|
|
313
322
|
if (!entry.q.length) return;
|
|
314
323
|
entry.sending = true;
|
|
315
|
-
const
|
|
316
|
-
api._sendMessageDirect(message,
|
|
324
|
+
const item = entry.q.shift();
|
|
325
|
+
api._sendMessageDirect(item.message, item.threadID, function (err, res) {
|
|
317
326
|
try { if (!err && this.globalSafety) this.globalSafety.recordEvent(); } catch (_) { }
|
|
318
|
-
if (typeof cb === 'function') cb(err, res);
|
|
327
|
+
if (typeof item.cb === 'function') item.cb(err, res);
|
|
319
328
|
entry.sending = false;
|
|
320
329
|
setImmediate(() => processQueue(threadID, entry));
|
|
321
|
-
}.bind(this));
|
|
330
|
+
}.bind(this), item.replyToMessage);
|
|
322
331
|
}
|
|
323
332
|
|
|
324
333
|
api._flushGroupQueue = function (threadID) {
|
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.
|
|
@@ -49,13 +50,13 @@ class FacebookSafety {
|
|
|
49
50
|
this.regions = ['ASH', 'ATL', 'DFW', 'ORD', 'PHX', 'SJC', 'IAD'];
|
|
50
51
|
this.currentRegion = this.regions[Math.floor(Math.random() * this.regions.length)];
|
|
51
52
|
|
|
52
|
-
// ULTRA-SAFE human delay patterns -
|
|
53
|
+
// ULTRA-SAFE human delay patterns - More conservative
|
|
53
54
|
this.humanDelayPatterns = {
|
|
54
|
-
typing: { min:
|
|
55
|
-
reading: { min:
|
|
56
|
-
thinking: { min:
|
|
57
|
-
browsing: { min:
|
|
58
|
-
messageDelay: { min:
|
|
55
|
+
typing: { min: 800, max: 2500 }, // Normal typing (0.8-2.5s)
|
|
56
|
+
reading: { min: 1500, max: 5000 }, // Normal reading (1.5-5s)
|
|
57
|
+
thinking: { min: 1500, max: 6000 }, // Normal thinking (1.5-6s)
|
|
58
|
+
browsing: { min: 1000, max: 3000 }, // Normal browsing (1-3s)
|
|
59
|
+
messageDelay: { min: 1500, max: 4000 } // 1.5-4s between messages (Conservative)
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
this.sessionMetrics = {
|
|
@@ -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));
|
|
@@ -800,16 +853,17 @@ class FacebookSafety {
|
|
|
800
853
|
const risk = this.sessionMetrics.riskLevel;
|
|
801
854
|
const since = Date.now() - this._lastHeavyMaintenanceTs;
|
|
802
855
|
const inWindow = since < this._adaptivePacingWindowMs;
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
} else {
|
|
809
|
-
|
|
810
|
-
|
|
856
|
+
|
|
857
|
+
let min = 1000, max = 2500; // Default baseline human pace
|
|
858
|
+
|
|
859
|
+
if (risk === 'high') {
|
|
860
|
+
min = 3500; max = 6500;
|
|
861
|
+
} else if (risk === 'medium') {
|
|
862
|
+
min = 2000; max = 4500;
|
|
863
|
+
} else if (inWindow) {
|
|
864
|
+
min = 1500; max = 3000;
|
|
811
865
|
}
|
|
812
|
-
|
|
866
|
+
|
|
813
867
|
return Math.floor(min + Math.random() * (max - min));
|
|
814
868
|
}
|
|
815
869
|
|
|
@@ -833,6 +887,71 @@ class FacebookSafety {
|
|
|
833
887
|
setSafetyEventHandler(handler) {
|
|
834
888
|
this.onSafetyEvent = handler;
|
|
835
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
|
+
}
|
|
836
955
|
}
|
|
837
956
|
|
|
838
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
|
@@ -87,23 +87,53 @@ function getJar() {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function getHeaders(url, options, ctx, customHeader) {
|
|
90
|
+
const ua = (options?.userAgent || "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36");
|
|
91
|
+
const isWindows = ua.includes("Windows NT");
|
|
92
|
+
const isAndroid = ua.includes("Android");
|
|
93
|
+
const isChrome = ua.includes("Chrome") && !ua.includes("Edg");
|
|
94
|
+
|
|
90
95
|
var headers = {
|
|
91
96
|
Referer: "https://www.facebook.com/",
|
|
92
97
|
Host: url.replace("https://", "").split("/")[0],
|
|
93
98
|
Origin: "https://www.facebook.com",
|
|
94
|
-
"user-agent":
|
|
99
|
+
"user-agent": ua,
|
|
95
100
|
Connection: "keep-alive",
|
|
96
|
-
"sec-fetch-site": 'same-origin',
|
|
97
|
-
"sec-fetch-mode": 'cors',
|
|
98
|
-
"sec-fetch-dest": "empty",
|
|
99
101
|
"accept": "*/*",
|
|
100
102
|
"accept-language": "en-US,en;q=0.9",
|
|
101
|
-
"sec-ch-ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
|
102
|
-
"sec-ch-ua-mobile": "?0",
|
|
103
|
-
"sec-ch-ua-platform": '"Windows"',
|
|
104
103
|
"dnt": "1",
|
|
105
104
|
"upgrade-insecure-requests": "1"
|
|
106
105
|
};
|
|
106
|
+
|
|
107
|
+
// Human-like Fetch headers
|
|
108
|
+
if (url.includes("/api/graphql/") || url.includes("/messaging/")) {
|
|
109
|
+
headers["sec-fetch-site"] = 'same-origin';
|
|
110
|
+
headers["sec-fetch-mode"] = 'cors';
|
|
111
|
+
headers["sec-fetch-dest"] = "empty";
|
|
112
|
+
} else {
|
|
113
|
+
headers["sec-fetch-site"] = 'none';
|
|
114
|
+
headers["sec-fetch-mode"] = 'navigate';
|
|
115
|
+
headers["sec-fetch-dest"] = "document";
|
|
116
|
+
}
|
|
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
|
+
|
|
127
|
+
// Dynamic Client Hints - CRITICAL: Must match User-Agent
|
|
128
|
+
if (isChrome && isWindows) {
|
|
129
|
+
headers["sec-ch-ua"] = '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"';
|
|
130
|
+
headers["sec-ch-ua-mobile"] = "?0";
|
|
131
|
+
headers["sec-ch-ua-platform"] = '"Windows"';
|
|
132
|
+
} else if (isAndroid) {
|
|
133
|
+
headers["sec-ch-ua-mobile"] = "?1";
|
|
134
|
+
// Remove Windows-specific headers for Android UAs
|
|
135
|
+
}
|
|
136
|
+
|
|
107
137
|
if (customHeader) Object.assign(headers, customHeader);
|
|
108
138
|
if (ctx && ctx.region) headers["X-MSGR-Region"] = ctx.region;
|
|
109
139
|
|
|
@@ -146,6 +176,7 @@ async function get(url, jar, qs, options, ctx) {
|
|
|
146
176
|
if (qs) op.searchParams = cleanObject(qs);
|
|
147
177
|
|
|
148
178
|
return got(url, op).then(function (res) {
|
|
179
|
+
saveCookies(jar, ctx)(res);
|
|
149
180
|
return res;
|
|
150
181
|
});
|
|
151
182
|
}
|
|
@@ -162,6 +193,7 @@ async function get2(url, jar, headers, options, ctx) {
|
|
|
162
193
|
};
|
|
163
194
|
|
|
164
195
|
return got(url, op).then(function (res) {
|
|
196
|
+
saveCookies(jar, ctx)(res);
|
|
165
197
|
return res;
|
|
166
198
|
});
|
|
167
199
|
}
|
|
@@ -209,6 +241,7 @@ async function post(url, jar, form, options, ctx, customHeader) {
|
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
return got(url, op).then(function (res) {
|
|
244
|
+
saveCookies(jar, ctx)(res);
|
|
212
245
|
return res;
|
|
213
246
|
});
|
|
214
247
|
}
|
|
@@ -235,6 +268,7 @@ async function postFormData(url, jar, form, qs, options, ctx) {
|
|
|
235
268
|
if (qs) op.searchParams = cleanObject(qs);
|
|
236
269
|
|
|
237
270
|
return got(url, op).then(function (res) {
|
|
271
|
+
saveCookies(jar, ctx)(res);
|
|
238
272
|
return res;
|
|
239
273
|
});
|
|
240
274
|
}
|
|
@@ -366,6 +400,19 @@ function generatePresence(userID) {
|
|
|
366
400
|
);
|
|
367
401
|
}
|
|
368
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
|
+
|
|
369
416
|
function getGUID() {
|
|
370
417
|
// 1. Check Environment Variable (Best for Render/Heroku/Replit)
|
|
371
418
|
if (process.env.NEXUS_DEVICE_ID) {
|
|
@@ -1290,9 +1337,11 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount = 0, sourceCall) {
|
|
|
1290
1337
|
});
|
|
1291
1338
|
};
|
|
1292
1339
|
}
|
|
1293
|
-
function saveCookies(jar) {
|
|
1340
|
+
function saveCookies(jar, ctx) {
|
|
1294
1341
|
return function (res) {
|
|
1295
1342
|
var cookies = res.headers["set-cookie"] || [];
|
|
1343
|
+
if (cookies.length === 0) return res;
|
|
1344
|
+
|
|
1296
1345
|
cookies.forEach(function (c) {
|
|
1297
1346
|
if (c.indexOf(".facebook.com") > -1 || c.indexOf("facebook.com") > -1) {
|
|
1298
1347
|
try {
|
|
@@ -1307,10 +1356,50 @@ function saveCookies(jar) {
|
|
|
1307
1356
|
} catch (e) { }
|
|
1308
1357
|
}
|
|
1309
1358
|
});
|
|
1359
|
+
|
|
1360
|
+
// AUTO-PERSISTENCE: If ctx has appStatePath, save to disk immediately
|
|
1361
|
+
if (ctx && ctx.appStatePath) {
|
|
1362
|
+
saveAppState(ctx);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1310
1365
|
return res;
|
|
1311
1366
|
};
|
|
1312
1367
|
}
|
|
1313
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
|
+
|
|
1314
1403
|
var NUM_TO_MONTH = [
|
|
1315
1404
|
"Jan",
|
|
1316
1405
|
"Feb",
|
|
@@ -1790,6 +1879,15 @@ module.exports = {
|
|
|
1790
1879
|
setData_Path,
|
|
1791
1880
|
getPaths,
|
|
1792
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
|
+
},
|
|
1793
1891
|
getType,
|
|
1794
1892
|
_formatAttachment,
|
|
1795
1893
|
formatHistoryMessage,
|
|
@@ -1806,6 +1904,7 @@ module.exports = {
|
|
|
1806
1904
|
formatReadReceipt,
|
|
1807
1905
|
formatRead,
|
|
1808
1906
|
generatePresence,
|
|
1907
|
+
generateClientID,
|
|
1809
1908
|
formatDate,
|
|
1810
1909
|
decodeClientPayload,
|
|
1811
1910
|
getAppState,
|