@lazyneoaz/testfca 1.0.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.
Files changed (120) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/COOKIE_LOGIN.md +208 -0
  3. package/LICENSE +3 -0
  4. package/README.md +492 -0
  5. package/index.js +2 -0
  6. package/package.json +120 -0
  7. package/scripts/build-go.mjs +54 -0
  8. package/scripts/detect-platform.mjs +36 -0
  9. package/scripts/download-prebuilt.mjs +119 -0
  10. package/scripts/package.mjs +6 -0
  11. package/scripts/postinstall.mjs +113 -0
  12. package/src/apis/addExternalModule.js +24 -0
  13. package/src/apis/addUserToGroup.js +108 -0
  14. package/src/apis/changeAdminStatus.js +148 -0
  15. package/src/apis/changeArchivedStatus.js +61 -0
  16. package/src/apis/changeAvatar.js +103 -0
  17. package/src/apis/changeBio.js +69 -0
  18. package/src/apis/changeBlockedStatus.js +54 -0
  19. package/src/apis/changeGroupImage.js +136 -0
  20. package/src/apis/changeThreadColor.js +116 -0
  21. package/src/apis/changeThreadEmoji.js +53 -0
  22. package/src/apis/comment.js +207 -0
  23. package/src/apis/createAITheme.js +129 -0
  24. package/src/apis/createNewGroup.js +79 -0
  25. package/src/apis/createPoll.js +73 -0
  26. package/src/apis/deleteMessage.js +52 -0
  27. package/src/apis/deleteThread.js +52 -0
  28. package/src/apis/e2ee.js +170 -0
  29. package/src/apis/editMessage.js +78 -0
  30. package/src/apis/emoji.js +124 -0
  31. package/src/apis/fetchThemeData.js +82 -0
  32. package/src/apis/follow.js +81 -0
  33. package/src/apis/forwardMessage.js +52 -0
  34. package/src/apis/friend.js +243 -0
  35. package/src/apis/gcmember.js +122 -0
  36. package/src/apis/gcname.js +123 -0
  37. package/src/apis/gcrule.js +119 -0
  38. package/src/apis/getAccess.js +111 -0
  39. package/src/apis/getBotInfo.js +88 -0
  40. package/src/apis/getBotInitialData.js +43 -0
  41. package/src/apis/getFriendsList.js +79 -0
  42. package/src/apis/getMessage.js +423 -0
  43. package/src/apis/getTheme.js +95 -0
  44. package/src/apis/getThemeInfo.js +116 -0
  45. package/src/apis/getThreadHistory.js +239 -0
  46. package/src/apis/getThreadInfo.js +267 -0
  47. package/src/apis/getThreadList.js +232 -0
  48. package/src/apis/getThreadPictures.js +58 -0
  49. package/src/apis/getUserID.js +117 -0
  50. package/src/apis/getUserInfo.js +513 -0
  51. package/src/apis/getUserInfoV2.js +146 -0
  52. package/src/apis/handleMessageRequest.js +50 -0
  53. package/src/apis/httpGet.js +63 -0
  54. package/src/apis/httpPost.js +89 -0
  55. package/src/apis/httpPostFormData.js +69 -0
  56. package/src/apis/listenMqtt.js +1236 -0
  57. package/src/apis/listenSpeed.js +179 -0
  58. package/src/apis/logout.js +93 -0
  59. package/src/apis/markAsDelivered.js +47 -0
  60. package/src/apis/markAsRead.js +115 -0
  61. package/src/apis/markAsReadAll.js +40 -0
  62. package/src/apis/markAsSeen.js +70 -0
  63. package/src/apis/mqttDeltaValue.js +250 -0
  64. package/src/apis/muteThread.js +45 -0
  65. package/src/apis/nickname.js +132 -0
  66. package/src/apis/notes.js +163 -0
  67. package/src/apis/pinMessage.js +150 -0
  68. package/src/apis/produceMetaTheme.js +180 -0
  69. package/src/apis/realtime.js +182 -0
  70. package/src/apis/removeUserFromGroup.js +117 -0
  71. package/src/apis/resolvePhotoUrl.js +58 -0
  72. package/src/apis/searchForThread.js +154 -0
  73. package/src/apis/sendMessage.js +346 -0
  74. package/src/apis/sendMessageMqtt.js +248 -0
  75. package/src/apis/sendTypingIndicator.js +105 -0
  76. package/src/apis/setMessageReaction.js +38 -0
  77. package/src/apis/setMessageReactionMqtt.js +61 -0
  78. package/src/apis/setThreadTheme.js +260 -0
  79. package/src/apis/setThreadThemeMqtt.js +94 -0
  80. package/src/apis/share.js +107 -0
  81. package/src/apis/shareContact.js +66 -0
  82. package/src/apis/stickers.js +257 -0
  83. package/src/apis/story.js +181 -0
  84. package/src/apis/theme.js +233 -0
  85. package/src/apis/unfriend.js +47 -0
  86. package/src/apis/unsendMessage.js +25 -0
  87. package/src/database/appStateBackup.js +298 -0
  88. package/src/database/models/index.js +56 -0
  89. package/src/database/models/thread.js +31 -0
  90. package/src/database/models/user.js +32 -0
  91. package/src/database/threadData.js +101 -0
  92. package/src/database/userData.js +90 -0
  93. package/src/e2ee/bridge.js +275 -0
  94. package/src/e2ee/index.js +60 -0
  95. package/src/engine/client.js +95 -0
  96. package/src/engine/models/buildAPI.js +152 -0
  97. package/src/engine/models/loginHelper.js +574 -0
  98. package/src/engine/models/setOptions.js +88 -0
  99. package/src/types/index.d.ts +574 -0
  100. package/src/utils/antiSuspension.js +529 -0
  101. package/src/utils/auth-helpers.js +149 -0
  102. package/src/utils/autoReLogin.js +336 -0
  103. package/src/utils/axios.js +436 -0
  104. package/src/utils/cache.js +54 -0
  105. package/src/utils/clients.js +282 -0
  106. package/src/utils/constants.js +410 -0
  107. package/src/utils/formatters/data/formatAttachment.js +370 -0
  108. package/src/utils/formatters/data/formatDelta.js +109 -0
  109. package/src/utils/formatters/index.js +159 -0
  110. package/src/utils/formatters/value/formatCookie.js +91 -0
  111. package/src/utils/formatters/value/formatDate.js +36 -0
  112. package/src/utils/formatters/value/formatID.js +16 -0
  113. package/src/utils/formatters.js +1373 -0
  114. package/src/utils/headers.js +235 -0
  115. package/src/utils/index.js +153 -0
  116. package/src/utils/monitoring.js +333 -0
  117. package/src/utils/rateLimiter.js +319 -0
  118. package/src/utils/tokenRefresh.js +680 -0
  119. package/src/utils/user-agents.js +238 -0
  120. package/src/utils/validation.js +157 -0
