hridoy-ultimate 9.0.2 → 9.0.3
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/module/config.js +10 -2
- package/module/login.js +15 -5
- package/module/loginHelper.js +25 -7
- package/package.json +8 -5
- package/src/api/action/enableAutoSaveAppState.js +8 -5
- package/src/api/socket/core/connectMqtt.js +18 -1
- package/src/api/socket/core/getSeqID.js +13 -2
- package/src/api/socket/listenMqtt.js +24 -15
- package/src/utils/fingerprint.js +75 -36
- package/src/utils/loginParser/parseAndCheckLogin.js +25 -18
package/module/config.js
CHANGED
|
@@ -2,13 +2,21 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const logger = require("../func/logger");
|
|
4
4
|
const defaultConfig = {
|
|
5
|
-
autoUpdate:
|
|
5
|
+
autoUpdate: false,
|
|
6
|
+
// Opt-in: tell the user when a newer npm version exists (one small request per login).
|
|
7
|
+
checkForUpdates: false,
|
|
8
|
+
// Opt-in: when Facebook shows its "automated behavior" warning, press Dismiss for the
|
|
9
|
+
// user. Off by default: silently clicking through Facebook's own bot warning is itself
|
|
10
|
+
// an automated action on the account. Dismiss it yourself in the browser instead.
|
|
11
|
+
autoDismissAutomationWarning: false,
|
|
6
12
|
mqtt: { enabled: true, reconnectInterval: 3600 },
|
|
7
13
|
// Opt-in only: auto re-login with credentials (via the third-party API)
|
|
8
14
|
// after a checkpoint/session failure is itself a strong suspicious-login
|
|
9
15
|
// signal and invalidates every other live session on the account.
|
|
10
16
|
autoLogin: false,
|
|
11
|
-
|
|
17
|
+
// No default third-party host: your e-mail/password must never be sent anywhere you
|
|
18
|
+
// did not choose. autoLogin stays off unless you set both autoLogin and apiServer.
|
|
19
|
+
apiServer: "",
|
|
12
20
|
apiKey: "",
|
|
13
21
|
credentials: { email: "", password: "", twofactor: "" },
|
|
14
22
|
antiGetInfo: {
|
package/module/login.js
CHANGED
|
@@ -11,7 +11,12 @@ global.fca = { config };
|
|
|
11
11
|
|
|
12
12
|
// Global error handlers to prevent bot crashes
|
|
13
13
|
// Handle unhandled promise rejections (e.g., fetch timeouts, network errors)
|
|
14
|
-
|
|
14
|
+
// A library must not own the process: the host app (the bot) installs its own handlers and
|
|
15
|
+
// decides what a crash means. These stay as a fallback ONLY for plain scripts that have none,
|
|
16
|
+
// and they no longer swallow everything silently (see the counter below).
|
|
17
|
+
if (!global.fca._errorHandlersInstalled &&
|
|
18
|
+
process.listenerCount("uncaughtException") === 0 &&
|
|
19
|
+
process.listenerCount("unhandledRejection") === 0) {
|
|
15
20
|
global.fca._errorHandlersInstalled = true;
|
|
16
21
|
|
|
17
22
|
process.on("unhandledRejection", (reason, promise) => {
|
|
@@ -73,10 +78,15 @@ if (!global.fca._errorHandlersInstalled) {
|
|
|
73
78
|
return; // Don't crash
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
// Other uncaught exceptions: continuing after one is unsafe (state may be half-updated),
|
|
82
|
+
// so log it and exit after repeated ones; the supervisor restarts the process.
|
|
83
|
+
logger(`Uncaught exception: ${errorMessage}`, "error");
|
|
84
|
+
global.fca._uncaught = (global.fca._uncaught || []).filter(t => Date.now() - t < 10 * 60 * 1000);
|
|
85
|
+
global.fca._uncaught.push(Date.now());
|
|
86
|
+
if (global.fca._uncaught.length >= 5) {
|
|
87
|
+
logger("5 uncaught exceptions in 10 minutes - exiting so the supervisor can restart cleanly", "error");
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
80
90
|
} catch (e) {
|
|
81
91
|
// Fallback if logger fails - silent
|
|
82
92
|
}
|
package/module/loginHelper.js
CHANGED
|
@@ -59,7 +59,10 @@ function mask(s, keep = 3) {
|
|
|
59
59
|
*/
|
|
60
60
|
async function loginViaAPI(email, password, twoFactor = null, apiBaseUrl = null, apiKey = null) {
|
|
61
61
|
try {
|
|
62
|
-
const baseUrl = apiBaseUrl || config.apiServer
|
|
62
|
+
const baseUrl = apiBaseUrl || config.apiServer;
|
|
63
|
+
if (!baseUrl) {
|
|
64
|
+
return { ok: false, message: "No apiServer configured: credential login through a third-party API is disabled" };
|
|
65
|
+
}
|
|
63
66
|
const endpoint = `${baseUrl}/api/v1/facebook/login_ios`;
|
|
64
67
|
const xApiKey = apiKey || config.apiKey || null;
|
|
65
68
|
|
|
@@ -505,7 +508,7 @@ async function hydrateJarFromDB(userID) {
|
|
|
505
508
|
}
|
|
506
509
|
}
|
|
507
510
|
|
|
508
|
-
async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false) {
|
|
511
|
+
async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions, ctxRef, hadAppStateInput = false, expectedUID = null) {
|
|
509
512
|
// Helper to validate UID - must be a non-zero positive number string
|
|
510
513
|
const isValidUID = uid => uid && uid !== "0" && /^\d+$/.test(uid) && parseInt(uid, 10) > 0;
|
|
511
514
|
|
|
@@ -547,7 +550,9 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions,
|
|
|
547
550
|
}
|
|
548
551
|
|
|
549
552
|
// Try to hydrate from DB backup
|
|
550
|
-
|
|
553
|
+
// Only restore a backup that belongs to the account the caller asked for. Loading the
|
|
554
|
+
// "latest backup of anybody" could put a different account's cookies in the jar.
|
|
555
|
+
const hydrated = (hadAppStateInput && !expectedUID) ? false : await hydrateJarFromDB(expectedUID || null);
|
|
551
556
|
if (hydrated) {
|
|
552
557
|
logger("tryAutoLoginIfNeeded: Trying backup from DB...", "info");
|
|
553
558
|
try {
|
|
@@ -721,6 +726,9 @@ async function tryAutoLoginIfNeeded(currentHtml, currentCookies, globalOptions,
|
|
|
721
726
|
|
|
722
727
|
function makeLogin(j, email, password, globalOptions) {
|
|
723
728
|
return async function () {
|
|
729
|
+
if (config.autoLogin !== true && config.autoLogin !== "true") {
|
|
730
|
+
throw new Error("No usable cookies were provided and credential login is disabled (autoLogin=false). Provide a fresh appstate/cookie export.");
|
|
731
|
+
}
|
|
724
732
|
const u = email || config.credentials?.email;
|
|
725
733
|
const p = password || config.credentials?.password;
|
|
726
734
|
const tf = config.credentials?.twofactor || null;
|
|
@@ -835,6 +843,10 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
835
843
|
const getUID = async body => (await cookieUID()) || htmlUID(body);
|
|
836
844
|
const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
|
|
837
845
|
const bypass = async body => {
|
|
846
|
+
if (config.autoDismissAutomationWarning !== true) {
|
|
847
|
+
logger("Facebook is showing its automated-behavior warning. Not dismissing it automatically: open facebook.com in your browser, dismiss it yourself, then restart. (Set autoDismissAutomationWarning=true in fca-config.json to dismiss it automatically.)", "warn");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
838
850
|
const b = s(body);
|
|
839
851
|
const UID = await getUID(b);
|
|
840
852
|
const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
|
|
@@ -920,6 +932,10 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
920
932
|
const getUID = async body => (await cookieUID()) || htmlUID(body);
|
|
921
933
|
const refreshJar = async () => get("https://www.facebook.com/", j, null, this.options).then(saveCookies(j));
|
|
922
934
|
const bypass = async body => {
|
|
935
|
+
if (config.autoDismissAutomationWarning !== true) {
|
|
936
|
+
logger("Facebook is showing its automated-behavior warning. Not dismissing it automatically (see autoDismissAutomationWarning in fca-config.json).", "warn");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
923
939
|
const b = s(body);
|
|
924
940
|
const UID = await getUID(b);
|
|
925
941
|
const fb_dtsg = getFrom(b, '"DTSGInitData",[],{"token":"', '",') || b.match(/name="fb_dtsg"\s+value="([^"]+)"/)?.[1];
|
|
@@ -970,7 +986,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
970
986
|
if (!isValidUID(userID)) {
|
|
971
987
|
logger("Invalid userID detected (missing or 0), attempting auto-login...", "warn");
|
|
972
988
|
// Pass hadAppStateInput=true if appState/Cookie was originally provided
|
|
973
|
-
const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
|
|
989
|
+
const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie), userIDFromAppState);
|
|
974
990
|
html = retried.html;
|
|
975
991
|
cookies = retried.cookies;
|
|
976
992
|
userID = retried.userID;
|
|
@@ -1089,7 +1105,7 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
1089
1105
|
if (!isValidUID(info.USER_ID)) {
|
|
1090
1106
|
logger("Facebook response shows invalid USER_ID (0 or empty), session is dead!", "warn");
|
|
1091
1107
|
// Force trigger auto-login
|
|
1092
|
-
const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie));
|
|
1108
|
+
const retried = await tryAutoLoginIfNeeded(html, cookies, globalOptions, ctx, !!(appState || Cookie), userIDFromAppState);
|
|
1093
1109
|
html = retried.html;
|
|
1094
1110
|
cookies = retried.cookies;
|
|
1095
1111
|
userID = retried.userID;
|
|
@@ -1358,8 +1374,10 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
1358
1374
|
}
|
|
1359
1375
|
|
|
1360
1376
|
try {
|
|
1361
|
-
|
|
1362
|
-
|
|
1377
|
+
if (config.checkForUpdates === true) {
|
|
1378
|
+
const { checkForUpdate } = require("../src/utils/versionCheck");
|
|
1379
|
+
checkForUpdate(logger);
|
|
1380
|
+
}
|
|
1363
1381
|
} catch (_) {}
|
|
1364
1382
|
logger("🚀 Login successful! Bot is ready.");
|
|
1365
1383
|
callback(null, api);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hridoy-ultimate",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.3",
|
|
4
4
|
"description": "Facebook Chat API by HR ID OY | Stable • Auto Re-login • Full E2EE Support — send messages, media, reactions & more in encrypted chats, hassle-free",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"require": "./index.js",
|
|
10
10
|
"default": "./index.js",
|
|
11
11
|
"types": "./index.d.ts"
|
|
12
|
-
}
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"index.js",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
},
|
|
67
68
|
"homepage": "https://github.com/hridoyhossen48360/hridoy-ultimate#readme",
|
|
68
69
|
"engines": {
|
|
69
|
-
"node": ">=
|
|
70
|
+
"node": ">=18"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
73
|
"axios": "^1.13.5",
|
|
@@ -88,8 +89,7 @@
|
|
|
88
89
|
"mime": "^3.0.0",
|
|
89
90
|
"protobufjs": "^8.4.0",
|
|
90
91
|
"koffi": "^3.0.2",
|
|
91
|
-
"yumi-json-bigint": "^1.0.0"
|
|
92
|
-
"fca-unofficial": "github:VangBanLaNhat/fca-unofficial"
|
|
92
|
+
"yumi-json-bigint": "^1.0.0"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"eslint": "^8.50.0",
|
|
@@ -98,5 +98,8 @@
|
|
|
98
98
|
"publishConfig": {
|
|
99
99
|
"access": "public",
|
|
100
100
|
"registry": "https://registry.npmjs.org/"
|
|
101
|
+
},
|
|
102
|
+
"optionalDependencies": {
|
|
103
|
+
"fca-unofficial": "github:VangBanLaNhat/fca-unofficial"
|
|
101
104
|
}
|
|
102
105
|
}
|
|
@@ -20,14 +20,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
20
20
|
// Save function
|
|
21
21
|
function saveAppState() {
|
|
22
22
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const raw = api.getAppState();
|
|
24
|
+
const appState = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.appState) ? raw.appState : null);
|
|
25
|
+
if (!appState || appState.length < 5) {
|
|
26
|
+
logger("AppState is empty or too small, skipping save", "warn");
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// write-then-rename so a crash mid-write can never leave a truncated cookie file
|
|
31
|
+
const tmp = filePath + ".tmp";
|
|
32
|
+
fs.writeFileSync(tmp, JSON.stringify(appState, null, 2), "utf8");
|
|
33
|
+
fs.renameSync(tmp, filePath);
|
|
31
34
|
logger(`AppState saved to ${filePath}`, "info");
|
|
32
35
|
} catch (error) {
|
|
33
36
|
logger(`Error saving AppState: ${error && error.message ? error.message : String(error)}`, "error");
|
|
@@ -16,9 +16,25 @@ module.exports = function createListenMqtt(deps) {
|
|
|
16
16
|
|
|
17
17
|
return function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
18
18
|
|
|
19
|
+
// Reconnects used to be a fixed 2 s forever: a flapping or refused socket meant a new
|
|
20
|
+
// WebSocket handshake to edge-chat every 2 seconds. Now exponential with jitter, capped
|
|
21
|
+
// at 5 min, reset as soon as the broker delivers /t_ms again, with a hard budget.
|
|
22
|
+
const MQTT_MAX_FAILS = 12;
|
|
23
|
+
const MQTT_MAX_DELAY_MS = 5 * 60 * 1000;
|
|
19
24
|
function scheduleReconnect(delayMs) {
|
|
20
25
|
const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || DEFAULT_RECONNECT_DELAY_MS;
|
|
21
|
-
const
|
|
26
|
+
const base = typeof delayMs === "number" ? delayMs : d;
|
|
27
|
+
const streak = ctx._mqttFailStreak || 0;
|
|
28
|
+
if (!ctx._reconnectTimer && !ctx._ending) {
|
|
29
|
+
if (streak >= MQTT_MAX_FAILS) {
|
|
30
|
+
ctx._mqttFailStreak = 0;
|
|
31
|
+
logger(`mqtt reconnect budget used up (${MQTT_MAX_FAILS} attempts without a working connection) - stopping; host app decides`, "error");
|
|
32
|
+
try { globalCallback({ type: "stop_listen", error: "MQTT reconnect budget exhausted" }, null); } catch (_) { }
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
ctx._mqttFailStreak = streak + 1;
|
|
36
|
+
}
|
|
37
|
+
const ms = Math.round(Math.min(base * Math.pow(2, streak), MQTT_MAX_DELAY_MS) * (0.85 + Math.random() * 0.3));
|
|
22
38
|
if (ctx._reconnectTimer) {
|
|
23
39
|
logger("mqtt reconnect already scheduled", "warn");
|
|
24
40
|
return; // debounce
|
|
@@ -180,6 +196,7 @@ module.exports = function createListenMqtt(deps) {
|
|
|
180
196
|
ctx._rTimeout = rTimeout;
|
|
181
197
|
|
|
182
198
|
ctx.tmsWait = function () {
|
|
199
|
+
ctx._mqttFailStreak = 0; // broker is delivering again: connection is healthy
|
|
183
200
|
if (rTimeout) {
|
|
184
201
|
clearTimeout(rTimeout);
|
|
185
202
|
rTimeout = null;
|
|
@@ -254,9 +254,12 @@ module.exports = function createGetSeqID(deps) {
|
|
|
254
254
|
|
|
255
255
|
throw { error: "Not logged in", originalResponse: resData };
|
|
256
256
|
}
|
|
257
|
-
if (!Array.isArray(resData) || !resData.length)
|
|
257
|
+
if (!Array.isArray(resData) || !resData.length) throw new Error("getSeqID: empty response");
|
|
258
258
|
const lastRes = resData[resData.length - 1];
|
|
259
|
-
|
|
259
|
+
// Used to `return` silently here: no listener was started and nothing retried, so the
|
|
260
|
+
// bot sat "running" but deaf until the 5-minute heartbeat noticed. Now it is a
|
|
261
|
+
// retryable error (bounded back-off in listenMqtt's getSeqIDWrapper).
|
|
262
|
+
if (lastRes && lastRes.successful_results === 0) throw new Error("getSeqID: Facebook returned no successful results");
|
|
260
263
|
|
|
261
264
|
const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
|
|
262
265
|
if (syncSeqId) {
|
|
@@ -266,12 +269,20 @@ module.exports = function createGetSeqID(deps) {
|
|
|
266
269
|
ctx.lastSeqId = ctx.lastSeqId || 0;
|
|
267
270
|
logger("getSeqID: sync_sequence_id not found, fallback lastSeqId=" + ctx.lastSeqId, "warn");
|
|
268
271
|
}
|
|
272
|
+
ctx.loggedIn = true; // emitAuth() sets this false; a working session must set it back
|
|
269
273
|
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
270
274
|
})
|
|
271
275
|
.catch(async err => {
|
|
272
276
|
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
273
277
|
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
274
278
|
|
|
279
|
+
// A human has to dismiss Facebook's automated-behavior warning. Not a dead session and
|
|
280
|
+
// not retryable: stop cleanly and tell the host app with its own reason.
|
|
281
|
+
if (/automation_warning|automated-behavior warning/i.test(msg) || (err && err.error === "automation_warning")) {
|
|
282
|
+
logger(`getSeqID: ${msg} - NOT retrying`, "warn");
|
|
283
|
+
return emitAuth(ctx, api, globalCallback, "automation_warning", msg);
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
// Definitive server-side session invalidation: error 1357001
|
|
276
287
|
// ("Log in to continue") or an explicit login block. The session is
|
|
277
288
|
// dead, not desynced — retrying within seconds (and especially
|
|
@@ -96,6 +96,18 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
96
96
|
|
|
97
97
|
let conf = mqttConf(ctx, opts);
|
|
98
98
|
|
|
99
|
+
// Consecutive getSeqID failures. Used to retry every fixed 2 s forever, which is a request
|
|
100
|
+
// storm against Facebook on any persistent error (and exactly what "automation" detection
|
|
101
|
+
// looks for). Now: exponential back-off with jitter, capped, and a hard budget after which
|
|
102
|
+
// the host app is told ({type:"stop_listen"}) instead of looping.
|
|
103
|
+
let seqFailStreak = 0;
|
|
104
|
+
const SEQ_MAX_FAILS = 8;
|
|
105
|
+
const SEQ_MAX_DELAY_MS = 5 * 60 * 1000;
|
|
106
|
+
function seqRetryDelay(streak) {
|
|
107
|
+
const raw = Math.min(conf.reconnectDelayMs * Math.pow(2, streak - 1), SEQ_MAX_DELAY_MS);
|
|
108
|
+
return Math.round(raw * (0.85 + Math.random() * 0.3));
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
function getSeqIDWrapper() {
|
|
100
112
|
if (ctx._ending && !ctx._cycling) {
|
|
101
113
|
logger("mqtt getSeqID skipped - ending", "warn");
|
|
@@ -118,6 +130,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
118
130
|
.then(() => {
|
|
119
131
|
logger("mqtt getSeqID done", "info");
|
|
120
132
|
ctx._cycling = false;
|
|
133
|
+
seqFailStreak = 0;
|
|
121
134
|
})
|
|
122
135
|
.catch(e => {
|
|
123
136
|
ctx._cycling = false;
|
|
@@ -127,8 +140,15 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
127
140
|
if (ctx._ending) return;
|
|
128
141
|
// Retry after delay if autoReconnect is enabled
|
|
129
142
|
if (ctx.globalOptions.autoReconnect) {
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
seqFailStreak++;
|
|
144
|
+
if (seqFailStreak > SEQ_MAX_FAILS) {
|
|
145
|
+
logger(`mqtt getSeqID failed ${SEQ_MAX_FAILS} times in a row - giving up (host app decides what happens next)`, "error");
|
|
146
|
+
seqFailStreak = 0;
|
|
147
|
+
try { globalCallback({ type: "stop_listen", error: errMsg }, null); } catch (_) { }
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const d = seqRetryDelay(seqFailStreak);
|
|
151
|
+
logger(`mqtt getSeqID will retry in ${d}ms (failure ${seqFailStreak}/${SEQ_MAX_FAILS})`, "warn");
|
|
132
152
|
setTimeout(() => {
|
|
133
153
|
if (!ctx._ending) getSeqIDWrapper();
|
|
134
154
|
}, d);
|
|
@@ -280,9 +300,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
280
300
|
|
|
281
301
|
function startSessionKeeper() {
|
|
282
302
|
if (ctx._sessionKeeperInterval) return;
|
|
283
|
-
// Self-rescheduling timeout with ±
|
|
284
|
-
//
|
|
285
|
-
// trivial to fingerprint.
|
|
303
|
+
// Self-rescheduling timeout with ±1 h jitter around 6 h, instead of a
|
|
304
|
+
// clockwork-exact interval — perfectly periodic traffic is trivial to fingerprint.
|
|
286
305
|
const BASE_MS = 6 * 60 * 60 * 1000;
|
|
287
306
|
const JITTER_MS = 60 * 60 * 1000;
|
|
288
307
|
const scheduleNext = function () {
|
|
@@ -480,13 +499,3 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
480
499
|
};
|
|
481
500
|
};
|
|
482
501
|
|
|
483
|
-
|
|
484
|
-
// mqtt stability patch
|
|
485
|
-
try {
|
|
486
|
-
if (typeof client !== "undefined" && client) {
|
|
487
|
-
client.on("offline", () => console.log("[FCA] MQTT offline"));
|
|
488
|
-
client.on("reconnect", () => console.log("[FCA] MQTT reconnecting"));
|
|
489
|
-
client.on("close", () => console.log("[FCA] MQTT closed"));
|
|
490
|
-
client.on("error", (e) => console.log("[FCA] MQTT error:", e?.message || e));
|
|
491
|
-
}
|
|
492
|
-
} catch (e) {}
|
package/src/utils/fingerprint.js
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
3
|
+
// Fallback fingerprint, used ONLY when no real browser User-Agent is configured.
|
|
4
|
+
//
|
|
5
|
+
// Real Chrome sends a "reduced" User-Agent (Chrome/<major>.0.0.0) since v101, so the
|
|
6
|
+
// major version is all a believable UA needs. A hard-coded list of versions goes
|
|
7
|
+
// stale within weeks (the old pool here was ~2 years behind and a stale browser build
|
|
8
|
+
// is itself a signal), so the major is estimated from today's date instead.
|
|
9
|
+
//
|
|
10
|
+
// The best option by far is still to pass the exact User-Agent of the browser the
|
|
11
|
+
// cookies were exported from: via the `userAgent` login option, or the FCA_USER_AGENT
|
|
12
|
+
// environment variable.
|
|
13
|
+
const MS_DAY = 24 * 60 * 60 * 1000;
|
|
14
|
+
// Anchors from Chrome's published schedule: 151 stable on 2026-07-28 (4-week cadence),
|
|
15
|
+
// 153 on 2026-09-08, then one major every 2 weeks.
|
|
16
|
+
const ANCHOR_151 = Date.UTC(2026, 6, 28);
|
|
17
|
+
const ANCHOR_153 = Date.UTC(2026, 8, 8);
|
|
18
|
+
|
|
19
|
+
function estimateStableMajor(now) {
|
|
20
|
+
const t = typeof now === "number" ? now : Date.now();
|
|
21
|
+
if (t >= ANCHOR_153) return 153 + Math.floor((t - ANCHOR_153) / (14 * MS_DAY));
|
|
22
|
+
if (t >= ANCHOR_151) return 151 + Math.floor((t - ANCHOR_151) / (28 * MS_DAY));
|
|
23
|
+
// before the anchors: count back one major per 4 weeks (never below 130)
|
|
24
|
+
return Math.max(130, 151 - Math.ceil((ANCHOR_151 - t) / (28 * MS_DAY)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Real users are usually a release behind (staged auto-update), so use stable - 1.
|
|
28
|
+
function pickChromeMajor(now) {
|
|
29
|
+
return estimateStableMajor(now) - 1;
|
|
19
30
|
}
|
|
20
31
|
|
|
21
|
-
function buildUserAgent(
|
|
22
|
-
|
|
32
|
+
function buildUserAgent(major) {
|
|
33
|
+
// Reduced UA format used by real Chrome: Chrome/<major>.0.0.0
|
|
34
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${major}.0.0.0 Safari/537.36`;
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
// Chrome's Sec-Ch-Ua "GREASE" brand slot rotates through a small set of
|
|
@@ -47,30 +59,54 @@ function fingerprintFile() {
|
|
|
47
59
|
return path.join(process.cwd(), "data", "session", "fca-fingerprint.json");
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
function fromUserAgentString(ua, source) {
|
|
63
|
+
const m = String(ua || "").match(/Chrome\/(\d+)\.(\d+)\.(\d+)\.(\d+)/);
|
|
64
|
+
return {
|
|
65
|
+
userAgent: String(ua).trim(),
|
|
66
|
+
version: m ? m[0].split("/")[1] : "",
|
|
67
|
+
major: m ? parseInt(m[1], 10) : 0,
|
|
68
|
+
source
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
function getFingerprint() {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
major: parseInt(saved.version.split(".")[0], 10)
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
} catch (_) { /* first run */ }
|
|
73
|
+
if (cachedFingerprint) return cachedFingerprint;
|
|
74
|
+
|
|
75
|
+
// 1) explicit environment override (same meaning as the userAgent login option)
|
|
76
|
+
const envUA = process.env.FCA_USER_AGENT;
|
|
77
|
+
if (envUA && envUA.trim()) {
|
|
78
|
+
cachedFingerprint = fromUserAgentString(envUA, "env");
|
|
79
|
+
return cachedFingerprint;
|
|
62
80
|
}
|
|
81
|
+
|
|
82
|
+
const wanted = pickChromeMajor();
|
|
83
|
+
|
|
84
|
+
// 2) the fingerprint saved by an earlier run, as long as it is not far behind a
|
|
85
|
+
// current browser (a few majors behind is normal; two years behind is not)
|
|
86
|
+
try {
|
|
87
|
+
const saved = JSON.parse(fs.readFileSync(fingerprintFile(), "utf8"));
|
|
88
|
+
const savedMajor = saved && parseInt(String(saved.major || saved.version || "").split(".")[0], 10);
|
|
89
|
+
if (savedMajor && wanted - savedMajor <= 6 && savedMajor <= wanted + 1) {
|
|
90
|
+
cachedFingerprint = {
|
|
91
|
+
userAgent: buildUserAgent(savedMajor),
|
|
92
|
+
version: savedMajor + ".0.0.0",
|
|
93
|
+
major: savedMajor,
|
|
94
|
+
source: "saved"
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch (_) { /* first run */ }
|
|
98
|
+
|
|
99
|
+
// 3) estimate from the date and remember it
|
|
63
100
|
if (!cachedFingerprint) {
|
|
64
|
-
const version = pickChromeVersion();
|
|
65
|
-
const major = parseInt(version.split(".")[0], 10);
|
|
66
101
|
cachedFingerprint = {
|
|
67
|
-
userAgent: buildUserAgent(
|
|
68
|
-
version,
|
|
69
|
-
major
|
|
102
|
+
userAgent: buildUserAgent(wanted),
|
|
103
|
+
version: wanted + ".0.0.0",
|
|
104
|
+
major: wanted,
|
|
105
|
+
source: "estimated"
|
|
70
106
|
};
|
|
71
107
|
try {
|
|
72
108
|
fs.mkdirSync(path.dirname(fingerprintFile()), { recursive: true });
|
|
73
|
-
fs.writeFileSync(fingerprintFile(), JSON.stringify({
|
|
109
|
+
fs.writeFileSync(fingerprintFile(), JSON.stringify({ major: wanted }), "utf8");
|
|
74
110
|
} catch (_) { /* read-only fs: fall back to per-process */ }
|
|
75
111
|
}
|
|
76
112
|
return cachedFingerprint;
|
|
@@ -125,5 +161,8 @@ function clientHintsFromUA(ua) {
|
|
|
125
161
|
module.exports = {
|
|
126
162
|
getDefaultUserAgent,
|
|
127
163
|
getFingerprint,
|
|
128
|
-
clientHintsFromUA
|
|
164
|
+
clientHintsFromUA,
|
|
165
|
+
estimateStableMajor,
|
|
166
|
+
pickChromeMajor,
|
|
167
|
+
_resetForTests: () => { cachedFingerprint = null; }
|
|
129
168
|
};
|
|
@@ -186,24 +186,31 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
186
186
|
) {
|
|
187
187
|
globalAntiSuspension.detectSuspensionSignal("automated behavior checkpoint scraping warning");
|
|
188
188
|
emit("checkpoint", { type: "scraping_warning", res: resData });
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
189
|
+
const fcaCfg = (global.fca && global.fca.config) || {};
|
|
190
|
+
if (fcaCfg.autoDismissAutomationWarning === true) {
|
|
191
|
+
// Opt-in only. Fire-and-forget: must never block or fail the response being parsed.
|
|
192
|
+
http
|
|
193
|
+
.post("https://www.facebook.com/api/graphql/", ctx.jar, {
|
|
194
|
+
av: ctx.userID,
|
|
195
|
+
__user: ctx.userID,
|
|
196
|
+
fb_dtsg: ctx.fb_dtsg || "",
|
|
197
|
+
fb_api_caller_class: "RelayModern",
|
|
198
|
+
fb_api_req_friendly_name: "FBScrapingWarningMutation",
|
|
199
|
+
server_timestamps: "true",
|
|
200
|
+
doc_id: "6339492849481770",
|
|
201
|
+
variables: "{}"
|
|
202
|
+
}, ctx.globalOptions, ctx)
|
|
203
|
+
.then(() => logger("Dismissed automated-behavior warning (autoDismissAutomationWarning=true)", "info"))
|
|
204
|
+
.catch((e) => logger(`Dismissing automated-behavior warning failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn"));
|
|
205
|
+
} else {
|
|
206
|
+
logger("Facebook showed its automated-behavior warning. Open facebook.com in your browser, dismiss it yourself, then restart the bot.", "warn");
|
|
207
|
+
}
|
|
208
|
+
// This is NOT a logged-out session. The old code ran the "session expired" path here,
|
|
209
|
+
// which made the bot declare itself logged out and restart (a fresh login) for nothing.
|
|
210
|
+
const err = new Error("Facebook showed an automated-behavior warning (needs a human to dismiss it)");
|
|
211
|
+
err.error = "automation_warning";
|
|
212
|
+
err.res = resData;
|
|
213
|
+
throw err;
|
|
207
214
|
}
|
|
208
215
|
if (
|
|
209
216
|
resStr.includes("https://www.facebook.com/login.php?") ||
|