hridoy-ultimate 9.0.3 → 9.0.4

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,51 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ---
8
8
 
9
+ ## [9.0.5] - 2026-10-30
10
+
11
+ ### Fixed (account / browser session safety)
12
+
13
+ - `api.logout()` used to POST `facebook.com/logout.php`. Facebook then ends that session everywhere, so the same account was also logged out in your normal browser whenever a bot called `logout()` (e.g. on shutdown/restart). `api.logout()` is now a **local stop only**: it stops the MQTT listener and the internal timers and sends nothing to Facebook. It resolves with `{ localOnly: true }`.
14
+ - The real logout is a separate method, `api.logoutFromFacebook()`. If you really want `api.logout()` to log out of Facebook, set `"allowFacebookLogout": true` in `fca-config.json` (default `false`).
15
+ - Cookie mirroring (`facebook.com` <-> `messenger.com`): a *delete this cookie* answer from `messenger.com` for a login cookie (`c_user`, `xs`, `i_user`, `datr`, `sb`, `fr`) is no longer copied onto `facebook.com`. Before, a rejected mirrored value could wipe a perfectly good `facebook.com` login cookie from the bot's jar and make a healthy session look expired.
16
+
17
+ ### Added
18
+
19
+ - `api.logoutFromFacebook()` (also `client.account.logoutFromFacebook` from `createFcaClient`).
20
+ - `allowFacebookLogout` option in `fca-config.json`.
21
+ - `test/logout.test.js`, `test/cookies.test.js`.
22
+
23
+ ### Not changed
24
+
25
+ - Facebook can still end a session it considers suspicious (cookies used from a different IP / User-Agent than the browser they came from). Pass the exact User-Agent of that browser with the `userAgent` login option (or `FCA_USER_AGENT`) and run the bot from the same network where you can.
26
+
27
+ ---
28
+
29
+ ## [9.0.4] - 2026-10-27
30
+
31
+ ### Fixed
32
+
33
+ - `api.getUserID()` and `api.changeGroupImage()` always threw `ReferenceError: parseAndCheckLogin is not defined` (missing import). Both work again.
34
+ - `api.refreshFb_dtsg()` threw `ReferenceError: CustomError is not defined` on a bad argument; it now throws a normal `Error`.
35
+ - `sendMessage`: when the safety guard blocked a send, the log call was `logger(msg, "error")` but the logger is an object, so it threw `TypeError: logger is not a function`. The send callback was never called and the caller hung. Now logs correctly and the callback / promise is rejected with `ACCOUNT_SAFETY_PAUSED`.
36
+ - Safety guard: rate-limit style text ("too many requests", "rate limited", "temporarily blocked", "action blocked", ...) used to switch sending off until restart. Only real account signals (checkpoint, automated-behavior warning, locked/disabled/suspended, identity check) still fail closed. Rate-limit style signals now only count as strikes (2 within 30 min = slow-down cooldown).
37
+ - `package.json` now declares `tough-cookie`, `form-data` and `request`, which the code already `require()`s. A clean `npm install` of this package alone used to crash on first use.
38
+
39
+ ### Added
40
+
41
+ - `api.getSafetyStatus()` -> `{ blocked, reason, coolingDown, warmup, hourSendCount, hourlyCap, daySendCount, dailyCap }` so a bot can detect `ACCOUNT_SAFETY_PAUSED` instead of retrying.
42
+ - `test/antiSuspension.test.js` (`npm test`, uses `node --test`).
43
+
44
+ ---
45
+
46
+ ## [9.0.3] - 2026-10-25 (entry added retroactively)
47
+
48
+ ### Account safety
49
+
50
+ - Outbound sending is paused (`ACCOUNT_SAFETY_PAUSED`) after a Facebook checkpoint / automated-behavior signal. Automatic dismissal of the automated-behavior warning stays off.
51
+
52
+ ---
53
+
9
54
  ## [9.0.2] - 2026-10-22
10
55
 
11
56
  ### Session stability
