@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,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;
|