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 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
@@ -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
- logger("Initial page load doesn't match expected UA rendering — retrying with fallback UA", "warn");
884
- globalOptions.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
885
- const retryInitial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
886
- return (await ctx.bypassAutomation(retryInitial, jar)) || retryInitial;
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.1",
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,vi;q=0.8",
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 = 90 * 60 * 1000;
287
- const JITTER_MS = 15 * 60 * 1000;
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 = 40;
67
- this.normalHourlyCap = 350;
68
- this.dailyCap = 2500;
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;
@@ -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
  }
@@ -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,vi;q=0.8";
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
- }