package/DOCS.md CHANGED
@@ -1711,24 +1711,32 @@ api.searchForThread("Study Group", (err, threads) => {
1711
1711
 
1712
1712
  ---
1713
1713
 
1714
- ### 3.29. logout - Logout
1714
+ ### 3.29. logout / logoutFromFacebook - Stop the bot / Logout
1715
1715
 
1716
- Logout from Facebook account.
1716
+ `api.logout()` **stops the bot locally** (MQTT listener and internal timers). It sends nothing to
1717
+ Facebook, so your normal browser session stays logged in. It resolves with `{ localOnly: true }`.
1718
+
1719
+ `api.logoutFromFacebook()` is the **real** Facebook logout. Facebook ends that session everywhere,
1720
+ so you are also logged out in your browser. Use it only when that is what you want.
1721
+
1722
+ If you want `api.logout()` itself to perform the real logout, set `"allowFacebookLogout": true` in
1723
+ `fca-config.json` (default `false`).
1717
1724
 
1718
1725
  #### Syntax:
1719
1726
  ```javascript
1720
- api.logout(callback);
1727
+ api.logout(callback); // local stop only (default)
1728
+ api.logoutFromFacebook(callback); // real Facebook logout
1721
1729
  ```
1722
1730
 
1723
1731
  #### Example:
1724
1732
 
1725
1733
  ```javascript
1726
- api.logout((err) => {
1734
+ api.logout((err, result) => {
1727
1735
  if (err) {
1728
- console.error("Logout error:", err);
1736
+ console.error("Stop error:", err);
1729
1737
  return;
1730
1738
  }
1731
- console.log("Logged out successfully!");
1739
+ console.log("Bot stopped.", result); // { localOnly: true }
1732
1740
  });
1733
1741
  ```
1734
1742
 
package/README.md CHANGED
@@ -492,7 +492,8 @@ api.sharePost(postID, "Check this!");
492
492
  api.getCurrentUserID();
493
493
  api.getAppState();
494
494
  api.setOptions({ listenTyping: true });
495
- api.logout();
495
+ api.logout(); // local stop only; your browser stays logged in
496
+ api.logoutFromFacebook(); // real Facebook logout (ends your browser session too)
496
497
  api.refreshFb_dtsg();
497
498
  api.addExternalModule("myFunc", (defaultFuncs, api, ctx) => {
498
499
  return function(text, threadID) {
@@ -534,7 +535,7 @@ api.uploadImageToImgbb(imageUrl);
534
535
 
535
536
  MIT License
536
537
 
537
- **hridoy-ultimate** v9.0.1 by [HR ID OY](https://github.com/hridoyhossen48360)
538
+ **hridoy-ultimate** v9.0.4 by [HR ID OY](https://github.com/hridoyhossen48360)
538
539
 
539
540
  > The E2EE native media engine (`src/api/socket/e2ee/native/`) bundles a
540
541
  > precompiled binary built from **mautrix-go** / **mautrix-meta**, © Tulir
package/index.d.ts CHANGED
@@ -168,6 +168,7 @@ declare module '@dongdev/fca-unofficial' {
168
168
  getUserID: (name: string, callback?: (err: Error | null, obj: IFCAU_UserIDResponse) => void) => Promise<IFCAU_UserIDResponse>;
169
169
  getFriendsList: (callback?: (err: Error | null, friends: IFCAU_Friend[]) => void) => Promise<IFCAU_Friend[]>;
170
170
  getCurrentUserID: () => string;
171
+ getSafetyStatus: () => { blocked: boolean; reason: string | null; coolingDown: boolean; warmup: boolean; hourSendCount: number; hourlyCap: number; daySendCount: number; dailyCap: number };
171
172
 
172
173
  // Utilities
173
174
  getAppState: () => any;
@@ -200,7 +201,10 @@ declare module '@dongdev/fca-unofficial' {
200
201
 
201
202
  // Configuration & Session
202
203
  setOptions: (options: Partial<IFCAU_Options>) => void;
203
- logout: (callback?: (err?: Error) => void) => Promise<void>;
204
+ /** Stops the bot locally (listener + timers). Does NOT log out of Facebook unless `allowFacebookLogout` is true in fca-config.json. */
205
+ logout: (callback?: (err?: Error | null, result?: { localOnly: true }) => void) => Promise<{ localOnly: true } | void>;
206
+ /** Real Facebook logout (POST logout.php). Also ends the session in your browser. */
207
+ logoutFromFacebook: (callback?: (err?: Error) => void) => Promise<void>;
204
208
 
205
209
  // Message Scheduler
206
210
  scheduler: {
package/module/config.js CHANGED
@@ -5,6 +5,10 @@ const defaultConfig = {
5
5
  autoUpdate: false,
6
6
  // Opt-in: tell the user when a newer npm version exists (one small request per login).
7
7
  checkForUpdates: false,
8
+ // api.logout() only stops the bot locally. Set true ONLY if you want api.logout() to
9
+ // really log the account out of Facebook, which also logs you out in your browser.
10
+ // The real logout is always available as api.logoutFromFacebook().
11
+ allowFacebookLogout: false,
8
12
  // Opt-in: when Facebook shows its "automated behavior" warning, press Dismiss for the
9
13
  // user. Off by default: silently clicking through Facebook's own bot warning is itself
10
14
  // an automated action on the account. Dismiss it yourself in the browser instead.
@@ -828,6 +828,16 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
828
828
  }
829
829
  const ctx = { globalOptions, options: globalOptions, reconnectAttempts: 0 };
830
830
  ctx.bypassAutomation = async function (resp, j) {
831
+ // Never submit mutations intended to dismiss/bypass Facebook security warnings.
832
+ // Preserve the response so upstream login validation can fail closed.
833
+ const body = typeof resp?.data === "string" ? resp.data : String(resp?.data ?? "");
834
+ const url = resp?.request?.res?.responseUrl || resp?.config?.url || "";
835
+ if (/601051028565049|XCheckpointFBScrapingWarningController|automated.?behavior|checkpoint\//i.test(`${url} ${body}`)) {
836
+ logger("Facebook security/automation checkpoint detected; not bypassing it. Stop the bot and review the account manually.", "error");
837
+ const err = new Error("Facebook Checkpoint/security warning detected; automatic recovery stopped");
838
+ err.code = "FACEBOOK_SECURITY_CHECKPOINT";
839
+ throw err;
840
+ }
831
841
  global.fca = global.fca || {};
832
842
  global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
833
843
  const s = x => (typeof x === "string" ? x : String(x ?? ""));
@@ -878,7 +888,8 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
878
888
  }
879
889
  return first;
880
890
  } catch (e) {
881
- logger(`Bypass automation error: ${e && e.message ? e.message : String(e)}`, "error");
891
+ logger(`Facebook security handling stopped: ${e && e.message ? e.message : String(e)}`, "error");
892
+ if (e && e.code === "FACEBOOK_SECURITY_CHECKPOINT") throw e;
882
893
  return resp;
883
894
  }
884
895
  };
@@ -917,6 +928,15 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
917
928
  const ctx = {};
918
929
  ctx.options = globalOptions;
919
930
  ctx.bypassAutomation = async function (resp, j) {
931
+ // Never submit mutations intended to dismiss/bypass Facebook security warnings.
932
+ const body = typeof resp?.data === "string" ? resp.data : String(resp?.data ?? "");
933
+ const url = resp?.request?.res?.responseUrl || resp?.config?.url || "";
934
+ if (/601051028565049|XCheckpointFBScrapingWarningController|automated.?behavior|checkpoint\//i.test(`${url} ${body}`)) {
935
+ logger("Facebook security/automation checkpoint detected; not bypassing it. Stop the bot and review the account manually.", "error");
936
+ const err = new Error("Facebook Checkpoint/security warning detected; automatic recovery stopped");
937
+ err.code = "FACEBOOK_SECURITY_CHECKPOINT";
938
+ throw err;
939
+ }
920
940
  global.fca = global.fca || {};
921
941
  global.fca.BypassAutomationNotification = this.bypassAutomation.bind(this);
922
942
  const s = x => (typeof x === "string" ? x : String(x ?? ""));
@@ -954,7 +974,8 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
954
974
  }
955
975
  logger("✅ No checkpoint detected — session is clean.", "info");
956
976
  return res;
957
- } catch {
977
+ } catch (e) {
978
+ if (e && e.code === "FACEBOOK_SECURITY_CHECKPOINT") throw e;
958
979
  return res;
959
980
  }
960
981
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hridoy-ultimate",
3
- "version": "9.0.3",
3
+ "version": "9.0.4",
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",
@@ -25,7 +25,7 @@
25
25
  "src/app/"
26
26
  ],
27
27
  "scripts": {
28
- "test": "mocha",
28
+ "test": "node --test",
29
29
  "lint": "eslint ."
30
30
  },
31
31
  "repository": {
@@ -76,19 +76,22 @@
76
76
  "chalk": "^4.1.2",
77
77
  "cheerio": "1.0.0-rc.12",
78
78
  "duplexify": "^4.1.3",
79
+ "form-data": "^4.0.0",
79
80
  "gradient-string": "^2.0.2",
80
81
  "https-proxy-agent": "^4.0.0",
82
+ "koffi": "^3.0.2",
83
+ "mime": "^3.0.0",
81
84
  "mqtt": "^5.10.1",
85
+ "@noble/curves": "^2.2.0",
86
+ "@noble/hashes": "^2.2.0",
87
+ "protobufjs": "^8.4.0",
88
+ "request": "^2.88.2",
82
89
  "sequelize": "^6.37.6",
90
+ "@signalapp/libsignal-client": "0.70.0",
83
91
  "sqlite3": "^5.1.7",
84
92
  "totp-generator": "^1.0.0",
93
+ "tough-cookie": "^5.0.0",
85
94
  "ws": "^8.18.0",
86
- "@noble/curves": "^2.2.0",
87
- "@noble/hashes": "^2.2.0",
88
- "@signalapp/libsignal-client": "0.70.0",
89
- "mime": "^3.0.0",
90
- "protobufjs": "^8.4.0",
91
- "koffi": "^3.0.2",
92
95
  "yumi-json-bigint": "^1.0.0"
93
96
  },
94
97
  "devDependencies": {
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ const { globalAntiSuspension } = require("../../utils/antiSuspension");
4
+
5
+ /**
6
+ * api.getSafetyStatus() -> {
7
+ * blocked, reason, coolingDown, warmup, hourSendCount, hourlyCap, daySendCount, dailyCap
8
+ * }
9
+ * `blocked` is true after Facebook showed a checkpoint / automated-behavior / disabled
10
+ * signal: sendMessage then rejects with err.code === "ACCOUNT_SAFETY_PAUSED" until the
11
+ * process is restarted after you reviewed the account by hand.
12
+ */
13
+ module.exports = function (defaultFuncs, api, ctx) {
14
+ return function getSafetyStatus() {
15
+ const st = globalAntiSuspension.getStats();
16
+ return {
17
+ blocked: !!st.safetyBlocked,
18
+ reason: st.safetyReason || null,
19
+ coolingDown: !!st.coolingDown,
20
+ warmup: !!st.warmup,
21
+ hourSendCount: st.hourSendCount,
22
+ hourlyCap: st.hourlyCap,
23
+ daySendCount: st.daySendCount,
24
+ dailyCap: st.dailyCap
25
+ };
26
+ };
27
+ };
@@ -1,75 +1,86 @@
1
1
  "use strict";
2
2
 
3
+ /**
4
+ * logout - stop this bot session WITHOUT logging the account out of Facebook.
5
+ *
6
+ * Older FCA builds POSTed facebook.com/logout.php here. That ends the session
7
+ * server-side, so the same account is logged out in your own browser as well,
8
+ * which is the opposite of what a bot owner wants when they just stop the bot.
9
+ *
10
+ * Default behaviour (since 9.0.5): local stop only
11
+ * - stops the MQTT listener,
12
+ * - cancels the fb_dtsg refresh timer,
13
+ * - stops sessionGuard's timer,
14
+ * - never sends anything to Facebook.
15
+ *
16
+ * The real Facebook logout is api.logoutFromFacebook(). It can also be made the
17
+ * behaviour of api.logout() by setting `"allowFacebookLogout": true` in
18
+ * fca-config.json (explicit opt-in).
19
+ */
20
+
3
21
  const log = require("../../../func/logAdapter");
4
- const { parseAndCheckLogin } = require("../../utils/client");
5
- const { getFrom } = require("../../utils/constants");
6
- const { saveCookies } = require("../../utils/client");
7
- module.exports = function(defaultFuncs, api, ctx) {
22
+ const { loadConfig } = require("../../../module/config");
23
+
24
+ module.exports = function (defaultFuncs, api, ctx) {
25
+ function stopListener() {
26
+ return new Promise(resolve => {
27
+ let done = false;
28
+ const finish = () => { if (!done) { done = true; resolve(); } };
29
+ try {
30
+ if (typeof api.stopListening === "function") {
31
+ api.stopListening(finish);
32
+ // never hang a shutdown on a listener that was never started
33
+ setTimeout(finish, 6000).unref?.();
34
+ } else {
35
+ finish();
36
+ }
37
+ } catch (_) {
38
+ finish();
39
+ }
40
+ });
41
+ }
42
+
43
+ function stopTimers() {
44
+ try {
45
+ if (ctx._dtsgRefreshInterval) {
46
+ clearInterval(ctx._dtsgRefreshInterval);
47
+ ctx._dtsgRefreshInterval = null;
48
+ }
49
+ } catch (_) { /* ignore */ }
50
+ try {
51
+ if (typeof api.stopSessionGuard === "function") api.stopSessionGuard();
52
+ } catch (_) { /* ignore */ }
53
+ }
54
+
8
55
  return function logout(callback) {
9
- let resolveFunc = function() {};
10
- let rejectFunc = function() {};
11
- const returnPromise = new Promise(function(resolve, reject) {
56
+ let resolveFunc = function () {};
57
+ let rejectFunc = function () {};
58
+ const returnPromise = new Promise(function (resolve, reject) {
12
59
  resolveFunc = resolve;
13
60
  rejectFunc = reject;
14
61
  });
15
62
 
16
- if (!callback) {
17
- callback = function(err, friendList) {
18
- if (err) {
19
- return rejectFunc(err);
20
- }
21
- resolveFunc(friendList);
63
+ if (typeof callback !== "function") {
64
+ callback = function (err, result) {
65
+ if (err) return rejectFunc(err);
66
+ resolveFunc(result);
22
67
  };
23
68
  }
24
69
 
25
- const form = {
26
- pmid: "0"
27
- };
28
-
29
- defaultFuncs
30
- .post(
31
- "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
32
- ctx.jar,
33
- form
34
- )
35
- .then(parseAndCheckLogin(ctx, defaultFuncs))
36
- .then(function(resData) {
37
- const elem = resData.jsmods.instances[0][2][0].filter(function(v) {
38
- return v.value === "logout";
39
- })[0];
40
-
41
- const html = resData.jsmods.markup.filter(function(v) {
42
- return v[0] === elem.markup.__m;
43
- })[0][1].__html;
44
-
45
- const form = {
46
- fb_dtsg: getFrom(html, '"fb_dtsg" value="', '"'),
47
- ref: getFrom(html, '"ref" value="', '"'),
48
- h: getFrom(html, '"h" value="', '"')
49
- };
50
-
51
- return defaultFuncs
52
- .post("https://www.facebook.com/logout.php", ctx.jar, form)
53
- .then(saveCookies(ctx.jar));
54
- })
55
- .then(function(res) {
56
- if (!res.headers) {
57
- throw { error: "An error occurred when logging out." };
58
- }
70
+ const { config } = loadConfig();
71
+ if (config && config.allowFacebookLogout === true && typeof api.logoutFromFacebook === "function") {
72
+ log.warn("logout", "allowFacebookLogout is true: logging out of Facebook for real (your browser session ends too).");
73
+ api.logoutFromFacebook(callback);
74
+ return returnPromise;
75
+ }
59
76
 
60
- return defaultFuncs
61
- .get(res.headers.location, ctx.jar)
62
- .then(saveCookies(ctx.jar));
63
- })
64
- .then(function() {
65
- ctx.loggedIn = false;
66
- log.info("logout", "Logged out successfully.");
67
- callback();
77
+ log.info("logout", "Stopping locally. The Facebook session is NOT ended (use api.logoutFromFacebook() for a real logout).");
78
+ stopListener()
79
+ .then(() => {
80
+ stopTimers();
81
+ callback(null, { localOnly: true });
68
82
  })
69
- .catch(function(err) {
70
- log.error("logout", err);
71
- return callback(err);
72
- });
83
+ .catch(err => callback(err));
73
84
 
74
85
  return returnPromise;
75
86
  };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * logoutFromFacebook - REAL Facebook logout.
5
+ *
6
+ * This POSTs facebook.com/logout.php with the session cookies. Facebook then
7
+ * invalidates that session everywhere it is used, so the same account is also
8
+ * logged out in your normal browser. It is NOT called by api.logout() unless you
9
+ * opt in with `allowFacebookLogout: true` in fca-config.json. Call it directly
10
+ * only when you really want to end the Facebook session.
11
+ */
12
+
13
+ const log = require("../../../func/logAdapter");
14
+ const { parseAndCheckLogin } = require("../../utils/client");
15
+ const { getFrom } = require("../../utils/constants");
16
+ const { saveCookies } = require("../../utils/client");
17
+ module.exports = function(defaultFuncs, api, ctx) {
18
+ return function logoutFromFacebook(callback) {
19
+ let resolveFunc = function() {};
20
+ let rejectFunc = function() {};
21
+ const returnPromise = new Promise(function(resolve, reject) {
22
+ resolveFunc = resolve;
23
+ rejectFunc = reject;
24
+ });
25
+
26
+ if (!callback) {
27
+ callback = function(err, friendList) {
28
+ if (err) {
29
+ return rejectFunc(err);
30
+ }
31
+ resolveFunc(friendList);
32
+ };
33
+ }
34
+
35
+ const form = {
36
+ pmid: "0"
37
+ };
38
+
39
+ defaultFuncs
40
+ .post(
41
+ "https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
42
+ ctx.jar,
43
+ form
44
+ )
45
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
46
+ .then(function(resData) {
47
+ const elem = resData.jsmods.instances[0][2][0].filter(function(v) {
48
+ return v.value === "logout";
49
+ })[0];
50
+
51
+ const html = resData.jsmods.markup.filter(function(v) {
52
+ return v[0] === elem.markup.__m;
53
+ })[0][1].__html;
54
+
55
+ const form = {
56
+ fb_dtsg: getFrom(html, '"fb_dtsg" value="', '"'),
57
+ ref: getFrom(html, '"ref" value="', '"'),
58
+ h: getFrom(html, '"h" value="', '"')
59
+ };
60
+
61
+ return defaultFuncs
62
+ .post("https://www.facebook.com/logout.php", ctx.jar, form)
63
+ .then(saveCookies(ctx.jar));
64
+ })
65
+ .then(function(res) {
66
+ if (!res.headers) {
67
+ throw { error: "An error occurred when logging out." };
68
+ }
69
+
70
+ return defaultFuncs
71
+ .get(res.headers.location, ctx.jar)
72
+ .then(saveCookies(ctx.jar));
73
+ })
74
+ .then(function() {
75
+ ctx.loggedIn = false;
76
+ log.info("logoutFromFacebook", "Logged out of Facebook (this also ends the browser session).");
77
+ callback();
78
+ })
79
+ .catch(function(err) {
80
+ log.error("logoutFromFacebook", err);
81
+ return callback(err);
82
+ });
83
+
84
+ return returnPromise;
85
+ };
86
+ };
@@ -12,7 +12,7 @@ module.exports = function (defaultFuncs, api, ctx) {
12
12
  }
13
13
  if (!obj) obj = {};
14
14
  if (getType(obj) !== "Object") {
15
- throw new CustomError("The first parameter must be an object or a callback function");
15
+ throw new Error("The first parameter must be an object or a callback function");
16
16
  }
17
17
  let resolveFunc, rejectFunc;
18
18
  const returnPromise = new Promise((resolve, reject) => {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { generateOfflineThreadingID } = require("../../utils/format");
4
4
  const log = require("../../../func/logAdapter");
5
+ const { parseAndCheckLogin } = require("../../utils/client");
5
6
 
6
7
  module.exports = function (defaultFuncs, api, ctx) {
7
8
  function handleUpload(image) {
@@ -119,7 +119,10 @@ module.exports = function (defaultFuncs, api, ctx) {
119
119
  form.signatureID = utils.getGUID().replace(/-/g, "").slice(0, 8);
120
120
  const doSend = () => sendContent(form, threadID, isSingleUser, messageAndOTID, callback);
121
121
  if (!ctx.globalOptions || ctx.globalOptions.antiBan !== false) {
122
- globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(doSend);
122
+ globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(err => {
123
+ if (typeof callback === "function") return callback(err);
124
+ return undefined;
125
+ });
123
126
  } else {
124
127
  doSend();
125
128
  }
@@ -254,7 +254,10 @@ module.exports = function (defaultFuncs, api, ctx) {
254
254
  try {
255
255
  await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || "");
256
256
  } catch (err) {
257
- logger("AntiSuspension prepareBeforeMessage error: " + (err && err.message ? err.message : err), "warn");
257
+ logger.error("sendMessage", "Outbound send blocked by safety guard: " + (err && err.message ? err.message : err));
258
+ if (callback) callback(err);
259
+ else reject(err);
260
+ return promise;
258
261
  }
259
262
  }
260
263
 
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const log = require("../../../func/logAdapter");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
4
5
  const { formatID } = require("../../utils/format");
5
6
  function formatData(data) {
6
7
  return {
@@ -88,6 +88,7 @@ function createFallbackNamespaces(api) {
88
88
  getCurrentUserID: bindOptionalMethod(api, "getCurrentUserID"),
89
89
  handleFriendRequest: bindOptionalMethod(api, "handleFriendRequest"),
90
90
  logout: bindOptionalMethod(api, "logout"),
91
+ logoutFromFacebook: bindOptionalMethod(api, "logoutFromFacebook"),
91
92
  refreshDtsg: bindOptionalMethod(api, "refreshFb_dtsg"),
92
93
  changeBlockedStatus: bindOptionalMethod(api, "changeBlockedStatus"),
93
94
  setOptions: bindOptionalMethod(api, "setOptions"),
@@ -6,17 +6,21 @@ const log = {
6
6
  };
7
7
 
8
8
  // Phrases Facebook responses use when it thinks this account is being
9
- // operated by a bot / script rather than a human. If any of these show up
10
- // in a response body or an error message, we treat it as a signal that
11
- // current traffic looks automated and should slow down / back off.
12
- const SUSPICION_PHRASES = [
9
+ // operated by a bot / script rather than a human, or when it is restricting it.
10
+ //
11
+ // HARD signals mean the account itself is flagged / locked / needs a human
12
+ // (checkpoint, automated-behavior warning, disabled, identity check). On these
13
+ // the guard FAILS CLOSED: outbound sending stops until the process is restarted
14
+ // after a manual review of the account.
15
+ //
16
+ // SOFT signals are rate-limit style responses ("too many requests", "rate
17
+ // limited", "temporarily blocked" ...). They are usually transient, so they only
18
+ // count as strikes and trigger the slow-down cooldown. Treating them as hard
19
+ // signals made one passing 429-style error silence the bot for good.
20
+ const HARD_SUSPICION_PHRASES = [
13
21
  "checkpoint",
14
22
  "automated behavior",
15
23
  "automated_behavior",
16
- "unusual activity",
17
- "unusual_activity",
18
- "suspicious activity",
19
- "action_required",
20
24
  "account_locked",
21
25
  "account locked",
22
26
  "account has been disabled",
@@ -26,6 +30,17 @@ const SUSPICION_PHRASES = [
26
30
  "bot detected",
27
31
  "bot_detected",
28
32
  "not a human",
33
+ "verify your account",
34
+ "confirm your identity",
35
+ "confirm it's you",
36
+ "security check required"
37
+ ];
38
+
39
+ const SOFT_SUSPICION_PHRASES = [
40
+ "unusual activity",
41
+ "unusual_activity",
42
+ "suspicious activity",
43
+ "action_required",
29
44
  "spam detected",
30
45
  "spam_detected",
31
46
  "looks like spam",
@@ -35,15 +50,14 @@ const SUSPICION_PHRASES = [
35
50
  "too_many_requests",
36
51
  "temporarily blocked",
37
52
  "feature temporarily unavailable",
38
- "verify your account",
39
- "confirm your identity",
40
- "confirm it's you",
41
- "security check required",
42
53
  "policy violation",
43
54
  "action blocked",
44
55
  "you're blocked from"
45
56
  ];
46
57
 
58
+ // Kept for backwards compatibility with anything that imported the old list.
59
+ const SUSPICION_PHRASES = HARD_SUSPICION_PHRASES.concat(SOFT_SUSPICION_PHRASES);
60
+
47
61
  /**
48
62
  * Tracks send volume and paces outbound traffic so it doesn't look like a
49
63
  * script firing messages as fast as the network allows. Three layers:
@@ -72,13 +86,18 @@ class SendPacer {
72
86
  this.dayWindowStart = Date.now();
73
87
  this.daySendCount = 0;
74
88
 
89
+ this.accountSafetyBlocked = false;
90
+ this.accountSafetyReason = null;
91
+
75
92
  this.cooldown = {
76
93
  active: false,
77
94
  since: null,
78
95
  durationMs: 45 * 60 * 1000,
79
96
  strikes: 0,
80
- strikesToActivate: 2
97
+ strikesToActivate: 2,
98
+ strikeWindowMs: 30 * 60 * 1000
81
99
  };
100
+ this.softStrikeTimes = [];
82
101
 
83
102
  this._gcTimer = setInterval(() => this._rollWindows(), 60 * 1000);
84
103
  if (this._gcTimer.unref) this._gcTimer.unref();
@@ -108,12 +127,30 @@ class SendPacer {
108
127
  return this._inWarmup() ? this.warmupHourlyCap : this.normalHourlyCap;
109
128
  }
110
129
 
111
- /** Records that a suspicious signal was seen; may activate the cooldown. */
112
- noteSuspicionSignal(source) {
113
- this.cooldown.strikes++;
130
+ /**
131
+ * Records that a suspicious signal was seen.
132
+ * hard=true (default): fail closed - stop sending until a human reviews the account.
133
+ * hard=false: count a strike; two strikes inside strikeWindowMs start the cooldown.
134
+ */
135
+ noteSuspicionSignal(source, hard) {
136
+ const isHard = hard !== false;
137
+ if (isHard) {
138
+ // Fail closed: once Facebook signals checkpoint/automation/disabled status,
139
+ // do not continue sending or retrying in this process. Human review is required.
140
+ if (!this.accountSafetyBlocked) {
141
+ log.warn("AntiSuspension", `Outbound sending paused: ${source || "Facebook security signal"}. Review the account manually, then restart the bot.`);
142
+ }
143
+ this.accountSafetyBlocked = true;
144
+ this.accountSafetyReason = String(source || "Facebook security signal");
145
+ return;
146
+ }
147
+ const now = Date.now();
148
+ this.softStrikeTimes = this.softStrikeTimes.filter(t => now - t < this.cooldown.strikeWindowMs);
149
+ this.softStrikeTimes.push(now);
150
+ this.cooldown.strikes = this.softStrikeTimes.length;
114
151
  if (!this.cooldown.active && this.cooldown.strikes >= this.cooldown.strikesToActivate) {
115
152
  this.cooldown.active = true;
116
- this.cooldown.since = Date.now();
153
+ this.cooldown.since = now;
117
154
  log.warn("AntiSuspension", `Cooldown activated after repeated suspicion signals (last: ${source || "unknown"}). Backing off for ${(this.cooldown.durationMs / 60000).toFixed(0)} min.`);
118
155
  }
119
156
  }
@@ -122,9 +159,17 @@ class SendPacer {
122
159
  scanForSuspicion(text) {
123
160
  if (!text || typeof text !== "string") return false;
124
161
  const lower = text.toLowerCase();
125
- const hit = SUSPICION_PHRASES.find(p => lower.includes(p));
126
- if (hit) this.noteSuspicionSignal(hit);
127
- return !!hit;
162
+ const hardHit = HARD_SUSPICION_PHRASES.find(p => lower.includes(p));
163
+ if (hardHit) {
164
+ this.noteSuspicionSignal(hardHit, true);
165
+ return true;
166
+ }
167
+ const softHit = SOFT_SUSPICION_PHRASES.find(p => lower.includes(p));
168
+ if (softHit) {
169
+ this.noteSuspicionSignal(softHit, false);
170
+ return true;
171
+ }
172
+ return false;
128
173
  }
129
174
 
130
175
  /** Inspects a thrown error's message for suspicion phrases. */
@@ -138,6 +183,7 @@ class SendPacer {
138
183
  if (Date.now() - this.cooldown.since >= this.cooldown.durationMs) {
139
184
  this.cooldown.active = false;
140
185
  this.cooldown.strikes = 0;
186
+ this.softStrikeTimes = [];
141
187
  this.cooldown.since = null;
142
188
  return false;
143
189
  }
@@ -151,6 +197,11 @@ class SendPacer {
151
197
  * rather than blocking a real message.
152
198
  */
153
199
  async waitForTurn(threadID) {
200
+ if (this.accountSafetyBlocked) {
201
+ const err = new Error(`Outbound messaging paused after Facebook security signal: ${this.accountSafetyReason}. Review the account manually before restarting.`);
202
+ err.code = "ACCOUNT_SAFETY_PAUSED";
203
+ throw err;
204
+ }
154
205
  try {
155
206
  this._rollWindows();
156
207
 
@@ -185,8 +236,10 @@ class SendPacer {
185
236
  this.daySendCount++;
186
237
  const threadCount = (this.threadSendCounts.get(key) || 0) + 1;
187
238
  this.threadSendCounts.set(key, threadCount);
188
- } catch {
189
- // Pacing must never be the reason a real message fails to send.
239
+ } catch (err) {
240
+ // Safety-related errors must not be swallowed.
241
+ if (err && err.code === "ACCOUNT_SAFETY_PAUSED") throw err;
242
+ log.warn("AntiSuspension", `Pacing check failed: ${err && err.message ? err.message : String(err)}`);
190
243
  }
191
244
  }
192
245
 
@@ -201,7 +254,9 @@ class SendPacer {
201
254
  hourlyCap: this._hourlyCap(),
202
255
  daySendCount: this.daySendCount,
203
256
  dailyCap: this.dailyCap,
204
- coolingDown: this.isCoolingDown()
257
+ coolingDown: this.isCoolingDown(),
258
+ safetyBlocked: this.accountSafetyBlocked,
259
+ safetyReason: this.accountSafetyReason
205
260
  };
206
261
  }
207
262
  }
@@ -210,10 +265,15 @@ const pacer = new SendPacer();
210
265
 
211
266
  module.exports = {
212
267
  SendPacer,
268
+ HARD_SUSPICION_PHRASES,
269
+ SOFT_SUSPICION_PHRASES,
270
+ SUSPICION_PHRASES,
213
271
  globalAntiSuspension: {
214
272
  prepareBeforeMessage: (threadID) => pacer.waitForTurn(threadID),
215
273
  detectSuspensionSignal: (text) => pacer.scanForSuspicion(text),
216
274
  checkAccountHealth: (err) => pacer.noteSendError(err),
217
- getStats: () => pacer.stats()
275
+ getStats: () => pacer.stats(),
276
+ isAccountSafetyBlocked: () => pacer.accountSafetyBlocked,
277
+ getAccountSafetyReason: () => pacer.accountSafetyReason
218
278
  }
219
279
  };
@@ -2,6 +2,28 @@
2
2
 
3
3
  // Cookie helpers extracted from client.js
4
4
 
5
+ const AUTH_COOKIES = new Set(["c_user", "xs", "i_user", "datr", "sb", "fr"]);
6
+
7
+ // True for a Set-Cookie header that deletes one of the login cookies
8
+ // (empty / "deleted" value, Max-Age<=0, or an expiry in the past).
9
+ function isAuthCookieDeletion(setCookieHeader) {
10
+ const first = String(setCookieHeader || "").split(";")[0];
11
+ const eq = first.indexOf("=");
12
+ if (eq <= 0) return false;
13
+ const name = first.slice(0, eq).trim();
14
+ if (!AUTH_COOKIES.has(name)) return false;
15
+ const value = first.slice(eq + 1).trim().replace(/^"|"$/g, "");
16
+ if (value === "" || /^deleted$/i.test(value)) return true;
17
+ const maxAge = /;\s*max-age\s*=\s*(-?\d+)/i.exec(setCookieHeader);
18
+ if (maxAge && parseInt(maxAge[1], 10) <= 0) return true;
19
+ const expires = /;\s*expires\s*=\s*([^;]+)/i.exec(setCookieHeader);
20
+ if (expires) {
21
+ const t = Date.parse(expires[1]);
22
+ if (!Number.isNaN(t) && t <= Date.now()) return true;
23
+ }
24
+ return false;
25
+ }
26
+
5
27
  function saveCookies(jar) {
6
28
  return res => {
7
29
  try {
@@ -29,6 +51,11 @@ function saveCookies(jar) {
29
51
  const mirrored = c.replace(/domain=\.?facebook\.com/i, "domain=.messenger.com");
30
52
  jar.setCookieSync(mirrored, "https://www.messenger.com");
31
53
  } else if (/domain=\.?messenger\.com/i.test(c)) {
54
+ // A messenger.com "delete this cookie" answer must never wipe the matching
55
+ // facebook.com login cookie in our jar: that would silently kill a perfectly
56
+ // good facebook.com session (the cookie values are mirrored, so messenger.com
57
+ // may well reject them while facebook.com is fine).
58
+ if (isAuthCookieDeletion(c)) continue;
32
59
  const mirrored = c.replace(/domain=\.?messenger\.com/i, "domain=.facebook.com");
33
60
  jar.setCookieSync(mirrored, "https://www.facebook.com");
34
61
  }
@@ -80,6 +107,7 @@ function getAppState(jar) {
80
107
 
81
108
  module.exports = {
82
109
  saveCookies,
83
- getAppState
110
+ getAppState,
111
+ isAuthCookieDeletion
84
112
  };
85
113
 
@@ -186,25 +186,7 @@ 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
- 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
- }
189
+ logger("Facebook automated-behavior warning detected. Automatic dismissal is disabled; stop the bot and review the account manually.", "error");
208
190
  // This is NOT a logged-out session. The old code ran the "session expired" path here,
209
191
  // which made the bot declare itself logged out and restart (a fresh login) for nothing.
210
192
  const err = new Error("Facebook showed an automated-behavior warning (needs a human to dismiss it)");