@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,680 @@
1
+ "use strict";
2
+
3
+ const utils = require('./index');
4
+ const { globalAutoReLoginManager } = require('./autoReLogin');
5
+
6
+ /**
7
+ * Token Refresh Manager - Enhanced for Maximum Reliability
8
+ * Automatically refreshes fb_dtsg, lsd, and other tokens to prevent expiration
9
+ */
10
+
11
+ class TokenRefreshManager {
12
+ constructor() {
13
+ this.refreshInterval = null;
14
+ this.REFRESH_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours base
15
+ this.SESSION_CHECK_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes - more frequent health checks
16
+ this.PRESENCE_KEEPALIVE_MS = 5 * 60 * 1000; // 5 minutes - frequent keepalive
17
+ this.COOKIE_REFRESH_INTERVAL_MS = 12 * 60 * 60 * 1000; // 12 hours - twice daily cookie refresh
18
+ this.lastRefresh = Date.now();
19
+ this.lastSessionCheck = Date.now();
20
+ this.lastPresencePing = Date.now();
21
+ this.lastCookieRefresh = Date.now();
22
+ this.failureCount = 0;
23
+ this.MAX_FAILURES = 15; // More tolerance for long-running bots
24
+ this.onSessionExpiry = null;
25
+ this.sessionHealthCheckInterval = null;
26
+ this.presenceKeepaliveInterval = null;
27
+ this.cookieRefreshInterval = null;
28
+ // Lock mechanism to prevent concurrent refresh attempts
29
+ this.isRefreshing = false;
30
+ this.refreshLock = null;
31
+ // Track consecutive successes for adaptive intervals
32
+ this.consecutiveSuccesses = 0;
33
+ // Store context for later use
34
+ this.storedCtx = null;
35
+ this.storedDefaultFuncs = null;
36
+ this.storedFbLink = null;
37
+ // Token cache to minimize requests
38
+ this.tokenCache = new Map();
39
+ }
40
+
41
+ /**
42
+ * Acquire refresh lock to prevent concurrent token refresh attempts
43
+ * @returns {Promise<boolean>} true if lock acquired, false if already locked
44
+ */
45
+ async acquireRefreshLock() {
46
+ if (this.isRefreshing) {
47
+ // Wait for existing refresh to complete
48
+ try {
49
+ await this.refreshLock;
50
+ } catch (_) {
51
+ // Previous refresh failed, we can proceed
52
+ }
53
+ }
54
+
55
+ if (this.isRefreshing) {
56
+ return false; // Still refreshing after waiting
57
+ }
58
+
59
+ this.isRefreshing = true;
60
+ this.refreshLock = new Promise((resolve) => {
61
+ this._releaseLock = resolve;
62
+ });
63
+ return true;
64
+ }
65
+
66
+ /**
67
+ * Release the refresh lock
68
+ */
69
+ releaseRefreshLock() {
70
+ this.isRefreshing = false;
71
+ if (this._releaseLock) {
72
+ this._releaseLock();
73
+ this._releaseLock = null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Start automatic token refresh with optimized intervals
79
+ * @param {Object} ctx - Application context
80
+ * @param {Object} defaultFuncs - Default functions
81
+ * @param {string} fbLink - Facebook link
82
+ */
83
+ startAutoRefresh(ctx, defaultFuncs, fbLink) {
84
+ // Store context for later recovery
85
+ this.storedCtx = ctx;
86
+ this.storedDefaultFuncs = defaultFuncs;
87
+ this.storedFbLink = fbLink;
88
+
89
+ if (this.refreshInterval) {
90
+ clearInterval(this.refreshInterval);
91
+ this.refreshInterval = null;
92
+ }
93
+ if (this.sessionHealthCheckInterval) {
94
+ clearInterval(this.sessionHealthCheckInterval);
95
+ this.sessionHealthCheckInterval = null;
96
+ }
97
+ if (this.presenceKeepaliveInterval) {
98
+ clearInterval(this.presenceKeepaliveInterval);
99
+ this.presenceKeepaliveInterval = null;
100
+ }
101
+ if (this.cookieRefreshInterval) {
102
+ clearInterval(this.cookieRefreshInterval);
103
+ this.cookieRefreshInterval = null;
104
+ }
105
+
106
+ const scheduleNext = () => {
107
+ // Adaptive interval: longer after consecutive successes
108
+ const base = this.REFRESH_INTERVAL_MS;
109
+ const successBonus = Math.min(this.consecutiveSuccesses * 15 * 60 * 1000, 4 * 60 * 60 * 1000); // Up to 4 hours bonus
110
+ const interval = base + successBonus;
111
+
112
+ this.refreshInterval = setTimeout(async () => {
113
+ try {
114
+ const refreshed = await this.refreshTokens(ctx, defaultFuncs, fbLink);
115
+ if (refreshed) {
116
+ this.consecutiveSuccesses++;
117
+ utils.log("TokenRefresh", `Tokens refreshed successfully (consecutive successes: ${this.consecutiveSuccesses})`);
118
+ } else {
119
+ this.consecutiveSuccesses = 0;
120
+ }
121
+ } catch (error) {
122
+ this.consecutiveSuccesses = 0;
123
+ utils.error("TokenRefresh", "Failed to refresh tokens:", error.message);
124
+ } finally {
125
+ scheduleNext();
126
+ }
127
+ }, interval);
128
+ utils.log("TokenRefresh", `Auto-refresh scheduled in ${Math.round(interval / 60000)}min`);
129
+ };
130
+
131
+ // Start session health checks - frequent to catch issues early
132
+ this.sessionHealthCheckInterval = setInterval(async () => {
133
+ try {
134
+ const isHealthy = await this.checkSessionHealth(ctx, defaultFuncs, fbLink);
135
+ if (isHealthy === 'network_error') {
136
+ utils.warn("TokenRefresh", "Session health probe returned network error — skipping refresh (transient)");
137
+ return;
138
+ }
139
+ if (!isHealthy) {
140
+ utils.warn("TokenRefresh", "Session health check failed, triggering refresh");
141
+ this.consecutiveSuccesses = 0;
142
+ const refreshed = await this.refreshTokens(ctx, defaultFuncs, fbLink);
143
+ if (!refreshed) {
144
+ // Try alternative refresh method
145
+ await this.refreshTokensAlternative(ctx, defaultFuncs, fbLink);
146
+ }
147
+ }
148
+ } catch (error) {
149
+ utils.error("TokenRefresh", "Session health check error:", error.message);
150
+ }
151
+ }, this.SESSION_CHECK_INTERVAL_MS);
152
+
153
+ // Start presence keepalive for low-activity bots
154
+ this.presenceKeepaliveInterval = setInterval(async () => {
155
+ try {
156
+ await this.sendPresenceKeepalive(ctx);
157
+ } catch (error) {
158
+ utils.warn("TokenRefresh", "Presence keepalive failed:", error.message);
159
+ }
160
+ }, this.PRESENCE_KEEPALIVE_MS);
161
+
162
+ // Start cookie refresh interval
163
+ this.cookieRefreshInterval = setInterval(async () => {
164
+ try {
165
+ await this.refreshCookies(ctx, defaultFuncs, fbLink);
166
+ } catch (error) {
167
+ utils.warn("TokenRefresh", "Cookie refresh failed:", error.message);
168
+ }
169
+ }, this.COOKIE_REFRESH_INTERVAL_MS);
170
+
171
+ utils.log("TokenRefresh", `Session health checks every ${Math.round(this.SESSION_CHECK_INTERVAL_MS / 60000)}min, keepalive every ${Math.round(this.PRESENCE_KEEPALIVE_MS / 60000)}min, cookie refresh every 12h`);
172
+
173
+ scheduleNext();
174
+ }
175
+
176
+ /**
177
+ * Check if session is healthy using a lightweight AJAX ping instead of a
178
+ * full homepage load. Fetching the full homepage every 2 hours is a clear
179
+ * automation fingerprint; a small presence/ping endpoint is far less
180
+ * conspicuous and produces a much smaller response.
181
+ *
182
+ * @param {Object} ctx - Application context
183
+ * @param {Object} defaultFuncs - Default functions
184
+ * @param {string} fbLink - Facebook link
185
+ * @returns {Promise<boolean>}
186
+ */
187
+ async checkSessionHealth(ctx, defaultFuncs, fbLink) {
188
+ try {
189
+ // Use a lightweight AJAX endpoint that only returns a small JSON
190
+ // payload — not the full multi-megabyte homepage.
191
+ const probeUrl = 'https://www.facebook.com/ajax/presence/reconnect.php';
192
+ const probeCtx = { ...ctx, _skipSessionInspect: true };
193
+ const resp = await utils.get(
194
+ probeUrl,
195
+ ctx.jar,
196
+ { reason: 'reconnect', __a: 1, __req: 'probe' },
197
+ ctx.globalOptions,
198
+ probeCtx
199
+ );
200
+
201
+ const body = resp.body;
202
+ if (!body) return false;
203
+
204
+ const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
205
+
206
+ // If we get a login-page redirect in the probe response the session is gone
207
+ const isLoginPage =
208
+ bodyStr.includes('<form id="login_form"') ||
209
+ bodyStr.includes('"login_page"') ||
210
+ bodyStr.includes('id="loginbutton"');
211
+
212
+ if (isLoginPage) return false;
213
+
214
+ const isCheckpoint =
215
+ bodyStr.includes('"checkpoint"') && bodyStr.includes('"flow_type"');
216
+ if (isCheckpoint) return false;
217
+
218
+ this.lastSessionCheck = Date.now();
219
+ return true;
220
+ } catch (error) {
221
+ const msg = error.message || '';
222
+ const code = error.code || '';
223
+ const NETWORK_CODES = ['ECONNRESET','ETIMEDOUT','ECONNREFUSED','ENETUNREACH',
224
+ 'EHOSTUNREACH','EAI_AGAIN','ENOTFOUND','ESOCKETTIMEDOUT'];
225
+ const isNetworkErr = NETWORK_CODES.some(c => code === c || msg.includes(c)) ||
226
+ msg.includes('socket hang up') || msg.includes('network error');
227
+ if (isNetworkErr) {
228
+ utils.warn("TokenRefresh", "Session health check: network error (ignoring):", msg);
229
+ // Return a sentinel so callers can skip refresh instead of triggering re-login.
230
+ return 'network_error';
231
+ }
232
+ if (msg.includes('Not logged in') || msg.includes('Session has expired')) {
233
+ utils.error("TokenRefresh", "Session health check: session expired:", msg);
234
+ return false;
235
+ }
236
+ utils.warn("TokenRefresh", "Session health check unexpected error (treating as healthy):", msg);
237
+ return true;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Manually refresh tokens with retry logic
243
+ * @param {Object} ctx - Application context
244
+ * @param {Object} defaultFuncs - Default functions
245
+ * @param {string} fbLink - Facebook link
246
+ * @param {number} retryCount - Current retry attempt (internal use)
247
+ * @returns {Promise<boolean>}
248
+ */
249
+ async refreshTokens(ctx, defaultFuncs, fbLink, retryCount = 0) {
250
+ // Acquire lock to prevent concurrent refresh attempts
251
+ const lockAcquired = await this.acquireRefreshLock();
252
+ if (!lockAcquired) {
253
+ utils.warn("TokenRefresh", "Token refresh already in progress, skipping concurrent request");
254
+ return false;
255
+ }
256
+
257
+ const MAX_RETRIES = 3;
258
+ const RETRY_DELAYS = [2000, 5000, 10000];
259
+
260
+ try {
261
+ // Validate ctx.jar exists
262
+ if (!ctx || !ctx.jar) {
263
+ throw new Error("Invalid context: cookie jar not available");
264
+ }
265
+
266
+ const resp = await utils.get(fbLink, ctx.jar, null, ctx.globalOptions, { noRef: true });
267
+
268
+ const html = resp.body;
269
+ if (!html) {
270
+ throw new Error("Empty response from Facebook");
271
+ }
272
+
273
+ // Precise check - broad html.includes("login") is a false positive because
274
+ // Facebook includes the word "login" all over authenticated pages too.
275
+ const isLoginPage = html.includes('<form id="login_form"') ||
276
+ html.includes('id="loginbutton"') ||
277
+ html.includes('"login_page"') ||
278
+ html.includes('id="email" name="email"');
279
+ const isCheckpoint = html.includes('"checkpoint"') && html.includes('"flow_type"');
280
+
281
+ if (isLoginPage || isCheckpoint) {
282
+ if (isCheckpoint) {
283
+ try {
284
+ const { globalAntiSuspension } = require('./antiSuspension');
285
+ globalAntiSuspension.tripCircuitBreaker('checkpoint_on_token_refresh', 60 * 60 * 1000);
286
+ } catch (lockErr) {
287
+ utils.warn("TokenRefresh", "Failed to trip circuit breaker for checkpoint:", lockErr.message);
288
+ }
289
+ }
290
+ throw new Error("Session expired or checkpoint required");
291
+ }
292
+
293
+ const dtsgMatch = html.match(/"DTSGInitialData",\[],{"token":"([^"]+)"/);
294
+ if (dtsgMatch) {
295
+ ctx.fb_dtsg = dtsgMatch[1];
296
+ ctx.ttstamp = "2";
297
+ for (let i = 0; i < ctx.fb_dtsg.length; i++) {
298
+ ctx.ttstamp += ctx.fb_dtsg.charCodeAt(i);
299
+ }
300
+ } else {
301
+ throw new Error("Failed to extract fb_dtsg token");
302
+ }
303
+
304
+ const lsdMatch = html.match(/"LSD",\[],{"token":"([^"]+)"/);
305
+ if (lsdMatch) {
306
+ ctx.lsd = lsdMatch[1];
307
+ }
308
+
309
+ const jazoestMatch = html.match(/jazoest=(\d+)/);
310
+ if (jazoestMatch) {
311
+ ctx.jazoest = jazoestMatch[1];
312
+ }
313
+
314
+ const revisionMatch = html.match(/"client_revision":(\d+)/);
315
+ if (revisionMatch) {
316
+ ctx.__rev = revisionMatch[1];
317
+ }
318
+
319
+ // Extract additional tokens for better session persistence
320
+ const dtsgAgMatch = html.match(/"DTSGAGInitialData",\[],{"token":"([^"]+)"/);
321
+ if (dtsgAgMatch) {
322
+ ctx.fb_dtsg_ag = dtsgAgMatch[1];
323
+ }
324
+
325
+ const spinRMatch = html.match(/"__spin_r":(\d+)/);
326
+ if (spinRMatch) {
327
+ ctx.__spin_r = spinRMatch[1];
328
+ }
329
+
330
+ const spinBMatch = html.match(/"__spin_b":"([^"]+)"/);
331
+ if (spinBMatch) {
332
+ ctx.__spin_b = spinBMatch[1];
333
+ }
334
+
335
+ const spinTMatch = html.match(/"__spin_t":(\d+)/);
336
+ if (spinTMatch) {
337
+ ctx.__spin_t = spinTMatch[1];
338
+ }
339
+
340
+ const hsiMatch = html.match(/"hsi":"(\d+)"/);
341
+ if (hsiMatch) {
342
+ ctx.hsi = hsiMatch[1];
343
+ }
344
+
345
+ const dynMatch = html.match(/"dyn":"([^"]+)"/);
346
+ if (dynMatch) {
347
+ ctx.dyn = dynMatch[1];
348
+ }
349
+
350
+ const csrMatch = html.match(/"csr":"([^"]+)"/);
351
+ if (csrMatch) {
352
+ ctx.csr = csrMatch[1];
353
+ }
354
+
355
+ this.lastRefresh = Date.now();
356
+ this.failureCount = 0;
357
+ try {
358
+ if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
359
+ const appState = utils.getAppState(ctx.jar);
360
+ globalAutoReLoginManager.updateAppState(appState);
361
+ }
362
+ } catch (updateErr) {
363
+ utils.warn("TokenRefresh", "Failed to update appState in re-login manager:", updateErr.message);
364
+ }
365
+ // Persist the refreshed cookies to the database so that a bot
366
+ // restart after a token refresh doesn't load stale cookies.
367
+ try {
368
+ const { backupAppStateSQL } = require('../database/appStateBackup');
369
+ await backupAppStateSQL(ctx.jar, ctx.userID);
370
+ utils.log("TokenRefresh", "AppState persisted to database after token refresh");
371
+ } catch (backupErr) {
372
+ utils.warn("TokenRefresh", "Failed to persist AppState after token refresh:", backupErr.message);
373
+ }
374
+ return true;
375
+ } catch (error) {
376
+ this.failureCount++;
377
+ utils.error("TokenRefresh", `Refresh failed (attempt ${retryCount + 1}/${MAX_RETRIES + 1}):`, error.message);
378
+
379
+ if (this.failureCount >= this.MAX_FAILURES) {
380
+ utils.error("TokenRefresh", `Maximum failures (${this.MAX_FAILURES}) reached. Session may be expired.`);
381
+ if (this.onSessionExpiry && typeof this.onSessionExpiry === 'function') {
382
+ this.onSessionExpiry(error);
383
+ }
384
+ return false;
385
+ }
386
+
387
+ if (retryCount < MAX_RETRIES) {
388
+ const delay = RETRY_DELAYS[retryCount];
389
+ utils.log("TokenRefresh", `Retrying in ${delay}ms...`);
390
+ // Release the lock BEFORE the recursive call — the inner call
391
+ // needs to acquire it and would deadlock waiting on a lock held
392
+ // by this call (which in turn is blocked awaiting the inner call).
393
+ this.releaseRefreshLock();
394
+ await new Promise(resolve => setTimeout(resolve, delay));
395
+ return await this.refreshTokens(ctx, defaultFuncs, fbLink, retryCount + 1);
396
+ }
397
+
398
+ return false;
399
+ } finally {
400
+ // Always release the lock
401
+ this.releaseRefreshLock();
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Alternative token refresh using different endpoints - fallback method
407
+ * @param {Object} ctx - Application context
408
+ * @param {Object} defaultFuncs - Default functions
409
+ * @param {string} fbLink - Facebook link
410
+ * @returns {Promise<boolean>}
411
+ */
412
+ async refreshTokensAlternative(ctx, defaultFuncs, fbLink) {
413
+ const endpoints = [
414
+ 'https://www.facebook.com/ajax/bootloader-endpoint/',
415
+ 'https://www.facebook.com/ajax/navigation/',
416
+ 'https://www.messenger.com/',
417
+ 'https://www.facebook.com/messages/'
418
+ ];
419
+
420
+ for (const endpoint of endpoints) {
421
+ try {
422
+ utils.log("TokenRefresh", `Trying alternative endpoint: ${endpoint}`);
423
+ const resp = await utils.get(endpoint, ctx.jar, { __a: 1 }, ctx.globalOptions, { noRef: true, _skipSessionInspect: true });
424
+ const html = resp.body;
425
+
426
+ if (!html || typeof html !== 'string') continue;
427
+
428
+ // Check for login page
429
+ if (html.includes('<form id="login_form"') || html.includes('id="loginbutton"')) {
430
+ continue; // Try next endpoint
431
+ }
432
+
433
+ // Try to extract tokens with multiple patterns
434
+ const tokens = this.extractTokensFromHtml(html);
435
+ if (tokens.fb_dtsg) {
436
+ ctx.fb_dtsg = tokens.fb_dtsg;
437
+ ctx.ttstamp = "2";
438
+ for (let i = 0; i < ctx.fb_dtsg.length; i++) {
439
+ ctx.ttstamp += ctx.fb_dtsg.charCodeAt(i);
440
+ }
441
+ }
442
+ if (tokens.lsd) ctx.lsd = tokens.lsd;
443
+ if (tokens.jazoest) ctx.jazoest = tokens.jazoest;
444
+ if (tokens.__rev) ctx.__rev = tokens.__rev;
445
+
446
+ if (tokens.fb_dtsg) {
447
+ utils.log("TokenRefresh", `Alternative refresh successful via ${endpoint}`);
448
+ this.lastRefresh = Date.now();
449
+ this.failureCount = 0;
450
+
451
+ // Persist tokens
452
+ try {
453
+ const { backupAppStateSQL } = require('../database/appStateBackup');
454
+ await backupAppStateSQL(ctx.jar, ctx.userID);
455
+ } catch (_) {}
456
+
457
+ return true;
458
+ }
459
+ } catch (error) {
460
+ utils.warn("TokenRefresh", `Alternative endpoint ${endpoint} failed:`, error.message);
461
+ continue;
462
+ }
463
+ }
464
+
465
+ return false;
466
+ }
467
+
468
+ /**
469
+ * Extract all tokens from HTML using multiple regex patterns
470
+ * @param {string} html - HTML content
471
+ * @returns {Object} Extracted tokens
472
+ */
473
+ extractTokensFromHtml(html) {
474
+ const tokens = {};
475
+
476
+ // DTSG patterns
477
+ const dtsgPatterns = [
478
+ /"DTSGInitialData",\[],{"token":"([^"]+)"/,
479
+ /name="fb_dtsg" value="([^"]+)"/,
480
+ /"token":"([^"]+)"[^}]*"DTSGInitialData"/,
481
+ /DTSG[^}]*token[^"]*"([^"]+)"/
482
+ ];
483
+
484
+ for (const pattern of dtsgPatterns) {
485
+ const match = html.match(pattern);
486
+ if (match && match[1]) {
487
+ tokens.fb_dtsg = match[1];
488
+ break;
489
+ }
490
+ }
491
+
492
+ // LSD patterns
493
+ const lsdPatterns = [
494
+ /"LSD",\[],{"token":"([^"]+)"/,
495
+ /name="lsd" value="([^"]+)"/
496
+ ];
497
+
498
+ for (const pattern of lsdPatterns) {
499
+ const match = html.match(pattern);
500
+ if (match && match[1]) {
501
+ tokens.lsd = match[1];
502
+ break;
503
+ }
504
+ }
505
+
506
+ // Jazoest
507
+ const jazoestMatch = html.match(/jazoest=(\d+)/) || html.match(/name="jazoest" value="(\d+)/);
508
+ if (jazoestMatch) tokens.jazoest = jazoestMatch[1];
509
+
510
+ // Revision
511
+ const revMatch = html.match(/"client_revision":(\d+)/) || html.match(/"__rev":(\d+)/);
512
+ if (revMatch) tokens.__rev = revMatch[1];
513
+
514
+ // Additional tokens
515
+ const hsiMatch = html.match(/"hsi":"(\d+)"/);
516
+ if (hsiMatch) tokens.hsi = hsiMatch[1];
517
+
518
+ const dynMatch = html.match(/"dyn":"([^"]+)"/);
519
+ if (dynMatch) tokens.dyn = dynMatch[1];
520
+
521
+ const csrMatch = html.match(/"csr":"([^"]+)"/);
522
+ if (csrMatch) tokens.csr = csrMatch[1];
523
+
524
+ return tokens;
525
+ }
526
+
527
+ /**
528
+ * Send a lightweight presence keepalive ping to prevent session expiration
529
+ * due to inactivity for low-activity bots.
530
+ * @param {Object} ctx - Application context
531
+ */
532
+ async sendPresenceKeepalive(ctx) {
533
+ try {
534
+ // Use the same lightweight endpoint as session health check
535
+ const probeUrl = 'https://www.facebook.com/ajax/presence/reconnect.php';
536
+ const resp = await utils.get(
537
+ probeUrl,
538
+ ctx.jar,
539
+ { reason: 'keepalive', __a: 1, __req: 'keepalive' },
540
+ ctx.globalOptions,
541
+ { noRef: true, _skipSessionInspect: true }
542
+ );
543
+ this.lastPresencePing = Date.now();
544
+ return true;
545
+ } catch (error) {
546
+ // Silent failure - don't spam logs for keepalive failures
547
+ return false;
548
+ }
549
+ }
550
+
551
+ /**
552
+ * Refresh cookies to extend session lifetime
553
+ * @param {Object} ctx - Application context
554
+ * @param {Object} defaultFuncs - Default functions
555
+ * @param {string} fbLink - Facebook link
556
+ */
557
+ async refreshCookies(ctx, defaultFuncs, fbLink) {
558
+ try {
559
+ // Visit a lightweight page to refresh cookies
560
+ const probeUrl = 'https://www.facebook.com/ajax/bootloader-endpoint/';
561
+ const probeCtx = { ...ctx, _skipSessionInspect: true };
562
+ await utils.get(
563
+ probeUrl,
564
+ ctx.jar,
565
+ { __a: 1, __req: 'cookierefresh' },
566
+ ctx.globalOptions,
567
+ probeCtx
568
+ );
569
+
570
+ this.lastCookieRefresh = Date.now();
571
+ utils.log("TokenRefresh", "Cookies refreshed successfully");
572
+
573
+ // Persist updated cookies
574
+ try {
575
+ const { backupAppStateSQL } = require('../database/appStateBackup');
576
+ await backupAppStateSQL(ctx.jar, ctx.userID);
577
+ } catch (backupErr) {
578
+ utils.warn("TokenRefresh", "Failed to persist refreshed cookies:", backupErr.message);
579
+ }
580
+
581
+ return true;
582
+ } catch (error) {
583
+ utils.warn("TokenRefresh", "Cookie refresh failed:", error.message);
584
+ return false;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Stop automatic token refresh
590
+ */
591
+ stopAutoRefresh() {
592
+ if (this.refreshInterval) {
593
+ clearTimeout(this.refreshInterval);
594
+ this.refreshInterval = null;
595
+ utils.log("TokenRefresh", "Auto-refresh disabled");
596
+ }
597
+ if (this.sessionHealthCheckInterval) {
598
+ clearInterval(this.sessionHealthCheckInterval);
599
+ this.sessionHealthCheckInterval = null;
600
+ utils.log("TokenRefresh", "Session health checks disabled");
601
+ }
602
+ if (this.presenceKeepaliveInterval) {
603
+ clearInterval(this.presenceKeepaliveInterval);
604
+ this.presenceKeepaliveInterval = null;
605
+ utils.log("TokenRefresh", "Presence keepalive disabled");
606
+ }
607
+ if (this.cookieRefreshInterval) {
608
+ clearInterval(this.cookieRefreshInterval);
609
+ this.cookieRefreshInterval = null;
610
+ utils.log("TokenRefresh", "Cookie refresh disabled");
611
+ }
612
+ }
613
+
614
+ /**
615
+ * Get time until next refresh
616
+ * @returns {number} Milliseconds until next refresh
617
+ */
618
+ getTimeUntilNextRefresh() {
619
+ if (!this.refreshInterval) return -1;
620
+ return Math.max(0, this.REFRESH_INTERVAL_MS - (Date.now() - this.lastRefresh));
621
+ }
622
+
623
+ /**
624
+ * Check if tokens need immediate refresh
625
+ * @returns {boolean}
626
+ */
627
+ needsImmediateRefresh() {
628
+ return (Date.now() - this.lastRefresh) >= this.REFRESH_INTERVAL_MS;
629
+ }
630
+
631
+ /**
632
+ * Set callback for session expiry detection
633
+ * @param {Function} callback - Callback function to trigger on session expiry
634
+ */
635
+ setSessionExpiryCallback(callback) {
636
+ this.onSessionExpiry = callback;
637
+ }
638
+
639
+ /**
640
+ * Reset failure count (useful after successful re-login)
641
+ */
642
+ resetFailureCount() {
643
+ this.failureCount = 0;
644
+ }
645
+
646
+ /**
647
+ * Get current failure count
648
+ * @returns {number}
649
+ */
650
+ getFailureCount() {
651
+ return this.failureCount;
652
+ }
653
+
654
+ /**
655
+ * Get comprehensive status for monitoring and debugging
656
+ * @returns {Object}
657
+ */
658
+ getStatus() {
659
+ const now = Date.now();
660
+ return {
661
+ lastRefresh: this.lastRefresh,
662
+ lastSessionCheck: this.lastSessionCheck,
663
+ lastPresencePing: this.lastPresencePing,
664
+ timeSinceLastRefresh: now - this.lastRefresh,
665
+ timeSinceLastSessionCheck: now - this.lastSessionCheck,
666
+ timeSinceLastPresencePing: now - this.lastPresencePing,
667
+ nextRefreshIn: this.getTimeUntilNextRefresh(),
668
+ failureCount: this.failureCount,
669
+ maxFailures: this.MAX_FAILURES,
670
+ isHealthy: this.failureCount < this.MAX_FAILURES,
671
+ refreshIntervalMs: this.REFRESH_INTERVAL_MS,
672
+ sessionCheckIntervalMs: this.SESSION_CHECK_INTERVAL_MS,
673
+ presenceKeepaliveIntervalMs: this.PRESENCE_KEEPALIVE_MS
674
+ };
675
+ }
676
+ }
677
+
678
+ module.exports = {
679
+ TokenRefreshManager
680
+ };