@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.
- package/CHANGELOG.md +229 -0
- package/COOKIE_LOGIN.md +208 -0
- package/LICENSE +3 -0
- package/README.md +492 -0
- package/index.js +2 -0
- package/package.json +120 -0
- package/scripts/build-go.mjs +54 -0
- package/scripts/detect-platform.mjs +36 -0
- package/scripts/download-prebuilt.mjs +119 -0
- package/scripts/package.mjs +6 -0
- package/scripts/postinstall.mjs +113 -0
- package/src/apis/addExternalModule.js +24 -0
- package/src/apis/addUserToGroup.js +108 -0
- package/src/apis/changeAdminStatus.js +148 -0
- package/src/apis/changeArchivedStatus.js +61 -0
- package/src/apis/changeAvatar.js +103 -0
- package/src/apis/changeBio.js +69 -0
- package/src/apis/changeBlockedStatus.js +54 -0
- package/src/apis/changeGroupImage.js +136 -0
- package/src/apis/changeThreadColor.js +116 -0
- package/src/apis/changeThreadEmoji.js +53 -0
- package/src/apis/comment.js +207 -0
- package/src/apis/createAITheme.js +129 -0
- package/src/apis/createNewGroup.js +79 -0
- package/src/apis/createPoll.js +73 -0
- package/src/apis/deleteMessage.js +52 -0
- package/src/apis/deleteThread.js +52 -0
- package/src/apis/e2ee.js +170 -0
- package/src/apis/editMessage.js +78 -0
- package/src/apis/emoji.js +124 -0
- package/src/apis/fetchThemeData.js +82 -0
- package/src/apis/follow.js +81 -0
- package/src/apis/forwardMessage.js +52 -0
- package/src/apis/friend.js +243 -0
- package/src/apis/gcmember.js +122 -0
- package/src/apis/gcname.js +123 -0
- package/src/apis/gcrule.js +119 -0
- package/src/apis/getAccess.js +111 -0
- package/src/apis/getBotInfo.js +88 -0
- package/src/apis/getBotInitialData.js +43 -0
- package/src/apis/getFriendsList.js +79 -0
- package/src/apis/getMessage.js +423 -0
- package/src/apis/getTheme.js +95 -0
- package/src/apis/getThemeInfo.js +116 -0
- package/src/apis/getThreadHistory.js +239 -0
- package/src/apis/getThreadInfo.js +267 -0
- package/src/apis/getThreadList.js +232 -0
- package/src/apis/getThreadPictures.js +58 -0
- package/src/apis/getUserID.js +117 -0
- package/src/apis/getUserInfo.js +513 -0
- package/src/apis/getUserInfoV2.js +146 -0
- package/src/apis/handleMessageRequest.js +50 -0
- package/src/apis/httpGet.js +63 -0
- package/src/apis/httpPost.js +89 -0
- package/src/apis/httpPostFormData.js +69 -0
- package/src/apis/listenMqtt.js +1236 -0
- package/src/apis/listenSpeed.js +179 -0
- package/src/apis/logout.js +93 -0
- package/src/apis/markAsDelivered.js +47 -0
- package/src/apis/markAsRead.js +115 -0
- package/src/apis/markAsReadAll.js +40 -0
- package/src/apis/markAsSeen.js +70 -0
- package/src/apis/mqttDeltaValue.js +250 -0
- package/src/apis/muteThread.js +45 -0
- package/src/apis/nickname.js +132 -0
- package/src/apis/notes.js +163 -0
- package/src/apis/pinMessage.js +150 -0
- package/src/apis/produceMetaTheme.js +180 -0
- package/src/apis/realtime.js +182 -0
- package/src/apis/removeUserFromGroup.js +117 -0
- package/src/apis/resolvePhotoUrl.js +58 -0
- package/src/apis/searchForThread.js +154 -0
- package/src/apis/sendMessage.js +346 -0
- package/src/apis/sendMessageMqtt.js +248 -0
- package/src/apis/sendTypingIndicator.js +105 -0
- package/src/apis/setMessageReaction.js +38 -0
- package/src/apis/setMessageReactionMqtt.js +61 -0
- package/src/apis/setThreadTheme.js +260 -0
- package/src/apis/setThreadThemeMqtt.js +94 -0
- package/src/apis/share.js +107 -0
- package/src/apis/shareContact.js +66 -0
- package/src/apis/stickers.js +257 -0
- package/src/apis/story.js +181 -0
- package/src/apis/theme.js +233 -0
- package/src/apis/unfriend.js +47 -0
- package/src/apis/unsendMessage.js +25 -0
- package/src/database/appStateBackup.js +298 -0
- package/src/database/models/index.js +56 -0
- package/src/database/models/thread.js +31 -0
- package/src/database/models/user.js +32 -0
- package/src/database/threadData.js +101 -0
- package/src/database/userData.js +90 -0
- package/src/e2ee/bridge.js +275 -0
- package/src/e2ee/index.js +60 -0
- package/src/engine/client.js +95 -0
- package/src/engine/models/buildAPI.js +152 -0
- package/src/engine/models/loginHelper.js +574 -0
- package/src/engine/models/setOptions.js +88 -0
- package/src/types/index.d.ts +574 -0
- package/src/utils/antiSuspension.js +529 -0
- package/src/utils/auth-helpers.js +149 -0
- package/src/utils/autoReLogin.js +336 -0
- package/src/utils/axios.js +436 -0
- package/src/utils/cache.js +54 -0
- package/src/utils/clients.js +282 -0
- package/src/utils/constants.js +410 -0
- package/src/utils/formatters/data/formatAttachment.js +370 -0
- package/src/utils/formatters/data/formatDelta.js +109 -0
- package/src/utils/formatters/index.js +159 -0
- package/src/utils/formatters/value/formatCookie.js +91 -0
- package/src/utils/formatters/value/formatDate.js +36 -0
- package/src/utils/formatters/value/formatID.js +16 -0
- package/src/utils/formatters.js +1373 -0
- package/src/utils/headers.js +235 -0
- package/src/utils/index.js +153 -0
- package/src/utils/monitoring.js +333 -0
- package/src/utils/rateLimiter.js +319 -0
- package/src/utils/tokenRefresh.js +680 -0
- package/src/utils/user-agents.js +238 -0
- 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
|
+
};
|