@@ -0,0 +1,574 @@
1
+ "use strict";
2
+
3
+ const utils = require('../../utils');
4
+ const axios = require("axios");
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+ const qs = require("querystring");
8
+ const { normalizeCookieHeaderString, setJarFromPairs } = require('../../utils/formatters/value/formatCookie');
9
+ const { parseRegion, genTotp } = require('../../utils/auth-helpers');
10
+ const { generateUserAgentByPersona, cachePersonaData } = require('../../utils/user-agents');
11
+
12
+ /**
13
+ * The main login helper function, orchestrating the login process.
14
+ *
15
+ * @param {object} credentials User credentials or appState.
16
+ * @param {object} globalOptions Global options for the API.
17
+ * @param {function} callback The final callback function.
18
+ * @param {function} setOptionsFunc Reference to the setOptions function from models.
19
+ * @param {function} buildAPIFunc Reference to the buildAPI function from models.
20
+ * @param {object} initialApi The initial API object to extend.
21
+ * @param {function} fbLinkFunc A function to generate Facebook links.
22
+ * @param {string} errorRetrievingMsg The error message for retrieving user ID.
23
+ * @returns {Promise<void>}
24
+ */
25
+ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc, buildAPIFunc, initialApi, fbLinkFunc, errorRetrievingMsg) {
26
+ let ctx = null;
27
+ let defaultFuncs = null;
28
+ let api = initialApi;
29
+
30
+ // Display startup banner
31
+ const { startupBanner } = require('../../utils');
32
+ startupBanner();
33
+
34
+ try {
35
+ const jar = utils.getJar();
36
+ utils.log("Logging in...");
37
+
38
+ const persona = globalOptions.persona || 'desktop';
39
+ const personaSwitched = globalOptions.cachedPersona && globalOptions.cachedPersona !== persona;
40
+
41
+ if (personaSwitched) {
42
+ const oldPersona = globalOptions.cachedPersona;
43
+ utils.log(`Persona switched from ${oldPersona} to ${persona}, clearing ALL cached fingerprints`);
44
+
45
+ delete globalOptions.cachedUserAgent;
46
+ delete globalOptions.cachedSecChUa;
47
+ delete globalOptions.cachedSecChUaFullVersionList;
48
+ delete globalOptions.cachedSecChUaPlatform;
49
+ delete globalOptions.cachedSecChUaPlatformVersion;
50
+ delete globalOptions.cachedBrowser;
51
+
52
+ delete globalOptions.cachedAndroidUA;
53
+ delete globalOptions.cachedAndroidVersion;
54
+ delete globalOptions.cachedAndroidDevice;
55
+ delete globalOptions.cachedAndroidBuildId;
56
+ delete globalOptions.cachedAndroidResolution;
57
+ delete globalOptions.cachedAndroidFbav;
58
+ delete globalOptions.cachedAndroidFbbv;
59
+ delete globalOptions.cachedAndroidLocale;
60
+ delete globalOptions.cachedAndroidCarrier;
61
+
62
+ delete globalOptions.cachedLocale;
63
+ delete globalOptions.cachedTimezone;
64
+ }
65
+
66
+ const needsDesktopCache = (persona === 'desktop') && !globalOptions.cachedUserAgent;
67
+ const needsAndroidCache = (persona === 'android' || persona === 'mobile') && !globalOptions.cachedAndroidUA;
68
+
69
+ if (needsDesktopCache || needsAndroidCache) {
70
+ const personaData = generateUserAgentByPersona(persona, globalOptions);
71
+ cachePersonaData(globalOptions, personaData);
72
+ globalOptions.cachedPersona = persona;
73
+
74
+ if (persona === 'desktop') {
75
+ utils.log("Using desktop persona with browser:", personaData.browser);
76
+ } else {
77
+ utils.log("Using Android/Orca mobile persona");
78
+ }
79
+
80
+ const { getRandomLocale, getRandomTimezone } = require('../../utils/headers');
81
+ if (!globalOptions.cachedLocale) {
82
+ globalOptions.cachedLocale = getRandomLocale();
83
+ }
84
+ if (!globalOptions.cachedTimezone) {
85
+ globalOptions.cachedTimezone = getRandomTimezone();
86
+ }
87
+
88
+ // Lock the session fingerprint in anti-suspension module so it
89
+ // stays consistent for the entire session — UA/platform changes
90
+ // between requests are a strong bot detection signal.
91
+ try {
92
+ const { globalAntiSuspension } = require('../../utils/antiSuspension');
93
+ globalAntiSuspension.lockSessionFingerprint(
94
+ personaData.userAgent || globalOptions.cachedAndroidUA,
95
+ personaData.secChUa || '',
96
+ personaData.secChUaPlatform || personaData.persona || 'desktop',
97
+ globalOptions.cachedLocale,
98
+ globalOptions.cachedTimezone
99
+ );
100
+ } catch (_) {}
101
+ } else {
102
+ if (persona === 'desktop' && globalOptions.cachedUserAgent) {
103
+ utils.log("Using cached desktop persona");
104
+ } else if ((persona === 'android' || persona === 'mobile') && globalOptions.cachedAndroidUA) {
105
+ utils.log("Using cached Android/Orca mobile persona");
106
+ }
107
+ }
108
+
109
+ let appState = credentials.appState;
110
+
111
+ if (!appState && !credentials.email && !credentials.password) {
112
+ try {
113
+ const { hydrateJarFromDB } = require('../../database/appStateBackup');
114
+ const restored = await hydrateJarFromDB(jar, null);
115
+ if (restored) {
116
+ utils.log("Restored AppState from database backup");
117
+ }
118
+ } catch (dbErr) {
119
+ utils.warn("Failed to restore AppState from database:", dbErr.message);
120
+ }
121
+ }
122
+
123
+ if (appState) {
124
+ let cookieStrings = [];
125
+ if (Array.isArray(appState)) {
126
+ cookieStrings = appState.map(c => [c.name || c.key, c.value].join('='));
127
+ } else if (typeof appState === 'string') {
128
+ cookieStrings = normalizeCookieHeaderString(appState);
129
+
130
+ if (cookieStrings.length === 0) {
131
+ cookieStrings = appState.split(';').map(s => s.trim()).filter(Boolean);
132
+ }
133
+ } else {
134
+ throw new Error("Invalid appState format. Please provide an array of cookie objects or a cookie string.");
135
+ }
136
+
137
+ setJarFromPairs(jar, cookieStrings);
138
+ utils.log("Cookies set for facebook.com and messenger.com domains");
139
+
140
+ } else if (credentials.email && credentials.password) {
141
+
142
+ if (credentials.totpSecret) {
143
+ utils.log("TOTP secret detected, will generate 2FA code if needed");
144
+ }
145
+
146
+ const url = "https://api.facebook.com/method/auth.login";
147
+ const params = {
148
+ access_token: "350685531728|62f8ce9f74b12f84c123cc23437a4a32",
149
+ format: "json",
150
+ sdk_version: 2,
151
+ email: credentials.email,
152
+ locale: "en_US",
153
+ password: credentials.password,
154
+ generate_session_cookies: 1,
155
+ sig: "c1c640010993db92e5afd11634ced864",
156
+ }
157
+
158
+ if (credentials.totpSecret) {
159
+ try {
160
+ const totpCode = await genTotp(credentials.totpSecret);
161
+ params.credentials_type = "two_factor";
162
+ params.twofactor_code = totpCode;
163
+ utils.log("TOTP code generated successfully");
164
+ } catch (totpError) {
165
+ utils.warn("Failed to generate TOTP code:", totpError.message);
166
+ }
167
+ }
168
+
169
+ const query = qs.stringify(params);
170
+ const xurl = `${url}?${query}`;
171
+ try {
172
+ const resp = await axios.get(xurl);
173
+ if (resp.status !== 200) {
174
+ throw new Error("Wrong password / email");
175
+ }
176
+ let cstrs = resp.data["session_cookies"].map(c => `${c.name}=${c.value}`);
177
+ setJarFromPairs(jar, cstrs);
178
+ utils.log("Login successful with email/password");
179
+ } catch (e) {
180
+ if (credentials.totpSecret && !params.twofactor_code) {
181
+ throw new Error("2FA required but TOTP code generation failed");
182
+ }
183
+ throw new Error("Wrong password / email");
184
+ }
185
+ } else {
186
+ throw new Error("No cookie or credentials found. Please provide cookies or credentials.");
187
+ }
188
+
189
+ if (!api) {
190
+ api = {
191
+ setOptions: setOptionsFunc.bind(null, globalOptions),
192
+ getAppState() {
193
+ const appState = utils.getAppState(jar);
194
+ if (!Array.isArray(appState)) return [];
195
+ const uniqueAppState = appState.filter((item, index, self) => self.findIndex((t) => t.key === item.key) === index);
196
+ return uniqueAppState.length > 0 ? uniqueAppState : appState;
197
+ },
198
+ };
199
+ }
200
+
201
+ // Enable warm-up mode for fresh logins — activity ramps up gradually
202
+ // which mimics a human just starting to use the app.
203
+ try {
204
+ const { globalAntiSuspension } = require('../../utils/antiSuspension');
205
+ globalAntiSuspension.resetCircuitBreaker();
206
+ globalAntiSuspension.enableWarmup();
207
+ } catch (_) {}
208
+
209
+ const resp = await utils.get(fbLinkFunc(), jar, null, globalOptions, { noRef: true }).then(utils.saveCookies(jar));
210
+ const extractNetData = (html) => {
211
+ const allScriptsData = [];
212
+ const scriptRegex = /<script type="application\/json"[^>]*>(.*?)<\/script>/g;
213
+ let match;
214
+ while ((match = scriptRegex.exec(html)) !== null) {
215
+ try {
216
+ allScriptsData.push(JSON.parse(match[1]));
217
+ } catch (e) {
218
+ utils.error(`Failed to parse a JSON blob from HTML`, e.message);
219
+ }
220
+ }
221
+ return allScriptsData;
222
+ };
223
+
224
+ const netData = extractNetData(resp.body);
225
+
226
+ const [newCtx, newDefaultFuncs] = await buildAPIFunc(resp.body, jar, netData, globalOptions, fbLinkFunc, errorRetrievingMsg);
227
+ ctx = newCtx;
228
+ defaultFuncs = newDefaultFuncs;
229
+
230
+ const region = parseRegion(resp.body);
231
+ ctx.region = region;
232
+ utils.log("Detected Facebook region:", region);
233
+
234
+ try {
235
+ const { backupAppStateSQL } = require('../../database/appStateBackup');
236
+ await backupAppStateSQL(jar, ctx.userID);
237
+ } catch (backupErr) {
238
+ utils.warn("Failed to backup AppState to database:", backupErr.message);
239
+ }
240
+ api.message = new Map();
241
+ api.timestamp = {};
242
+
243
+ /**
244
+ * Loads API modules from the apis directory.
245
+ *
246
+ * @returns {void}
247
+ */
248
+ const loadApiModules = () => {
249
+ const apiPath = path.join(__dirname, '..', '..', 'apis');
250
+
251
+ if (!fs.existsSync(apiPath)) {
252
+ utils.error('API directory not found:', apiPath);
253
+ return;
254
+ }
255
+
256
+ const helperModules = ['mqttDeltaValue'];
257
+
258
+ fs.readdirSync(apiPath)
259
+ .filter(file => file.endsWith('.js'))
260
+ .forEach(file => {
261
+ const moduleName = path.basename(file, '.js');
262
+
263
+ if (helperModules.includes(moduleName)) {
264
+ return;
265
+ }
266
+
267
+ const fullPath = path.join(apiPath, file);
268
+ try {
269
+ const moduleExport = require(fullPath);
270
+ if (typeof moduleExport === 'function') {
271
+ api[moduleName] = moduleExport(defaultFuncs, api, ctx);
272
+ }
273
+ } catch (e) {
274
+ utils.error(`Failed to load API module ${moduleName}:`, e);
275
+ }
276
+ });
277
+ };
278
+
279
+ api.getCurrentUserID = () => ctx.userID;
280
+ api.getOptions = (key) => key ? globalOptions[key] : globalOptions;
281
+ loadApiModules();
282
+
283
+ if (api.nickname && typeof api.nickname === 'function') {
284
+ api.changeNickname = api.nickname;
285
+ }
286
+
287
+ try {
288
+ const models = require('../../database/models');
289
+ const threadDataModule = require('../../database/threadData');
290
+ const userDataModule = require('../../database/userData');
291
+
292
+ models.syncAll().then(() => {
293
+ utils.log("Database synchronized successfully");
294
+ }).catch(err => {
295
+ utils.warn("Failed to sync database:", err.message);
296
+ });
297
+
298
+ api.threadData = threadDataModule(api);
299
+ api.userData = userDataModule(api);
300
+ utils.log("Database methods initialized");
301
+ } catch (dbError) {
302
+ utils.warn("Database initialization failed (optional feature):", dbError.message);
303
+ }
304
+
305
+ api.ctx = ctx;
306
+ api.defaultFuncs = defaultFuncs;
307
+ api.globalOptions = globalOptions;
308
+
309
+ const { TokenRefreshManager } = require('../../utils/tokenRefresh');
310
+ if (api.tokenRefreshManager) {
311
+ api.tokenRefreshManager.stopAutoRefresh();
312
+ } else {
313
+ api.tokenRefreshManager = new TokenRefreshManager();
314
+ }
315
+
316
+ const { globalAutoReLoginManager } = require('../../utils/autoReLogin');
317
+
318
+ if (globalOptions.autoReLogin !== false) {
319
+ globalAutoReLoginManager.setCredentials(credentials, globalOptions, callback);
320
+ utils.log("AutoReLogin", "Auto re-login enabled with stored credentials");
321
+ // NOTE: startSessionMonitoring(api) is called later, after api.isSessionValid
322
+ // is registered, so the health-check interval can actually call it.
323
+ try {
324
+ const appState = api.getAppState();
325
+ globalAutoReLoginManager.updateAppState(appState);
326
+ } catch (_) {}
327
+
328
+ api.tokenRefreshManager.setSessionExpiryCallback((error) => {
329
+ utils.warn("TokenRefresh", "Session expiry detected. Triggering auto re-login...");
330
+ globalAutoReLoginManager.handleSessionExpiry(api, fbLinkFunc(), errorRetrievingMsg);
331
+ });
332
+
333
+ // Wire ctx.performAutoLogin so the axios response inspector can
334
+ // trigger re-login directly when it detects a login-redirect in any
335
+ // API response, without waiting for the next scheduled health check.
336
+ ctx.performAutoLogin = async () => {
337
+ try {
338
+ const result = await globalAutoReLoginManager.handleSessionExpiry(
339
+ api,
340
+ fbLinkFunc(),
341
+ errorRetrievingMsg
342
+ );
343
+ return result !== false;
344
+ } catch (_) {
345
+ return false;
346
+ }
347
+ };
348
+ }
349
+
350
+ api.logout = () => {
351
+ const logoutFn = require('../../apis/logout')(defaultFuncs, api, ctx);
352
+ return logoutFn();
353
+ };
354
+
355
+ // Graceful shutdown handler - clean up all resources
356
+ const cleanup = () => {
357
+ utils.log("Shutdown", "Cleaning up resources...");
358
+
359
+ // Stop token refresh
360
+ if (api.tokenRefreshManager) {
361
+ api.tokenRefreshManager.stopAutoRefresh();
362
+ }
363
+
364
+ // Stop session monitoring
365
+ if (globalAutoReLoginManager) {
366
+ globalAutoReLoginManager.stopSessionMonitoring();
367
+ globalAutoReLoginManager.disable();
368
+ }
369
+
370
+ // Stop MQTT
371
+ if (ctx.mqttClient && typeof ctx.mqttClient.end === 'function') {
372
+ try {
373
+ ctx.mqttClient.end(true);
374
+ } catch (_) {}
375
+ }
376
+
377
+ // Stop listening
378
+ if (ctx._emitter) {
379
+ ctx._emitter.removeAllListeners();
380
+ }
381
+
382
+ // Clear intervals
383
+ if (ctx._mqttWatchdog) clearInterval(ctx._mqttWatchdog);
384
+ if (ctx._tmsTimeout) clearTimeout(ctx._tmsTimeout);
385
+ if (ctx._autoCycleTimer) clearInterval(ctx._autoCycleTimer);
386
+ if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer);
387
+ if (ctx._periodicBackupInterval) clearInterval(ctx._periodicBackupInterval);
388
+
389
+ utils.log("Shutdown", "Cleanup complete");
390
+ };
391
+
392
+ // Register cleanup handlers using named functions so they can be removed
393
+ // if loginHelper is called again (e.g. after auto re-login), preventing
394
+ // accumulation of stale handlers across restarts.
395
+ if (!process._nkxfcaCleanupRegistered) {
396
+ process._nkxfcaCleanupRegistered = true;
397
+ process.on('exit', () => cleanup());
398
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
399
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
400
+ process.on('uncaughtException', (err) => {
401
+ utils.error("Uncaught Exception", err.message);
402
+ cleanup();
403
+ process.exit(1);
404
+ });
405
+ process.on('unhandledRejection', (reason) => {
406
+ utils.error("Unhandled Rejection", String(reason && reason.message ? reason.message : reason));
407
+ });
408
+ }
409
+
410
+ // Periodic cookie backup (every 15 min) — keeps the database in sync with
411
+ // the live cookie jar so a restart always loads the freshest cookies.
412
+ if (ctx._periodicBackupInterval) clearInterval(ctx._periodicBackupInterval);
413
+ ctx._periodicBackupInterval = setInterval(async () => {
414
+ try {
415
+ const { backupAppStateSQL } = require('../../database/appStateBackup');
416
+ await backupAppStateSQL(jar, ctx.userID);
417
+ } catch (_) {}
418
+ }, 15 * 60 * 1000);
419
+
420
+ api.tokenRefreshManager.startAutoRefresh(ctx, defaultFuncs, fbLinkFunc());
421
+
422
+ api.refreshTokens = () => api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, fbLinkFunc());
423
+ api.getTokenRefreshStatus = () => api.tokenRefreshManager.getStatus();
424
+ api.getHealthStatus = () => {
425
+ const mqttConnected = !!(ctx.mqttClient && ctx.mqttClient.connected);
426
+ const rateStats = (() => {
427
+ try {
428
+ const { getRateLimiterStats } = require('../../utils/rateLimiter');
429
+ return getRateLimiterStats();
430
+ } catch (_e) {
431
+ return null;
432
+ }
433
+ })();
434
+ return {
435
+ mqttConnected,
436
+ autoReconnect: !!ctx.globalOptions.autoReconnect,
437
+ tokenRefresh: {
438
+ lastRefresh: api.tokenRefreshManager.lastRefresh,
439
+ nextRefresh: api.tokenRefreshManager.getTimeUntilNextRefresh(),
440
+ failureCount: api.tokenRefreshManager.getFailureCount()
441
+ },
442
+ autoReLogin: {
443
+ enabled: globalAutoReLoginManager.isEnabled(),
444
+ sessionMonitoring: !!globalAutoReLoginManager.sessionMonitorInterval
445
+ },
446
+ rateLimiter: rateStats
447
+ };
448
+ };
449
+ api.enableAutoReLogin = (enable = true) => {
450
+ if (enable) {
451
+ globalAutoReLoginManager.setCredentials(credentials, globalOptions, callback);
452
+ } else {
453
+ globalAutoReLoginManager.disable();
454
+ }
455
+ };
456
+ api.isAutoReLoginEnabled = () => globalAutoReLoginManager.isEnabled();
457
+
458
+ api.isSessionValid = () => {
459
+ return new Promise(async (resolve) => {
460
+ try {
461
+ // Use the lightweight presence endpoint instead of fetching the
462
+ // full homepage (~400 kB). Returns 200 JSON when authenticated,
463
+ // 302→login when the session is expired.
464
+ //
465
+ // IMPORTANT: use _skipSessionInspect so the axios response
466
+ // inspector does NOT try to trigger performAutoLogin from inside
467
+ // this check — that would cause a reentrant re-login call.
468
+ const probeCtx = { noRef: true, _skipSessionInspect: true };
469
+ const resp = await utils.get(
470
+ 'https://www.facebook.com/ajax/presence/reconnect.php?reason=14&fb_dtsg_ag=&__a=1',
471
+ ctx.jar, null, ctx.globalOptions, probeCtx
472
+ );
473
+ const html = resp.body || '';
474
+
475
+ // Any redirect to /login indicates a dead session.
476
+ const isLoginPage = html.includes('<form id="login_form"') ||
477
+ html.includes('id="loginbutton"') ||
478
+ html.includes('"login_page"') ||
479
+ html.includes('id="email" name="email"') ||
480
+ html.includes('name="pass"') ||
481
+ html.includes('action="/login.php');
482
+ if (isLoginPage) return resolve(false);
483
+
484
+ const isCheckpoint = html.includes('"checkpoint"') && html.includes('"flow_type"');
485
+ if (isCheckpoint) {
486
+ try {
487
+ const { globalAntiSuspension } = require('../../utils/antiSuspension');
488
+ globalAntiSuspension.tripCircuitBreaker('checkpoint_detected', 60 * 60 * 1000);
489
+ } catch (_) {}
490
+ return resolve(false);
491
+ }
492
+
493
+ // Verify we have valid tokens in context — the presence endpoint
494
+ // returning a non-login page is sufficient proof the session is alive.
495
+ const hasValidTokens = !!(ctx.fb_dtsg && ctx.fb_dtsg.length > 10);
496
+ resolve(hasValidTokens);
497
+ } catch (error) {
498
+ const msg = error.message || String(error || '');
499
+ const code = error.code || '';
500
+
501
+ // Distinguish transient network errors from real auth failures.
502
+ // Network errors should NOT be treated as session expiry — the
503
+ // session is likely fine, just the network blipped.
504
+ const NETWORK_CODES = ['ECONNRESET','ETIMEDOUT','ECONNREFUSED','ENETUNREACH',
505
+ 'EHOSTUNREACH','EAI_AGAIN','ENOTFOUND','ESOCKETTIMEDOUT'];
506
+ const isNetworkErr = NETWORK_CODES.some(c => code === c || msg.includes(c)) ||
507
+ msg.includes('socket hang up') || msg.includes('network error') ||
508
+ msg.includes('connect ETIMEDOUT');
509
+ if (isNetworkErr) {
510
+ utils.warn("Session validation — network error (treating as valid, not triggering re-login):", msg);
511
+ return resolve('network_error');
512
+ }
513
+
514
+ utils.error("Session validation failed:", msg);
515
+ resolve(false);
516
+ }
517
+ });
518
+ };
519
+
520
+ // Start session monitoring now that api.isSessionValid is defined.
521
+ if (globalOptions.autoReLogin !== false) {
522
+ try {
523
+ const { globalAutoReLoginManager: arm } = require('../../utils/autoReLogin');
524
+ arm.startSessionMonitoring(api);
525
+ utils.log("AutoReLogin", "Session monitoring started");
526
+ } catch (_) {}
527
+ }
528
+
529
+ // Expose anti-suspension controls on the API object
530
+ try {
531
+ const { globalAntiSuspension } = require('../../utils/antiSuspension');
532
+ api.antiSuspension = {
533
+ getConfig: () => globalAntiSuspension.getConfig(),
534
+ getHealth: () => globalAntiSuspension.checkAccountHealth(null),
535
+ tripCircuitBreaker: (reason, ms) => globalAntiSuspension.tripCircuitBreaker(reason, ms),
536
+ resetCircuitBreaker: () => globalAntiSuspension.resetCircuitBreaker(),
537
+ isCircuitBreakerTripped: () => globalAntiSuspension.isCircuitBreakerTripped(),
538
+ getDailyStats: () => globalAntiSuspension.dailyStats,
539
+ getHourlyStats: () => globalAntiSuspension.hourlyBucket,
540
+ detectSignal: (text) => globalAntiSuspension.detectSuspensionSignal(text)
541
+ };
542
+ } catch (_) {}
543
+
544
+ // Start auto backup for session persistence
545
+ try {
546
+ const { startAutoBackup } = require('../../database/appStateBackup');
547
+ startAutoBackup(jar, ctx.userID, 5 * 60 * 1000); // Backup every 5 minutes
548
+ utils.log("AutoBackup", "Automatic session backup started");
549
+ } catch (backupErr) {
550
+ utils.warn("AutoBackup", "Failed to start auto backup:", backupErr.message);
551
+ }
552
+
553
+ api.validateSession = async () => {
554
+ const isValid = await api.isSessionValid();
555
+ if (!isValid) {
556
+ utils.warn("Session validation failed - session may be expired");
557
+ // Trigger token refresh which will handle session expiry
558
+ try {
559
+ await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
560
+ } catch (error) {
561
+ utils.error("Failed to refresh session:", error.message);
562
+ }
563
+ }
564
+ return isValid;
565
+ };
566
+
567
+ return callback(null, api);
568
+ } catch (error) {
569
+ utils.error("loginHelper", error.error || error);
570
+ return callback(error);
571
+ }
572
+ }
573
+
574
+ module.exports = loginHelper;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ const utils = require('../../utils');
4
+
5
+ /**
6
+ * Sets global options for the API.
7
+ *
8
+ * @param {object} globalOptions The global options object to modify.
9
+ * @param {object} [options={}] New options to apply.
10
+ * @returns {Promise<void>}
11
+ */
12
+ async function setOptions(globalOptions, options = {}) {
13
+ const optionHandlers = {
14
+ online: (value) => (globalOptions.online = Boolean(value)),
15
+ selfListen: (value) => (globalOptions.selfListen = Boolean(value)),
16
+ selfListenEvent: (value) => (globalOptions.selfListenEvent = value),
17
+ listenEvents: (value) => (globalOptions.listenEvents = Boolean(value)),
18
+ updatePresence: (value) => (globalOptions.updatePresence = Boolean(value)),
19
+ forceLogin: (value) => (globalOptions.forceLogin = Boolean(value)),
20
+ userAgent: (value) => (globalOptions.userAgent = value),
21
+ autoMarkDelivery: (value) => (globalOptions.autoMarkDelivery = Boolean(value)),
22
+ autoMarkRead: (value) => (globalOptions.autoMarkRead = Boolean(value)),
23
+ listenTyping: (value) => (globalOptions.listenTyping = Boolean(value)),
24
+ proxy(value) {
25
+ if (typeof value !== "string") {
26
+ delete globalOptions.proxy;
27
+ utils.setProxy();
28
+ } else {
29
+ globalOptions.proxy = value;
30
+ utils.setProxy(value);
31
+ }
32
+ },
33
+ autoReconnect: (value) => (globalOptions.autoReconnect = Boolean(value)),
34
+ emitReady: (value) => (globalOptions.emitReady = Boolean(value)),
35
+ randomUserAgent(value) {
36
+ globalOptions.randomUserAgent = Boolean(value);
37
+ if (value) {
38
+ globalOptions.userAgent = utils.randomUserAgent();
39
+ }
40
+ },
41
+ simulateTyping: (value) => (globalOptions.simulateTyping = Boolean(value)),
42
+ bypassRegion(value) {
43
+ if (value){
44
+ value = value.toUpperCase();
45
+ }
46
+ globalOptions.bypassRegion = value;
47
+ },
48
+ maxConcurrentRequests(value) {
49
+ if (typeof value === 'number') {
50
+ globalOptions.maxConcurrentRequests = Math.floor(value);
51
+ utils.configureRateLimiter({ maxConcurrentRequests: globalOptions.maxConcurrentRequests });
52
+ }
53
+ },
54
+ maxRequestsPerMinute(value) {
55
+ if (typeof value === 'number') {
56
+ globalOptions.maxRequestsPerMinute = Math.floor(value);
57
+ utils.configureRateLimiter({ maxRequestsPerMinute: globalOptions.maxRequestsPerMinute });
58
+ }
59
+ },
60
+ requestCooldownMs(value) {
61
+ if (typeof value === 'number') {
62
+ globalOptions.requestCooldownMs = Math.floor(value);
63
+ utils.configureRateLimiter({ requestCooldownMs: globalOptions.requestCooldownMs });
64
+ }
65
+ },
66
+ errorCacheTtlMs(value) {
67
+ if (typeof value === 'number') {
68
+ globalOptions.errorCacheTtlMs = Math.floor(value);
69
+ utils.configureRateLimiter({ errorCacheTtlMs: globalOptions.errorCacheTtlMs });
70
+ }
71
+ },
72
+ stealthMode(value) {
73
+ const enable = Boolean(value);
74
+ globalOptions.stealthMode = enable;
75
+ if (enable) {
76
+ globalOptions.updatePresence = false;
77
+ globalOptions.online = false;
78
+ globalOptions.simulateTyping = false;
79
+ utils.configureRateLimiter({ maxConcurrentRequests: 2, maxRequestsPerMinute: 60 });
80
+ }
81
+ }
82
+ };
83
+ Object.entries(options).forEach(([key, value]) => {
84
+ if (optionHandlers[key]) optionHandlers[key](value);
85
+ });
86
+ }
87
+
88
+ module.exports = setOptions;