hridoy-ultimate 9.0.1 → 9.0.2
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 +12 -0
- package/module/loginHelper.js +4 -4
- package/module/options.js +6 -0
- package/package.json +1 -1
- package/src/api/messaging/sessionGuard.js +8 -0
- package/src/api/socket/core/connectMqtt.js +1 -1
- package/src/api/socket/listenMqtt.js +2 -2
- package/src/utils/antiSuspension.js +3 -3
- package/src/utils/fingerprint.js +28 -0
- package/src/utils/headers.js +1 -1
- package/src/api/socket/e2ee/proto/proto-writer.ts +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [9.0.2] - 2026-10-22
|
|
10
|
+
|
|
11
|
+
### Session stability
|
|
12
|
+
|
|
13
|
+
- Browser fingerprint (Chrome build) is now saved to `data/session/fca-fingerprint.json` and reused after restarts. Before, every restart picked a new random UA = "new browser" each time.
|
|
14
|
+
- Login no longer swaps the User-Agent to a hardcoded Chrome/125 in the middle of a session; it warns instead.
|
|
15
|
+
- Default `Accept-Language` is `en-US,en;q=0.9` (was `vi`), configurable with the new `acceptLanguage` option; HTTP and MQTT use the same value.
|
|
16
|
+
- Fewer requests: session keeper every ~6h (was ~90 min), lower send caps (warmup 20/h, 120/h, 800/day).
|
|
17
|
+
- `sessionGuard` also saves the cookie jar when the process exits.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
9
21
|
## [9.0.1] - 2026-10-17
|
|
10
22
|
|
|
11
23
|
### Rebrand
|
package/module/loginHelper.js
CHANGED
|
@@ -880,10 +880,10 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
|
|
|
880
880
|
// inconsistent to Facebook.
|
|
881
881
|
const bodyStr = typeof initial?.data === "string" ? initial.data : String(initial?.data ?? "");
|
|
882
882
|
if (!/MPageLoadClientMetrics/.test(bodyStr)) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
883
|
+
// Do NOT swap the User-Agent mid-session: a session that starts as
|
|
884
|
+
// one browser and continues as another is exactly what gets
|
|
885
|
+
// cookies invalidated. Keep one UA and tell the user how to fix it.
|
|
886
|
+
logger("Initial page load doesn't match the expected rendering for this User-Agent. Set the exact User-Agent of the browser your cookies came from (facebookAccount.userAgent in config.json).", "warn");
|
|
887
887
|
}
|
|
888
888
|
return (await ctx.bypassAutomation(initial, jar)) || initial;
|
|
889
889
|
}
|
package/module/options.js
CHANGED
|
@@ -25,6 +25,12 @@ function setOptions(globalOptions, options) {
|
|
|
25
25
|
globalOptions.userAgent = options.userAgent || getDefaultUserAgent();
|
|
26
26
|
break;
|
|
27
27
|
}
|
|
28
|
+
case "acceptLanguage": {
|
|
29
|
+
if (typeof options.acceptLanguage === "string" && options.acceptLanguage.trim()) {
|
|
30
|
+
globalOptions.acceptLanguage = options.acceptLanguage.trim();
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
28
34
|
case "humanize": {
|
|
29
35
|
globalOptions.humanize = options.humanize;
|
|
30
36
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hridoy-ultimate",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.2",
|
|
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",
|
|
@@ -86,6 +86,14 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
86
86
|
return result;
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
+
// Save the freshest cookies when the process exits (restart, crash
|
|
90
|
+
// handler, Ctrl+C). Facebook rotates cookies while the bot runs; if
|
|
91
|
+
// the last periodic save is older than that rotation, the next start
|
|
92
|
+
// would load already-superseded cookies and look "expired".
|
|
93
|
+
process.once("exit", function () {
|
|
94
|
+
try { saveToDisk(appStatePath); } catch (_) {}
|
|
95
|
+
});
|
|
96
|
+
|
|
89
97
|
// Expose manual save
|
|
90
98
|
api.saveSession = function (customPath) {
|
|
91
99
|
return saveToDisk(customPath || appStatePath);
|
|
@@ -78,7 +78,7 @@ module.exports = function createListenMqtt(deps) {
|
|
|
78
78
|
// Must match the HTTP layer's Accept-Language (src/utils/headers.js)
|
|
79
79
|
// — a hardcoded different value here made the WS handshake and the
|
|
80
80
|
// HTTP requests disagree about the same "browser".
|
|
81
|
-
"Accept-Language": ctx.globalOptions.acceptLanguage || "en-US,en;q=0.9
|
|
81
|
+
"Accept-Language": ctx.globalOptions.acceptLanguage || "en-US,en;q=0.9",
|
|
82
82
|
"Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits"
|
|
83
83
|
},
|
|
84
84
|
origin: "https://www.facebook.com",
|
|
@@ -283,8 +283,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
283
283
|
// Self-rescheduling timeout with ±15 min jitter around 90 min, instead of
|
|
284
284
|
// a clockwork-exact 90:00.000 interval — perfectly periodic traffic is
|
|
285
285
|
// trivial to fingerprint.
|
|
286
|
-
const BASE_MS =
|
|
287
|
-
const JITTER_MS =
|
|
286
|
+
const BASE_MS = 6 * 60 * 60 * 1000;
|
|
287
|
+
const JITTER_MS = 60 * 60 * 1000;
|
|
288
288
|
const scheduleNext = function () {
|
|
289
289
|
const delay = BASE_MS - JITTER_MS + Math.floor(Math.random() * 2 * JITTER_MS);
|
|
290
290
|
ctx._sessionKeeperInterval = setTimeout(function () {
|
|
@@ -63,9 +63,9 @@ class SendPacer {
|
|
|
63
63
|
|
|
64
64
|
this.startedAt = Date.now();
|
|
65
65
|
this.warmupWindowMs = 15 * 60 * 1000;
|
|
66
|
-
this.warmupHourlyCap =
|
|
67
|
-
this.normalHourlyCap =
|
|
68
|
-
this.dailyCap =
|
|
66
|
+
this.warmupHourlyCap = 20;
|
|
67
|
+
this.normalHourlyCap = 120;
|
|
68
|
+
this.dailyCap = 800;
|
|
69
69
|
|
|
70
70
|
this.hourWindowStart = Date.now();
|
|
71
71
|
this.hourSendCount = 0;
|
package/src/utils/fingerprint.js
CHANGED
|
@@ -35,7 +35,31 @@ function greaseForVersion(major) {
|
|
|
35
35
|
// to different hardcoded defaults) is itself inconsistent and detectable.
|
|
36
36
|
let cachedFingerprint = null;
|
|
37
37
|
|
|
38
|
+
// The fingerprint is also saved to disk. Picking a new random Chrome build
|
|
39
|
+
// on every restart made the same account look like a new browser each time
|
|
40
|
+
// the bot restarted, which is a common reason for "session dropped" logouts.
|
|
41
|
+
// Best option of all: pass your real browser's User-Agent (the one the
|
|
42
|
+
// cookies were exported from) via the userAgent option; this file is only
|
|
43
|
+
// the fallback.
|
|
44
|
+
const fs = require("fs");
|
|
45
|
+
const path = require("path");
|
|
46
|
+
function fingerprintFile() {
|
|
47
|
+
return path.join(process.cwd(), "data", "session", "fca-fingerprint.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function getFingerprint() {
|
|
51
|
+
if (!cachedFingerprint) {
|
|
52
|
+
try {
|
|
53
|
+
const saved = JSON.parse(fs.readFileSync(fingerprintFile(), "utf8"));
|
|
54
|
+
if (saved && CHROME_VERSIONS.includes(saved.version)) {
|
|
55
|
+
cachedFingerprint = {
|
|
56
|
+
userAgent: buildUserAgent(saved.version),
|
|
57
|
+
version: saved.version,
|
|
58
|
+
major: parseInt(saved.version.split(".")[0], 10)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
} catch (_) { /* first run */ }
|
|
62
|
+
}
|
|
39
63
|
if (!cachedFingerprint) {
|
|
40
64
|
const version = pickChromeVersion();
|
|
41
65
|
const major = parseInt(version.split(".")[0], 10);
|
|
@@ -44,6 +68,10 @@ function getFingerprint() {
|
|
|
44
68
|
version,
|
|
45
69
|
major
|
|
46
70
|
};
|
|
71
|
+
try {
|
|
72
|
+
fs.mkdirSync(path.dirname(fingerprintFile()), { recursive: true });
|
|
73
|
+
fs.writeFileSync(fingerprintFile(), JSON.stringify({ version }), "utf8");
|
|
74
|
+
} catch (_) { /* read-only fs: fall back to per-process */ }
|
|
47
75
|
}
|
|
48
76
|
return cachedFingerprint;
|
|
49
77
|
}
|
package/src/utils/headers.js
CHANGED
|
@@ -45,7 +45,7 @@ function getHeaders(url, options, ctx, customHeader) {
|
|
|
45
45
|
const referer = options?.referer || "https://www.facebook.com/";
|
|
46
46
|
const origin = referer.replace(/\/+$/, "");
|
|
47
47
|
const contentType = options?.contentType || "application/x-www-form-urlencoded";
|
|
48
|
-
const acceptLang = options?.acceptLanguage || "en-US,en;q=0.9
|
|
48
|
+
const acceptLang = options?.acceptLanguage || "en-US,en;q=0.9";
|
|
49
49
|
// NOTE: keep the && chain parenthesized — `||` binds looser than `&&`.
|
|
50
50
|
const isNavigation = (u.pathname === "/" &&
|
|
51
51
|
!url.includes("ajax") &&
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/** Minimal protobuf writer - only what we need */
|
|
2
|
-
export class ProtoWriter {
|
|
3
|
-
private chunks: Buffer[] = [];
|
|
4
|
-
|
|
5
|
-
private encodeVarint(value: number): Buffer {
|
|
6
|
-
const bytes: number[] = [];
|
|
7
|
-
let v = value >>> 0;
|
|
8
|
-
while (v > 127) {
|
|
9
|
-
bytes.push((v & 0x7f) | 0x80);
|
|
10
|
-
v >>>= 7;
|
|
11
|
-
}
|
|
12
|
-
bytes.push(v);
|
|
13
|
-
return Buffer.from(bytes);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
private fieldHeader(fieldNum: number, wireType: number): Buffer {
|
|
17
|
-
return this.encodeVarint((fieldNum << 3) | wireType);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Wire type 0 (varint) but supports bigint */
|
|
21
|
-
private encodeVarintBigInt(value: bigint): Buffer {
|
|
22
|
-
const bytes: number[] = [];
|
|
23
|
-
let v = value;
|
|
24
|
-
while (v > 127n) {
|
|
25
|
-
bytes.push(Number((v & 0x7fn) | 0x80n));
|
|
26
|
-
v >>= 7n;
|
|
27
|
-
}
|
|
28
|
-
bytes.push(Number(v));
|
|
29
|
-
return Buffer.from(bytes);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Wire type 0 (varint) for uint64 fields */
|
|
33
|
-
uint64_varint(fieldNum: number, value: bigint): this {
|
|
34
|
-
this.chunks.push(this.fieldHeader(fieldNum, 0));
|
|
35
|
-
this.chunks.push(this.encodeVarintBigInt(value));
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Wire type 2 (length-delimited) - bytes, string, embedded message */
|
|
40
|
-
bytes(fieldNum: number, data: Uint8Array): this {
|
|
41
|
-
const d = Buffer.from(data);
|
|
42
|
-
this.chunks.push(this.fieldHeader(fieldNum, 2));
|
|
43
|
-
this.chunks.push(this.encodeVarint(d.length));
|
|
44
|
-
this.chunks.push(d);
|
|
45
|
-
return this;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
string(fieldNum: number, value: string): this {
|
|
49
|
-
return this.bytes(fieldNum, Buffer.from(value, "utf8"));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Wire type 0 (varint) */
|
|
53
|
-
varint(fieldNum: number, value: number): this {
|
|
54
|
-
this.chunks.push(this.fieldHeader(fieldNum, 0));
|
|
55
|
-
this.chunks.push(this.encodeVarint(value));
|
|
56
|
-
return this;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Wire type 0, bool */
|
|
60
|
-
bool(fieldNum: number, value: boolean): this {
|
|
61
|
-
return this.varint(fieldNum, value ? 1 : 0);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Wire type 1 (64-bit fixed) - uint64 */
|
|
65
|
-
uint64(fieldNum: number, value: bigint): this {
|
|
66
|
-
const buf = Buffer.alloc(8);
|
|
67
|
-
buf.writeBigUInt64LE(value);
|
|
68
|
-
this.chunks.push(this.fieldHeader(fieldNum, 1));
|
|
69
|
-
this.chunks.push(buf);
|
|
70
|
-
return this;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
build(): Buffer {
|
|
74
|
-
return Buffer.concat(this.chunks);
|
|
75
|
-
}
|
|
76
|
-
}
|