toru-fca 1.0.8 → 1.1.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 CHANGED
@@ -1,42 +1,114 @@
1
- # CHANGELOG — toru-fca
1
+ # toru-fca Changelog
2
2
 
3
- ## v1.0.6 — Ultimate Fixed, Advanced & Merged Release
3
+ ## v1.1.0 — Audit, Repair & Stability Upgrade
4
4
 
5
- ### 🔴 BROWSER LOGOUT FIX (ROOT CAUSE — FULLY FIXED)
5
+ > Full professional audit and repair of all critical, high, and medium issues.
6
6
 
7
- **কারণ:** `forceLogin: true` ছিল।
8
- - Facebook checkpoint এ "This was me" auto-submit করত
9
- - Facebook নতুন session তৈরি করত → পুরনো browser session invalidate → **browser logout**
7
+ ### 🔴 Critical Fixes
10
8
 
11
- **Fix (3 স্তরে):**
12
- 1. `forceLogin: false` — checkpoint auto-submit বন্ধ → browser session অক্ষুণ্ণ
13
- 2. Cookie expiry minimum **1 বছর** — expire হলে session drop থেকে সুরক্ষা
14
- 3. SessionGuard হর 3 মিনিটে **cookie expiry extend** — bot চলার সময় কখনো expire হবে না
9
+ #### `sendMessage.js` — Wrong global config key
10
+ - **Bug**: `global._alphaFcaConfig` was read to check `inbox.enabled`. This global is never set — toru-fca uses `global._toruFcaConfig`. The inbox-disabled guard silently never fired.
11
+ - **Fix**: Changed to `global._toruFcaConfig`. Inbox guard now works correctly.
15
12
 
16
- ### ✅ সব Fix একসাথে
13
+ #### `index.js` — Triple-wrapping of `api.sendMessage`
14
+ - **Bug**: `patchSmooth()` and `initFacebookProtection()` each independently re-wrapped `api.sendMessage`, creating three nested layers. This caused:
15
+ - Inconsistent Promise/callback contract on synchronous throws from inner layers
16
+ - `api.send` alias reassigned twice, creating divergence risk
17
+ - Undocumented stacked delays (300ms + 50–200ms + typing duration)
18
+ - **Fix**: Replaced both separate wrappers with a single unified `patchSendWrapper()` that applies human jitter → smooth delay → original `sendMessage` in one clean Promise chain. `api.send` is now assigned exactly once.
17
19
 
18
- | File | Fix |
20
+ #### `index.js` / `config.json` — E2EE enabled by default without native binary
21
+ - **Bug**: `config.json` had `"e2ee": { "enable": true }` and the fallback defaults also had `enable: true`. The `lib/index.mjs` ESM bundle throws `Error: Native library not found at .../build/messagix.so` on load because the `build/` directory is not included in the package. This caused E2EE init to fail on every startup with a caught but opaque error.
22
+ - **Fix**: `e2ee.enable` defaults to `false` in both `config.json` and the fallback defaults. E2EE must be explicitly enabled by users who have compiled the native binary.
23
+
24
+ #### `listenMqtt.js` — Module-scope `getSeqID` mutation (cross-instance contamination)
25
+ - **Bug**: `var getSeqID = function(){};` was declared at module scope and then reassigned inside the exported factory. Multiple login instances in the same process would overwrite each other's `getSeqID` closure, causing the wrong instance's callback to receive another bot's events.
26
+ - **Fix**: `getSeqID` and `form` are now declared inside the exported factory function (one per instance). Each login gets its own isolated closure.
27
+
28
+ #### Version inconsistency across all files
29
+ - **Bug**: `package.json` said `1.0.9`, `config.json` said `1.0.8`, `index.js`/`checkUpdate.js`/MQTT banner all said `1.0.6`. Target is `1.1.0`.
30
+ - **Fix**: All version strings unified to `1.1.0` across `package.json`, `config.json`, `checkUpdate.js`, `index.js` header, and the MQTT connect banner.
31
+
32
+ ---
33
+
34
+ ### 🔴 High-Priority Fixes
35
+
36
+ #### `sendTypingIndicator.js` — Local counter always sends `request_id: 1`
37
+ - **Bug**: `let count_req = 0` was declared inside the async function, so `++count_req` always yielded `1`. Every typing indicator request sent the same MQTT request ID, potentially confusing the ack system.
38
+ - **Fix**: Now uses `ctx.wsReqNumber` (the shared per-session counter, same as `sendTypingIndicatorV2`) incremented before each publish. Request IDs are now unique across all `ls_req` calls.
39
+
40
+ #### `sendTypingIndicatorV2.js` — Redundant duplicate implementation
41
+ - **Bug**: Two separate typing indicator implementations with inconsistent counter strategies.
42
+ - **Fix**: `sendTypingIndicatorV2` now delegates to `sendTypingIndicator`. Both `api.sendTypingIndicator` and `api.sendTypingIndicatorV2` use the same fixed implementation.
43
+
44
+ #### `stopListenMqtt.js` — Dead-connection watchdog not cleared on stop
45
+ - **Bug**: `stopListenMqtt()` ended the MQTT client but did not clear `_deadTimer` (the 8-minute watchdog). The watchdog was a local variable inside `listenMqtt()` — `stopListenMqtt.js` had no reference to it. After calling `stopListenMqtt()`, the watchdog could still fire and attempt a reconnect on a stopped bot.
46
+ - **Fix**: Watchdog timer now stored on `ctx._deadTimer` (accessible to all modules with a `ctx` reference). Both `stopListenMqtt()` and `stopListening()` now clear `ctx._deadTimer` before teardown.
47
+
48
+ #### `listenMqtt.js` — `stopListening` referenced undefined `_deadTimer`
49
+ - **Bug**: `stopListening` in `MessageEmitter` tried `if (typeof _deadTimer !== 'undefined' && _deadTimer)` — this was correct when the watchdog was local, but needed updating to match the new `ctx._deadTimer` approach.
50
+ - **Fix**: Updated to `if (ctx._deadTimer)` consistently.
51
+
52
+ #### `utils.js` — `parseAndCheckLogin` retry sends wrong body on 5xx
53
+ - **Bug**: Line 2611: `defaultFuncs.post(url, ctx.jar, data.request.formData)` — for standard POST requests `data.request.formData` is `undefined` (that property is only set for multipart uploads). All non-upload 5xx retries silently sent an empty body.
54
+ - **Fix**: Retry now uses `data.request.form || data.request.formData || {}` — correctly using `form` (the standard POST body property set by the `request` library) and falling back to `formData` for multipart, then to empty object as a last resort.
55
+
56
+ #### `index.js` — `forceLogout` used fb_dtsg as the `h` URL parameter
57
+ - **Bug**: `logout.php?h=<fb_dtsg>` — the `h` parameter on Facebook's logout endpoint is a separate logout nonce, not the CSRF token. Passing `fb_dtsg` there caused logout requests to silently fail.
58
+ - **Fix**: POSTs to `logout.php` (no `h` param) with `fb_dtsg` in the body only. Also adds graceful handling: marks `ctx.loggedIn = false` even when the HTTP request fails, so the bot's internal state stays consistent.
59
+
60
+ ---
61
+
62
+ ### 🟡 Medium Fixes
63
+
64
+ #### `refreshFb_dtsg.js` — Broad regex pattern ordered too early
65
+ - **Bug**: Pattern `/"token":"([^"]+)"/` (matches any JSON `token` key) was #3 in the list, potentially matching unrelated tokens before the specific DTSG patterns.
66
+ - **Fix**: Moved broad pattern to last position. Added minimum length check (`m[1].length > 4`) to filter out short false-match values.
67
+
68
+ #### `index.js` — `sessionGuard` jar monkey-patch fragile against internal API changes
69
+ - **Bug**: `jar._jar` is a private internal property of `tough-cookie`. If absent (different tough-cookie version, custom jar, or API change), the patch silently did nothing with no warning.
70
+ - **Fix**: Wrapped in try/catch with a `log.verbose` warning when `jar._jar` is not available. The interval-based save still runs — only the debounce-on-cookie-change is affected.
71
+
72
+ #### `examples/basic-bot.js` — Shutdown called `api.stopListenMqtt()` without clearing watchdog
73
+ - **Bug**: `api.stopListenMqtt()` was called in the shutdown handler. While this now clears `ctx._deadTimer` (fixed above), `api.stopListening()` is the preferred method when `listenMqtt()` was used directly, as it also resets the global callback and handles the `MessageEmitter` lifecycle.
74
+ - **Fix**: Shutdown now calls `api.stopListening(callback)` first, with `api.stopListenMqtt()` as a fallback. Also added guard against double-shutdown (`_shutdownCalled` flag). Stops session guard timer properly.
75
+
76
+ ---
77
+
78
+ ### 🟢 Low / Optimization
79
+
80
+ #### `checkUpdate.js` — Added hard abort guard on version check HTTP request
81
+ - Added a `setTimeout(destroy, 8000)` as a belt-and-suspenders guard alongside the existing 5000ms socket timeout, preventing the rare case where DNS resolution hangs beyond the socket timeout window.
82
+
83
+ #### `config.json` — Bengali/mixed-language notes replaced with English
84
+ - All `_note` fields now in English for international maintainability.
85
+
86
+ #### `fca-config.example.json` — Updated to v1.1.0 with accurate E2EE guidance
87
+ - Added clear note that E2EE requires native binary. All fields documented.
88
+
89
+ ---
90
+
91
+ ### Files Modified
92
+
93
+ | File | Changes |
19
94
  |---|---|
20
- | `index.js` | forceLogin=false, cookie expiry 1yr, clean rewrite |
21
- | `src/logout.js` | Cleaner no-op with better message |
22
- | `src/refreshFb_dtsg.js` | Better pattern matching, error handling |
23
- | `src/sendTypingIndicatorV2.js` | ctx.mqttClient undefined bug fixed |
24
- | `src/listenMqtt.js` | ForcedFetch DM bug fixed, reaction off+timestamp added |
25
- | `config.json` | facebookProtection added, 3min sessionGuard, 300ms smooth |
26
- | `package.json` | Version 1.0.6 |
27
- | `checkUpdate.js` | Version 1.0.6 |
28
- | `examples/basic-bot.js` | Full rewrite, GoatBot compatible |
29
-
30
- ### Features Active in v1.0.6
31
-
32
- - ✅ Browser logout হবে না (cookie login)
33
- - ✅ Automation restriction আসবে না
34
- - ✅ Inbox/DM এবং Group — উভয়তেই bot reply করবে
35
- - ✅ Reaction detect (off field + timestamp)
36
- - ✅ GoatBot compatible
37
- - ✅ MQTT stable, auto-reconnect
38
- - ✅ E2EE bridge
39
- - ✅ Error-free (no crash)
40
-
41
- ## v1.0.2 — Previous Stable
42
- - Anti-logout, session guard, inbox fix, GoatBot defaults, MQTT patch stream
95
+ | `package.json` | Version → 1.1.0 |
96
+ | `config.json` | Version → 1.1.0, e2ee.enable → false, notes in English |
97
+ | `checkUpdate.js` | CURRENT_VERSION → 1.1.0, hard abort guard |
98
+ | `index.js` | Header version, fallback config version/e2ee, unified sendMessage wrapper, forceLogout fix, sessionGuard defensive patch, logout message updated |
99
+ | `src/sendMessage.js` | Fixed `global._alphaFcaConfig` → `global._toruFcaConfig` |
100
+ | `src/sendTypingIndicator.js` | Fixed local counter (always sent request_id:1), proper error handling, non-fatal when MQTT not connected |
101
+ | `src/sendTypingIndicatorV2.js` | Delegates to sendTypingIndicator; removed duplicate logic |
102
+ | `src/stopListenMqtt.js` | Clears ctx._deadTimer before MQTT teardown |
103
+ | `src/listenMqtt.js` | getSeqID/form moved to instance scope; _deadTimer → ctx._deadTimer; banner version updated; stopListening uses ctx._deadTimer |
104
+ | `src/refreshFb_dtsg.js` | Regex ordering fixed (broad pattern last), min-length guard |
105
+ | `examples/basic-bot.js` | Proper shutdown (stopListening), double-shutdown guard, version updated |
106
+ | `fca-config.example.json` | Version → 1.1.0, E2EE docs updated |
107
+ | `utils.js` | parseAndCheckLogin retry body fix (form vs formData) |
108
+
109
+ ---
110
+
111
+ ## v1.0.9 (previous)
112
+ - Dead connection watchdog added
113
+ - Reaction detection works indefinitely (no longer stops after ~1hr)
114
+ - Browser logout free, anti-automation, GoatBot ready
package/checkUpdate.js CHANGED
@@ -1,23 +1,43 @@
1
1
  "use strict";
2
- const CURRENT_VERSION = "1.0.6";
2
+
3
+ const CURRENT_VERSION = "1.1.0";
3
4
 
4
5
  async function checkForFCAUpdate() {
5
6
  try {
6
7
  const https = require("https");
7
- const data = await new Promise((resolve) => {
8
- const req = https.get("https://registry.npmjs.org/toru-fca/latest", { timeout: 5000 }, (res) => {
9
- let body = "";
10
- res.on("data", (c) => { body += c; });
11
- res.on("end", () => { try { resolve(JSON.parse(body)); } catch (_) { resolve(null); } });
12
- });
13
- req.on("error", () => resolve(null));
14
- req.on("timeout", () => { req.destroy(); resolve(null); });
8
+
9
+ const data = await new Promise(function(resolve) {
10
+ var req = https.get(
11
+ "https://registry.npmjs.org/toru-fca/latest",
12
+ { timeout: 5000 },
13
+ function(res) {
14
+ var body = "";
15
+ res.on("data", function(c) { body += c; });
16
+ res.on("end", function() {
17
+ try { resolve(JSON.parse(body)); } catch (_) { resolve(null); }
18
+ });
19
+ res.on("error", function() { resolve(null); });
20
+ }
21
+ );
22
+ req.on("error", function() { resolve(null); });
23
+ // Destroy on timeout to avoid hanging socket
24
+ req.on("timeout", function() { req.destroy(); resolve(null); });
25
+ // Hard abort after 8s as a belt-and-suspenders guard
26
+ var hard = setTimeout(function() { try { req.destroy(); } catch (_) {} resolve(null); }, 8000);
27
+ if (hard.unref) hard.unref();
15
28
  });
29
+
16
30
  if (data && data.version && data.version !== CURRENT_VERSION) {
17
- const log = require("npmlog");
18
- log.warn("toru-fca", "Update available: v" + CURRENT_VERSION + " → v" + data.version + " — npm update toru-fca");
31
+ var log = require("npmlog");
32
+ log.warn(
33
+ "toru-fca",
34
+ "Update available: v" + CURRENT_VERSION + " \u2192 v" + data.version +
35
+ " \u2014 npm update toru-fca"
36
+ );
19
37
  }
20
- } catch (_) {}
38
+ } catch (_) {
39
+ // Non-fatal; version check is best-effort only
40
+ }
21
41
  }
22
42
 
23
43
  module.exports = { checkForFCAUpdate, CURRENT_VERSION };
package/config.json CHANGED
@@ -1,60 +1,51 @@
1
1
  {
2
- "_info": "toru-fca v1.0.8 — Main Config",
3
- "version": "1.0.8",
4
-
2
+ "_info": "toru-fca v1.1.0 — Main Config",
3
+ "version": "1.1.0",
5
4
  "antiLogout": {
6
5
  "enabled": true,
7
6
  "refreshIntervalMs": 1200000,
8
- "_note": "fb_dtsg হর 20 মিনিটে refresh — session alive রাখে। Browser logout হবে না।"
7
+ "_note": "fb_dtsg refreshed every 20 minutes to keep session alive and prevent browser logout."
9
8
  },
10
-
11
9
  "sessionGuard": {
12
10
  "enabled": true,
13
11
  "intervalMs": 180000,
14
12
  "debounceMs": 20000,
15
13
  "backupEnabled": true,
16
- "_note": "Appstate হর 3 মিনিটে auto-save। .bak backup রাখে। Cookie expiry extend করে।"
14
+ "_note": "Appstate auto-saved every 3 minutes. Keeps a .bak backup. Cookie expiry is extended."
17
15
  },
18
-
19
16
  "automation": {
20
17
  "enabled": false,
21
- "_note": "KEEP FALSE। Background automation বন্ধ রাখে।"
18
+ "_note": "KEEP FALSE. Disables background automation. Protects against Facebook restrictions."
22
19
  },
23
-
24
20
  "smooth": {
25
21
  "enabled": true,
26
22
  "messageDelay": 300,
27
- "_note": "Send করার আগে typing indicator দেখাবে। 300ms delay — human-like behavior।"
23
+ "_note": "Shows typing indicator before sending. 300ms delay for human-like behavior."
28
24
  },
29
-
30
25
  "connection": {
31
26
  "minDelayMs": 3000,
32
27
  "maxDelayMs": 60000,
33
- "_note": "MQTT reconnect backoff range।"
28
+ "_note": "MQTT reconnect exponential backoff range."
34
29
  },
35
-
36
30
  "e2ee": {
37
- "enable": true,
31
+ "enable": false,
38
32
  "saveType": "memory",
39
33
  "devicePath": "./data/e2ee-device.json",
40
34
  "autoReconnect": true,
41
35
  "logLevel": "none",
42
- "_note": "E2EE Labyrinth bridge। DM + group support।"
36
+ "_note": "E2EE Labyrinth bridge. Requires native build/messagix binary. Set enable:true only if binary is present."
43
37
  },
44
-
45
38
  "facebookProtection": {
46
39
  "enabled": true,
47
40
  "blockAutomationDetection": true,
48
41
  "humanLikeDelay": true,
49
- "_note": "online=false, updatePresence=false। Human-like 50-200ms micro-delay। Account restriction থেকে সুরক্ষা।"
42
+ "_note": "Sets online=false, updatePresence=false. Adds 50-200ms micro-delay. Reduces account restriction risk."
50
43
  },
51
-
52
44
  "inbox": {
53
45
  "enabled": true,
54
46
  "replyToInbox": true,
55
- "_note": "Bot inbox/DM এবং group উভয়তেই reply করবে।"
47
+ "_note": "Bot replies in both inbox/DM and group threads."
56
48
  },
57
-
58
49
  "enableTypingIndicator": false,
59
50
  "typingDuration": 4000
60
51
  }
@@ -1,13 +1,13 @@
1
1
  /**
2
- * toru-fca v1.0.6 — basic-bot.js (GoatBot compatible example)
2
+ * toru-fca v1.1.0 — basic-bot.js (GoatBot compatible example)
3
3
  *
4
4
  * Features:
5
- * ✅ Cookie login — browser logout হবে না
6
- * ✅ Inbox + Group — উভয়তেই bot reply করবে
7
- * ✅ Session guard — appstate হর 3 মিনিটে auto-save
8
- * ✅ Anti-logout — fb_dtsg 20 মিনিটে refresh
9
- * ✅ Facebook protection — automation detection bypass
10
- * ✅ Graceful shutdown
5
+ * ✅ Cookie login — browser logout prevention
6
+ * ✅ Inbox + Group — bot replies in both DM and group threads
7
+ * ✅ Session guard — appstate auto-saved every 3 minutes
8
+ * ✅ Anti-logout — fb_dtsg refreshed every 20 minutes
9
+ * ✅ Facebook protection — automation detection guard
10
+ * ✅ Graceful shutdown — cleans up timers, MQTT, and session on exit
11
11
  */
12
12
 
13
13
  "use strict";
@@ -23,18 +23,18 @@ let appState;
23
23
  try {
24
24
  appState = JSON.parse(fs.readFileSync(APPSTATE_FILE, "utf8"));
25
25
  } catch (err) {
26
- console.error("[toru-fca] ❌ Could not load appstate.json:", err.message);
26
+ console.error("[toru-fca] \u274C Could not load appstate.json:", err.message);
27
27
  process.exit(1);
28
28
  }
29
29
 
30
30
  // ── Login options (GoatBot compatible) ───────────────────────────────────────
31
31
  const options = {
32
32
  listenEvents : true,
33
- selfListen : false, // bot নিজের message শুনবে না
33
+ selfListen : false, // bot does not listen to its own messages
34
34
  autoReconnect : true,
35
- online : false, // automation detection bypass
35
+ online : false, // reduces automation detection risk
36
36
  updatePresence : false,
37
- forceLogin : false, // browser logout fix
37
+ forceLogin : false, // browser logout protection
38
38
  autoMarkRead : false,
39
39
  autoMarkDelivery: false,
40
40
  logLevel : "verbose"
@@ -42,48 +42,81 @@ const options = {
42
42
 
43
43
  // ── Login ─────────────────────────────────────────────────────────────────────
44
44
  login({ appState }, options, function (err, api) {
45
- if (err) { console.error("[toru-fca] ❌ Login failed:", err.message || err); process.exit(1); }
45
+ if (err) {
46
+ console.error("[toru-fca] \u274C Login failed:", err.message || err);
47
+ process.exit(1);
48
+ }
46
49
 
47
50
  const botID = api.getCurrentUserID ? api.getCurrentUserID() : "unknown";
48
- console.log("[toru-fca] ✅ Bot running as:", botID);
51
+ console.log("[toru-fca] \u2705 Bot running as:", botID);
49
52
 
50
- // Session guard — auto-save appstate হর 3 মিনিটে
51
- api.sessionGuard(APPSTATE_FILE, { interval: 3 * 60 * 1000, backup: true });
53
+ // Session guard — auto-saves appstate every 3 minutes
54
+ const guard = api.sessionGuard(APPSTATE_FILE, { interval: 3 * 60 * 1000, backup: true });
52
55
 
53
56
  // ── Main listener ─────────────────────────────────────────────────────────
54
57
  api.listenMqtt(function (err, event) {
55
- if (err) { console.error("[listen]", err); return; }
58
+ if (err) {
59
+ console.error("[listen]", err);
60
+ return;
61
+ }
56
62
 
57
- // Group এবং Inbox/DM — উভয়তেই কাজ করবে
63
+ // Respond in both Group and Inbox/DM threads
58
64
  if (event.type === "message") {
59
65
  const { threadID, senderID, body, isGroup } = event;
60
66
  const tag = isGroup ? "[Group]" : "[Inbox/DM]";
61
67
  console.log(tag, senderID + ":", body);
62
68
 
63
- // Echo bot
69
+ // Simple echo bot
64
70
  if (body && body.toLowerCase() === "ping") {
65
- api.sendMessage("🏓 pong! — toru-fca v1.0.6", threadID);
71
+ api.sendMessage("\uD83C\uDFD3 pong! \u2014 toru-fca v1.1.0", threadID);
66
72
  }
67
73
  }
68
74
 
69
- // Reaction detect
75
+ // Reaction events
70
76
  if (event.type === "message_reaction") {
71
- console.log("[Reaction]", event.senderID, event.off ? "removed reaction" : "reacted:", event.reaction);
77
+ console.log(
78
+ "[Reaction]",
79
+ event.senderID,
80
+ event.off ? "removed reaction" : "reacted:",
81
+ event.reaction
82
+ );
72
83
  }
73
84
 
74
- // Events (group join, leave, etc.)
85
+ // Group events (join, leave, name change, etc.)
75
86
  if (event.type === "event") {
76
87
  console.log("[Event]", event.logMessageType || event.type);
77
88
  }
78
89
  });
79
90
 
80
91
  // ── Graceful shutdown ─────────────────────────────────────────────────────
92
+ var _shutdownCalled = false;
81
93
  function shutdown() {
94
+ if (_shutdownCalled) return;
95
+ _shutdownCalled = true;
82
96
  console.log("[toru-fca] Shutting down...");
83
- if (api.stopAntiLogout) api.stopAntiLogout();
84
- if (api.stopListenMqtt) api.stopListenMqtt();
85
- setTimeout(() => process.exit(0), 1000);
97
+
98
+ // Stop anti-logout timer
99
+ if (api.stopAntiLogout) api.stopAntiLogout();
100
+
101
+ // Stop session guard timer
102
+ if (guard && typeof guard.stop === "function") guard.stop();
103
+
104
+ // FIX v1.1.0: use api.stopListening() (from MessageEmitter) which correctly
105
+ // clears the dead-connection watchdog AND the MQTT client.
106
+ // api.stopListenMqtt() also works but stopListening() is preferred here
107
+ // because it was returned by the listen() call and has direct scope access.
108
+ if (typeof api.stopListening === "function") {
109
+ api.stopListening(function() {
110
+ setTimeout(function() { process.exit(0); }, 500);
111
+ });
112
+ } else if (api.stopListenMqtt) {
113
+ api.stopListenMqtt();
114
+ setTimeout(function() { process.exit(0); }, 500);
115
+ } else {
116
+ setTimeout(function() { process.exit(0); }, 500);
117
+ }
86
118
  }
119
+
87
120
  process.once("SIGINT", shutdown);
88
121
  process.once("SIGTERM", shutdown);
89
122
  });
@@ -1,56 +1,66 @@
1
1
  {
2
- "_comment": "alpha-fca v1.0.0 — Example Config. Copy to config.json and customize.",
2
+ "_comment": "toru-fca v1.1.0 — Example Config. Copy to config.json and customize.",
3
3
 
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
 
6
6
  "automation": {
7
7
  "enabled": false,
8
- "_comment": "KEEP FALSE. Enabling this allows background task automation (use with care)."
8
+ "_comment": "KEEP FALSE. Enabling this allows background task automation. Keep false to protect your account."
9
9
  },
10
10
 
11
11
  "inbox": {
12
12
  "enabled": true,
13
- "_comment": "Inbox/getThreadList access. Always true for a working bot."
13
+ "replyToInbox": true,
14
+ "_comment": "Bot replies in both inbox/DM and group threads."
14
15
  },
15
16
 
16
17
  "smooth": {
17
18
  "enabled": true,
18
- "messageDelay": 400,
19
- "_comment": "When true, shows typing indicator before sending message. Set messageDelay (ms) for natural feel. Recommended: false for low automation."
19
+ "messageDelay": 300,
20
+ "_comment": "Adds a short delay before sending. Combines with human jitter (50-200ms) when facebookProtection is enabled."
20
21
  },
21
22
 
22
23
  "connection": {
23
- "minDelayMs": 1000,
24
- "maxDelayMs": 30000,
25
- "_comment": "Reconnect backoff range in milliseconds."
24
+ "minDelayMs": 3000,
25
+ "maxDelayMs": 60000,
26
+ "_comment": "MQTT reconnect exponential backoff range in milliseconds."
27
+ },
28
+
29
+ "antiLogout": {
30
+ "enabled": true,
31
+ "refreshIntervalMs": 1200000,
32
+ "_comment": "Refreshes fb_dtsg CSRF token every 20 minutes to keep session alive."
33
+ },
34
+
35
+ "sessionGuard": {
36
+ "enabled": true,
37
+ "intervalMs": 180000,
38
+ "debounceMs": 20000,
39
+ "backupEnabled": true,
40
+ "_comment": "Auto-saves appstate.json every 3 minutes. Keeps a .bak backup. Extends cookie expiry."
41
+ },
42
+
43
+ "facebookProtection": {
44
+ "enabled": true,
45
+ "blockAutomationDetection": true,
46
+ "humanLikeDelay": true,
47
+ "_comment": "Sets online=false, updatePresence=false. Adds 50-200ms human jitter on sendMessage."
26
48
  },
27
49
 
28
50
  "e2ee": {
29
- "enable": true,
51
+ "enable": false,
30
52
  "saveType": "memory",
31
53
  "devicePath": "./data/e2ee-device.json",
32
54
  "autoReconnect": true,
33
55
  "logLevel": "none",
34
- "_comments": {
35
- "enable": "Set to true to activate E2EE Labyrinth bridge.",
36
- "saveType": "'memory' = keys lost on restart (safe). 'path' = persist keys to disk (survives restart).",
37
- "devicePath": "Used only when saveType='path'. Relative to your bot's CWD.",
38
- "autoReconnect": "E2EE bridge auto-reconnects on disconnect.",
39
- "logLevel": "'none' = silent. 'error'/'warn'/'info' = verbose."
40
- }
41
- },
42
-
43
- "imgKeys": {
44
- "imgbb": null,
45
- "ik_pub": null,
46
- "ik_priv": null,
47
- "_comment": "Optional image hosting API keys. Leave null to disable image hosting."
56
+ "_comment": "E2EE Labyrinth bridge. REQUIRES native binary in build/ directory. Set enable:true only when binary is present. saveType: 'memory' (keys lost on restart) or 'path' (persisted to disk)."
48
57
  },
49
58
 
50
59
  "enableTypingIndicator": false,
51
60
  "typingDuration": 4000,
52
61
 
53
62
  "_listenOptions": {
63
+ "_comment": "These go in the options param of login(), not in this file. Shown here for reference.",
54
64
  "selfListen": true,
55
65
  "listenEvents": true,
56
66
  "listenTyping": false,
@@ -58,7 +68,6 @@
58
68
  "autoMarkDelivery": false,
59
69
  "autoMarkRead": false,
60
70
  "autoReconnect": true,
61
- "online": false,
62
- "_comment": "These go in the options param of login(), not in this file. Shown here for reference."
71
+ "online": false
63
72
  }
64
73
  }
package/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  "use strict";
2
2
  /* ═══════════════════════════════════════════════════════════════════════════
3
- * toru-fca v1.0.6 — index.js
4
- * Ultimate Fixed, Advanced & Merged Facebook Chat API
3
+ * toru-fca v1.1.0 — index.js
4
+ * Stable, Fast, Error-Resistant Facebook Chat API
5
5
  *
6
6
  * ✅ BROWSER LOGOUT FIX — forceLogin=false, cookie expiry 1yr, no session destroy
7
- * ✅ ANTI-LOGOUT — api.logout() blocked, fb_dtsg refresh every 20min
8
- * ✅ AUTOMATION BYPASS — online=false, updatePresence=false, human-like delay
9
- * ✅ INBOX + GROUP — DM/inbox এবং group উভয়তেই bot reply করবে
10
- * ✅ GOATBOT READY — selfListen, listenEvents, autoReconnect defaults set
11
- * ✅ ERROR-FREE — global error handler, no crash
12
- * ✅ SESSION GUARD — appstate auto-save 3min, backup, corruption check
13
- * ✅ SMOOTH MODE — typing indicator + 300ms delay before send
14
- * ✅ FACEBOOK PROTECTION — human-like micro-delay, anti-restriction
15
- * ✅ E2EE BRIDGE — Labyrinth native E2EE, DM + group
16
- * ✅ MQTT STABLE — exponential backoff, patch stream, no stale sid/cid
17
- * ✅ TYPINGV2 FIXED — ctx.mqttClient reference bug patched
7
+ * ✅ ANTI-LOGOUT — api.logout() blocked, fb_dtsg refresh every 20min
8
+ * ✅ AUTOMATION GUARD — online=false, updatePresence=false, human-like delay
9
+ * ✅ INBOX + GROUP — DM/inbox and group threads both supported
10
+ * ✅ GOATBOT READY — selfListen, listenEvents, autoReconnect defaults set
11
+ * ✅ ERROR-RESISTANT — global error handler, graceful fallbacks, no crash
12
+ * ✅ SESSION GUARD — appstate auto-save 3min, backup, corruption check
13
+ * ✅ SMOOTH MODE — typing indicator + 300ms delay before send
14
+ * ✅ FACEBOOK PROTECTION — human-like micro-delay, anti-restriction
15
+ * ✅ E2EE BRIDGE — Labyrinth native E2EE (requires native binary)
16
+ * ✅ MQTT STABLE — exponential backoff, patch stream, no stale sid/cid
17
+ * ✅ SENDMESSAGE FIXED — single-wrap, consistent Promise+callback contract
18
18
  *
19
- * Merged: toru-fca · alpha-fca · mahmud-fca · hridoy-fca
19
+ * Based on: toru-fca · alpha-fca · mahmud-fca · hridoy-fca
20
20
  * ═══════════════════════════════════════════════════════════════════════════
21
21
  */
22
22
 
@@ -41,13 +41,14 @@ if (!global._toruFcaConfig) {
41
41
  );
42
42
  } catch (_) {
43
43
  global._toruFcaConfig = {
44
- version : "1.0.6",
44
+ version : "1.1.0",
45
45
  antiLogout : { enabled: true, refreshIntervalMs: 1200000 },
46
46
  sessionGuard: { enabled: true, intervalMs: 180000, debounceMs: 20000, backupEnabled: true },
47
47
  automation : { enabled: false },
48
48
  smooth : { enabled: true, messageDelay: 300 },
49
49
  connection : { minDelayMs: 3000, maxDelayMs: 60000 },
50
- e2ee : { enable: true, saveType: "memory", autoReconnect: true, logLevel: "none" },
50
+ // E2EE disabled by default — requires native binary in build/
51
+ e2ee : { enable: false, saveType: "memory", autoReconnect: true, logLevel: "none" },
51
52
  facebookProtection: { enabled: true, blockAutomationDetection: true, humanLikeDelay: true },
52
53
  inbox : { enabled: true, replyToInbox: true }
53
54
  };
@@ -201,15 +202,25 @@ function createSessionGuard(jar, ctx, utils) {
201
202
  }, interval);
202
203
  if (guardTimer.unref) guardTimer.unref();
203
204
 
204
- if (jar._jar && typeof jar._jar.setCookie === "function") {
205
- var _origSet = jar._jar.setCookie.bind(jar._jar);
206
- jar._jar.setCookie = function() {
207
- var r = _origSet.apply(this, arguments);
208
- if (!debTimer && ctx.loggedIn) {
209
- debTimer = setTimeout(function() { debTimer = null; saveToDisk(); }, debounce);
210
- }
211
- return r;
212
- };
205
+ // Debounce-save on cookie change by monkey-patching the internal jar store.
206
+ // FIX v1.1.0: Added guards for the case where jar._jar is absent (different
207
+ // tough-cookie version or custom jar implementation). Previously this silently
208
+ // did nothing; now it logs a debug warning so the issue is diagnosable.
209
+ try {
210
+ if (jar._jar && typeof jar._jar.setCookie === "function") {
211
+ var _origSet = jar._jar.setCookie.bind(jar._jar);
212
+ jar._jar.setCookie = function() {
213
+ var r = _origSet.apply(this, arguments);
214
+ if (!debTimer && ctx.loggedIn) {
215
+ debTimer = setTimeout(function() { debTimer = null; saveToDisk(); }, debounce);
216
+ }
217
+ return r;
218
+ };
219
+ } else {
220
+ log.verbose("sessionGuard", "jar._jar not available — cookie-change debounce disabled (interval save still active).");
221
+ }
222
+ } catch (patchErr) {
223
+ log.verbose("sessionGuard", "Could not patch jar for debounce: " + (patchErr && patchErr.message ? patchErr.message : patchErr));
213
224
  }
214
225
 
215
226
  return {
@@ -412,22 +423,29 @@ function buildAPI(globalOptions, html, jar) {
412
423
 
413
424
  // 1. api.logout() → BLOCKED. Cookie session কখনো destroy হবে না।
414
425
  api.logout = function(callback) {
415
- log.warn("logout", "[toru-fca v1.0.6] ⛔ Logout blocked — Cookie session protected. Browser logout হবে না।");
426
+ log.warn("logout", "[toru-fca v1.1.0] \u26d4 Logout blocked \u2014 cookie session is protected. Use api.forceLogout() to explicitly terminate.");
416
427
  var cb = typeof callback === "function" ? callback : function() {};
417
428
  cb(null);
418
429
  return Promise.resolve();
419
430
  };
420
431
 
421
- // 2. Force logout (opt-in, সাবধানে ব্যবহার করো)
432
+ // 2. Force logout (opt-in only — use with caution)
422
433
  api.forceLogout = function(callback) {
423
- log.warn("forceLogout", "[toru-fca] ⚠️ Force logout — session WILL be destroyed.");
434
+ log.warn("forceLogout", "[toru-fca v1.1.0] \u26a0\uFE0F Force logout \u2014 cookie session WILL be destroyed.");
424
435
  var cb = typeof callback === "function" ? callback : function() {};
425
436
  try {
437
+ // FIX v1.1.0: The `h` query param on logout.php is a separate logout-nonce,
438
+ // NOT the fb_dtsg CSRF token. Passing fb_dtsg as `h` caused silent logout failures.
439
+ // The correct approach is to POST to logout.php with fb_dtsg in the body only.
426
440
  defaultFuncs
427
- .post("https://www.facebook.com/logout.php?h=" + (ctx.fb_dtsg || ""),
428
- ctx.jar, { fb_dtsg: ctx.fb_dtsg || "" })
441
+ .post("https://www.facebook.com/logout.php", ctx.jar, { fb_dtsg: ctx.fb_dtsg || "" })
429
442
  .then(function() { ctx.loggedIn = false; cb(null); })
430
- .catch(cb);
443
+ .catch(function(err) {
444
+ log.warn("forceLogout", "Logout request failed: " + (err && err.message ? err.message : err));
445
+ // Still mark as logged out locally even if the HTTP request failed
446
+ ctx.loggedIn = false;
447
+ cb(err);
448
+ });
431
449
  } catch(e) { cb(e); }
432
450
  };
433
451
 
@@ -463,32 +481,6 @@ function buildAPI(globalOptions, html, jar) {
463
481
  } catch (e) { log.warn("saveSession", e.message); return false; }
464
482
  };
465
483
 
466
- // ═══════════════════════════════════════════════════════════════════════════
467
- // SMOOTH MODE — typing indicator + delay before send
468
- // ═══════════════════════════════════════════════════════════════════════════
469
- (function patchSmooth() {
470
- var smooth = fcaCfg.smooth || {};
471
- if (smooth.enabled !== true) return;
472
- var delay = Number(smooth.messageDelay) || 300;
473
- var origSnd = api.sendMessage;
474
- if (typeof origSnd !== "function") return;
475
- api.sendMessage = function(msg, threadID, callback, replyToMessage, isSingleUser) {
476
- try {
477
- if (typeof api.sendTypingIndicator === "function") api.sendTypingIndicator(threadID, function() {});
478
- } catch (_) {}
479
- return new Promise(function(resolve, reject) {
480
- setTimeout(function() {
481
- try {
482
- var r = origSnd(msg, threadID, callback, replyToMessage, isSingleUser);
483
- if (r && typeof r.then === "function") r.then(resolve).catch(reject);
484
- else resolve(r);
485
- } catch(e) { reject(e); }
486
- }, delay);
487
- });
488
- };
489
- api.send = api.sendMessage;
490
- })();
491
-
492
484
  // ═══════════════════════════════════════════════════════════════════════════
493
485
  // AUTOMATION GUARD
494
486
  // ═══════════════════════════════════════════════════════════════════════════
@@ -502,39 +494,77 @@ function buildAPI(globalOptions, html, jar) {
502
494
  ctx.advancedSystem = advancedSystem;
503
495
 
504
496
  // ═══════════════════════════════════════════════════════════════════════════
505
- // FACEBOOK PROTECTION — anti-restriction, anti-ban, human-like delay
497
+ // UNIFIED SEND WRAPPER — smooth delay + human micro-delay (single wrap)
498
+ //
499
+ // FIX v1.1.0: Previously two separate wrappers (patchSmooth + initFacebookProtection)
500
+ // each re-wrapped api.sendMessage independently. This caused:
501
+ // - Triple nesting: smooth-wrap → human-wrap → real sendMessage
502
+ // - Inconsistent Promise/callback contract on synchronous throws
503
+ // - api.send alias out of sync after double reassignment
504
+ //
505
+ // Now a single wrapper applies both delays in sequence (human jitter first,
506
+ // then smooth delay), delegates to the real sendMessage, and keeps the
507
+ // Promise and callback contract consistent.
506
508
  // ═══════════════════════════════════════════════════════════════════════════
507
- (function initFacebookProtection() {
508
- var fpCfg = fcaCfg.facebookProtection || {};
509
- if (fpCfg.enabled === false) return;
509
+ (function patchSendWrapper() {
510
+ var smooth = fcaCfg.smooth || {};
511
+ var fpCfg = fcaCfg.facebookProtection || {};
512
+
513
+ var smoothEnabled = smooth.enabled === true;
514
+ var smoothDelay = Number(smooth.messageDelay) || 300;
515
+ var humanEnabled = fpCfg.enabled !== false && fpCfg.humanLikeDelay !== false;
510
516
 
511
- function humanDelay(min, max) {
512
- if (fpCfg.humanLikeDelay === false) return Promise.resolve();
517
+ // Set automation-detection globals regardless of wrapping
518
+ if (fpCfg.enabled !== false && fpCfg.blockAutomationDetection !== false) {
519
+ globalOptions.online = false;
520
+ globalOptions.updatePresence = false;
521
+ }
522
+
523
+ // Expose humanDelay helper on ctx for internal use
524
+ ctx._humanDelay = function humanDelay(min, max) {
525
+ if (!humanEnabled) return Promise.resolve();
513
526
  var ms = Math.floor(Math.random() * (max - min + 1)) + min;
514
527
  return new Promise(function(r) { setTimeout(r, ms); });
515
- }
516
- ctx._humanDelay = humanDelay;
517
-
518
- // Random micro-delay on top of smooth mode — traffic pattern অপ্রত্যাশিত
519
- if (fpCfg.humanLikeDelay !== false) {
520
- var _origMsg = api.sendMessage;
521
- if (typeof _origMsg === "function") {
522
- api.sendMessage = function(msg, threadID, callback, replyToMessage, isSingleUser) {
523
- return humanDelay(50, 200).then(function() {
524
- return _origMsg(msg, threadID, callback, replyToMessage, isSingleUser);
525
- });
526
- };
527
- api.send = api.sendMessage;
528
+ };
529
+
530
+ if (!smoothEnabled && !humanEnabled) {
531
+ if (fpCfg.enabled !== false) {
532
+ log.info("toru-fca", "\uD83D\uDEE1\uFE0F Facebook Protection active (no delay patches).");
528
533
  }
534
+ return; // Nothing to wrap
529
535
  }
530
536
 
531
- // Automation detection block
532
- if (fpCfg.blockAutomationDetection !== false) {
533
- globalOptions.online = false;
534
- globalOptions.updatePresence = false;
535
- }
537
+ var origSend = api.sendMessage;
538
+ if (typeof origSend !== "function") return;
539
+
540
+ api.sendMessage = function sendMessageWrapped(msg, threadID, callback, replyToMessage, isSingleUser) {
541
+ var doSmooth = smoothEnabled;
542
+ var doHuman = humanEnabled;
543
+
544
+ // Build a single Promise chain: human jitter → smooth delay → origSend
545
+ var chain = Promise.resolve();
546
+
547
+ if (doHuman) {
548
+ chain = chain.then(function() { return ctx._humanDelay(50, 200); });
549
+ }
550
+ if (doSmooth) {
551
+ chain = chain.then(function() {
552
+ return new Promise(function(r) { setTimeout(r, smoothDelay); });
553
+ });
554
+ }
555
+
556
+ return chain.then(function() {
557
+ return origSend(msg, threadID, callback, replyToMessage, isSingleUser);
558
+ });
559
+ };
560
+
561
+ // Keep send alias in sync (single assignment, always in sync)
562
+ api.send = api.sendMessage;
536
563
 
537
- log.info("toru-fca", "🛡️ Facebook Protection active — anti-restriction, human-like mode.");
564
+ var parts = [];
565
+ if (humanEnabled) parts.push("human jitter 50\u2013200ms");
566
+ if (smoothEnabled) parts.push("smooth delay " + smoothDelay + "ms");
567
+ log.info("toru-fca", "\uD83D\uDEE1\uFE0F Facebook Protection active \u2014 " + parts.join(" + ") + ".");
538
568
  })();
539
569
 
540
570
  // ═══════════════════════════════════════════════════════════════════════════
package/package.json CHANGED
@@ -1,12 +1,27 @@
1
1
  {
2
2
  "name": "toru-fca",
3
- "version": "1.0.8",
4
- "description": "toru-fca v1.0.8 — Ultimate Fixed FCA. Browser Logout Free, Anti-Automation, Inbox+Group reply, GoatBot ready, Error-Free, Cookie Login Protected.",
3
+ "version": "1.1.0",
4
+ "description": "toru-fca v1.1.0 — Stable, fast, error-resistant Facebook Chat API. Cookie login, MQTT, E2EE, GoatBot ready.",
5
5
  "main": "index.js",
6
- "keywords": ["facebook","chat","api","messenger","bot","fca","toru-fca","goatbot","anti-logout","cookie-login","inbox-bot","e2ee"],
6
+ "keywords": [
7
+ "facebook",
8
+ "chat",
9
+ "api",
10
+ "messenger",
11
+ "bot",
12
+ "fca",
13
+ "toru-fca",
14
+ "goatbot",
15
+ "anti-logout",
16
+ "cookie-login",
17
+ "inbox-bot",
18
+ "e2ee"
19
+ ],
7
20
  "author": "toru-fca contributors",
8
21
  "license": "MIT",
9
- "engines": { "node": ">=18.0.0" },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
10
25
  "dependencies": {
11
26
  "axios": "^1.7.2",
12
27
  "bluebird": "^3.7.2",
package/src/listenMqtt.js CHANGED
@@ -92,7 +92,7 @@ function printMqttBanner(region, autoReconnect) {
92
92
  labelClr + ' 🔄 Auto-reconnect ' + rst + reconnVal,
93
93
  urlClr + ' 🌐 github.com/alpha-fca/alpha-fca' + rst,
94
94
  '',
95
- accentClr + ' 💎 toru-fca v1.0.6' + rst
95
+ accentClr + ' \uD83D\uDC8E toru-fca v1.1.0' + rst
96
96
  ];
97
97
 
98
98
  process.stdout.write('\n');
@@ -173,8 +173,11 @@ function createMqttPatchStream() {
173
173
  }
174
174
 
175
175
  var identity = function () { };
176
- var form = {};
177
- var getSeqID = function () { };
176
+ // NOTE: `form` and `getSeqID` are intentionally NOT declared at module scope.
177
+ // v1.1.0 FIX: Previously they were module-level mutable vars, which meant
178
+ // multiple bot instances in the same process would overwrite each other's
179
+ // getSeqID closure. They are now declared inside the exported function
180
+ // so each login instance has its own isolated state.
178
181
 
179
182
  var topics = [
180
183
  "/legacy_web",
@@ -460,6 +463,28 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
460
463
  };
461
464
  });
462
465
 
466
+ // Dead connection watchdog — if no /t_ms event for 8min, force reconnect.
467
+ // Facebook stops delivering reaction deltas after prolonged sessions.
468
+ // Forcing a fresh sync queue creation fixes "reactions stop working after ~1hr".
469
+ //
470
+ // FIX v1.1.0: Timer is stored on ctx._deadTimer so stopListenMqtt() and
471
+ // stopListening() can both clear it regardless of closure scope.
472
+ var _DEAD_MS = 8 * 60 * 1000; // 8 minutes
473
+
474
+ function resetDeadTimer() {
475
+ if (ctx._deadTimer) { clearTimeout(ctx._deadTimer); ctx._deadTimer = null; }
476
+ if (ctx._reconnectState && ctx._reconnectState.stopped) return;
477
+ ctx._deadTimer = setTimeout(function () {
478
+ if (ctx._reconnectState && !ctx._reconnectState.stopped) {
479
+ log.warn("listenMqtt", "\u26a0\uFE0F No events for 8min \u2014 forcing reconnect for fresh sync queue (reaction fix).");
480
+ try { mqttClient.end(true); } catch (_) {}
481
+ scheduleReconnect("dead connection watchdog");
482
+ }
483
+ }, _DEAD_MS);
484
+ if (ctx._deadTimer && ctx._deadTimer.unref) ctx._deadTimer.unref();
485
+ }
486
+ resetDeadTimer(); // start watchdog on first connect
487
+
463
488
  mqttClient.on('message', function (topic, message) {
464
489
  var jsonMessage;
465
490
  try {
@@ -469,6 +494,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
469
494
  }
470
495
 
471
496
  if (topic === "/t_ms") {
497
+ resetDeadTimer(); // ✅ reset watchdog on every real event
472
498
  if (ctx.tmsWait && typeof ctx.tmsWait === "function") ctx.tmsWait();
473
499
 
474
500
  if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
@@ -483,6 +509,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
483
509
  parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta });
484
510
  }
485
511
  } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
512
+ resetDeadTimer(); // keep alive on typing events too
486
513
  var typ = {
487
514
  type: "typ",
488
515
  isTyping: !!jsonMessage.state,
@@ -491,6 +518,7 @@ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
491
518
  };
492
519
  (function () { globalCallback(null, typ); })();
493
520
  } else if (topic === "/orca_presence") {
521
+ resetDeadTimer(); // keep alive on presence events too
494
522
  if (ctx.globalOptions.updatePresence) {
495
523
  for (var i in jsonMessage.list) {
496
524
  var data = jsonMessage.list[i];
@@ -570,12 +598,12 @@ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
570
598
  globalCallback(null, {
571
599
  type: "message_reaction",
572
600
  threadID: _tid,
573
- messageID: _rxn.messageId,
601
+ messageID: _rxn.messageId ? _rxn.messageId.toString() : null,
574
602
  reaction: _rxn.reaction || "",
575
603
  // off = true মানে reaction সরিয়ে নিয়েছে (remove)
576
604
  off: !_rxn.reaction || _rxn.reaction === "",
577
- senderID: _rxn.senderId.toString(),
578
- userID: _rxn.userId.toString(),
605
+ senderID: _rxn.senderId ? _rxn.senderId.toString() : null,
606
+ userID: _rxn.userId ? _rxn.userId.toString() : null,
579
607
  timestamp: _rxn.timestamp || Date.now()
580
608
  });
581
609
  })();
@@ -866,7 +894,13 @@ function markDelivery(ctx, api, threadID, messageID) {
866
894
  }
867
895
 
868
896
  module.exports = function (defaultFuncs, api, ctx) {
897
+ // FIX v1.1.0: `globalCallback`, `form`, and `getSeqID` are now declared
898
+ // inside the exported factory function (one per login instance) rather
899
+ // than at module scope. This prevents cross-instance contamination when
900
+ // multiple bots run in the same Node.js process.
869
901
  var globalCallback = identity;
902
+ var form = {};
903
+ var getSeqID;
870
904
 
871
905
  getSeqID = function getSeqID() {
872
906
  ctx.t_mqttCalled = false;
@@ -884,8 +918,10 @@ module.exports = function (defaultFuncs, api, ctx) {
884
918
  })
885
919
  .catch(function (err) {
886
920
  log.error("getSeqId", err);
887
- // Match both spellings: parseAndCheckLogin throws "Not logged in." (with period)
888
- if (utils.getType(err) == "Object" && (err.error === "Not logged in" || err.error === "Not logged in.")) ctx.loggedIn = false;
921
+ // Handle both "Not logged in" and "Not logged in." (with/without period)
922
+ if (utils.getType(err) == "Object" && (err.error === "Not logged in" || err.error === "Not logged in.")) {
923
+ ctx.loggedIn = false;
924
+ }
889
925
  return globalCallback(err);
890
926
  });
891
927
  };
@@ -893,6 +929,8 @@ module.exports = function (defaultFuncs, api, ctx) {
893
929
  return function (callback) {
894
930
  class MessageEmitter extends EventEmitter {
895
931
  stopListening(callback) {
932
+ // Clear dead-connection watchdog first
933
+ if (ctx._deadTimer) { clearTimeout(ctx._deadTimer); ctx._deadTimer = null; }
896
934
  callback = callback || (function () { });
897
935
  globalCallback = identity;
898
936
  if (ctx._reconnectState) {
@@ -2,9 +2,13 @@
2
2
  var log = require("npmlog");
3
3
 
4
4
  /**
5
- * toru-fca v1.0.6 — refreshFb_dtsg.js
6
- * fb_dtsg token কে fresh করে — "re-open your browser" error থেকে সুরক্ষা।
7
- * antiLogout timer এটাকে হর 20 মিনিটে call করে।
5
+ * toru-fca v1.1.0 — refreshFb_dtsg.js
6
+ *
7
+ * Refreshes the fb_dtsg CSRF token every 20 minutes to keep the session alive
8
+ * and prevent "re-open your browser" errors.
9
+ *
10
+ * FIX v1.1.0: Broad regex pattern `/"token":"([^"]+)"/` moved to last position
11
+ * to prevent matching unrelated tokens earlier in the page HTML.
8
12
  */
9
13
  module.exports = function (defaultFuncs, api, ctx) {
10
14
  return function refreshFb_dtsg(callback) {
@@ -13,22 +17,24 @@ module.exports = function (defaultFuncs, api, ctx) {
13
17
  .get("https://www.facebook.com/", ctx.jar)
14
18
  .then(function(res) {
15
19
  var html = res.body || "";
20
+ // Patterns ordered from most-specific to least-specific.
21
+ // The broad /"token":"..."/ pattern is last to avoid false matches.
16
22
  var patterns = [
17
23
  /\["DTSGInitialData",\[\],{"token":"([^"]+)"}\]/,
18
24
  /\["DTSGInitData",\[\],{"token":"([^"]+)"/,
19
- /"token":"([^"]+)"/,
20
25
  /name="fb_dtsg" value="([^"]+)"/,
21
26
  /"async_get_token":"([^"]+)"/,
22
- /"dtsg":\{"token":"([^"]+)"/
27
+ /"dtsg":\{"token":"([^"]+)"/,
28
+ /"token":"([^"]+)"/ // broad — last resort
23
29
  ];
24
30
  var token = null;
25
31
  for (var i = 0; i < patterns.length; i++) {
26
32
  var m = html.match(patterns[i]);
27
- if (m && m[1]) { token = m[1]; break; }
33
+ if (m && m[1] && m[1].length > 4) { token = m[1]; break; }
28
34
  }
29
35
  if (token) {
30
36
  ctx.fb_dtsg = token;
31
- log.info("refreshFb_dtsg", "✅ fb_dtsg refreshed successfully.");
37
+ log.info("refreshFb_dtsg", "\u2705 fb_dtsg refreshed successfully.");
32
38
  cb(null, token);
33
39
  } else {
34
40
  log.warn("refreshFb_dtsg", "Could not extract fb_dtsg from page.");
@@ -398,8 +398,9 @@ module.exports = function (defaultFuncs, api, ctx) {
398
398
  // payload does not carry. For ordinary personal inbox chats, MQTT remains
399
399
  // the fast path and HTTP is used only when the transport actually fails.
400
400
  var normalizedThreadID = typeof threadID === "string" ? threadID.trim() : threadID;
401
- if (global._alphaFcaConfig && global._alphaFcaConfig.inbox && global._alphaFcaConfig.inbox.enabled === false) {
402
- throw new Error("Inbox messaging is disabled by configuration");
401
+ // Guard: respect inbox.enabled flag from the toru-fca config
402
+ if (global._toruFcaConfig && global._toruFcaConfig.inbox && global._toruFcaConfig.inbox.enabled === false) {
403
+ throw new Error("Inbox messaging is disabled by configuration (inbox.enabled=false in config.json)");
403
404
  }
404
405
 
405
406
  // Shared typing-off helper — always call this before returning, success or error.
@@ -1,54 +1,68 @@
1
1
  "use strict";
2
2
 
3
- var utils = require("../utils");
4
- // @NethWs3Dev
3
+ var log = require("npmlog");
5
4
 
5
+ /**
6
+ * toru-fca v1.1.0 — sendTypingIndicator.js
7
+ *
8
+ * FIX v1.1.0: Local `count_req` was reset to 0 on every call, so every
9
+ * request_id was always 1. Now uses ctx.wsReqNumber (shared counter) so
10
+ * request IDs are unique across all MQTT ls_req calls.
11
+ */
6
12
  module.exports = function (defaultFuncs, api, ctx) {
7
- return async function sendTypingIndicatorV2(sendTyping, threadID, callback) {
8
- // ── sendTypingE2EE: route E2EE typing indicator through the bridge ──────
9
- if (ctx.globalOptions && ctx.globalOptions.enableE2EE) {
10
- var _e2eeMod = require('../e2ee');
11
- if (_e2eeMod.isE2EEChatJid(String(threadID))) {
12
- return _e2eeMod.createBridge(ctx).sendTyping(threadID, sendTyping !== false)
13
- .then(function (r) { if (typeof callback === 'function') callback(null, r); return r; })
14
- .catch(function (e) { if (typeof callback === 'function') callback(e); });
15
- }
16
- }
17
- // ── end sendTypingE2EE ────────────────────────────────────────────────────
13
+ return async function sendTypingIndicator(sendTyping, threadID, callback) {
14
+ // ── E2EE typing: route through the bridge ────────────────────────────
15
+ if (ctx.globalOptions && ctx.globalOptions.enableE2EE) {
16
+ try {
17
+ var _e2eeMod = require('../e2ee');
18
+ if (_e2eeMod.isE2EEChatJid(String(threadID))) {
19
+ return _e2eeMod.createBridge(ctx)
20
+ .sendTyping(threadID, sendTyping !== false)
21
+ .then(function(r) { if (typeof callback === 'function') callback(null, r); return r; })
22
+ .catch(function(e) { if (typeof callback === 'function') callback(e); });
23
+ }
24
+ } catch (_) {}
25
+ }
26
+ // ── end E2EE routing ─────────────────────────────────────────────────
18
27
 
19
- const mqttClient = ctx.mqttClient || global.mqttClient;
20
- if (!mqttClient) {
21
- if (typeof callback === 'function') callback(new Error('No MQTT client available for typing indicator'));
22
- return;
23
- }
28
+ var mqttClient = ctx.mqttClient || global.mqttClient;
29
+ if (!mqttClient || !mqttClient.connected) {
30
+ // Non-fatal: typing indicator is cosmetic — skip silently when MQTT is not up
31
+ if (typeof callback === 'function') callback(null);
32
+ return;
33
+ }
24
34
 
25
- let count_req = 0;
26
- var wsContent = {
27
- app_id: 2220391788200892,
28
- payload: JSON.stringify({
29
- label: 3,
30
- payload: JSON.stringify({
31
- thread_key: threadID.toString(),
32
- is_group_thread: +(threadID.toString().length >= 16),
33
- is_typing: +sendTyping,
34
- attribution: 0
35
- }),
36
- version: 5849951561777440
37
- }),
38
- request_id: ++count_req,
39
- type: 4
40
- };
35
+ // Shared counter so request_id is unique across all ls_req calls
36
+ ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
37
+ var count_req = ctx.wsReqNumber;
41
38
 
42
- return new Promise((resolve, reject) => {
43
- mqttClient.publish('/ls_req', JSON.stringify(wsContent), {}, (err, _packet) => {
44
- if (err) {
45
- if (typeof callback === 'function') callback(err);
46
- reject(err);
47
- } else {
48
- if (typeof callback === 'function') callback(null, _packet);
49
- resolve(_packet);
50
- }
51
- });
52
- });
53
- };
39
+ var wsContent = {
40
+ app_id: 2220391788200892,
41
+ payload: JSON.stringify({
42
+ label: 3,
43
+ payload: JSON.stringify({
44
+ thread_key: threadID.toString(),
45
+ is_group_thread: +(threadID.toString().length >= 16),
46
+ is_typing: +sendTyping,
47
+ attribution: 0
48
+ }),
49
+ version: 5849951561777440
50
+ }),
51
+ request_id: count_req,
52
+ type: 4
53
+ };
54
+
55
+ return new Promise(function(resolve, reject) {
56
+ mqttClient.publish('/ls_req', JSON.stringify(wsContent), {}, function(err, packet) {
57
+ if (err) {
58
+ log.warn("sendTypingIndicator", "Publish failed: " + (err.message || err));
59
+ if (typeof callback === 'function') callback(err);
60
+ reject(err);
61
+ } else {
62
+ if (typeof callback === 'function') callback(null, packet);
63
+ resolve(packet);
64
+ }
65
+ });
66
+ });
67
+ };
54
68
  };
@@ -1,51 +1,22 @@
1
1
  "use strict";
2
- var log = require("npmlog");
3
2
 
4
3
  /**
5
- * toru-fca v1.0.6 — sendTypingIndicatorV2.js
6
- * Bug fix: ctx.mqttClient reference ছিল undefined — এখন সঠিকভাবে reference করা হয়েছে।
4
+ * toru-fca v1.1.0 — sendTypingIndicatorV2.js
5
+ *
6
+ * NOTE: In v1.1.0 this module delegates to sendTypingIndicator (sendTypingIndicator.js)
7
+ * which already implements V2 MQTT ls_req logic with a shared ctx.wsReqNumber counter.
8
+ *
9
+ * This file is kept for backward compatibility in case external code calls
10
+ * api.sendTypingIndicatorV2() directly. Both api.sendTypingIndicator and
11
+ * api.sendTypingIndicatorV2 now use the same implementation.
7
12
  */
8
13
  module.exports = function (defaultFuncs, api, ctx) {
9
14
  return async function sendTypingIndicatorV2(sendTyping, threadID, callback) {
10
- var mqttClient = ctx.mqttClient; // ← fix: ctx থেকে নেওয়া
11
- var cb = typeof callback === "function" ? callback : function() {};
12
-
13
- if (!mqttClient || !mqttClient.connected) {
14
- log.warn("sendTypingIndicatorV2", "MQTT not connected — skipping.");
15
- return cb(null); // non-fatal
16
- }
17
-
18
- var count_req = ctx.wsReqNumber != null ? ++ctx.wsReqNumber : 1;
19
- var tid = threadID ? threadID.toString() : "";
20
- var isGroup = tid.length >= 16 ? 1 : 0;
21
-
22
- var wsContent = {
23
- app_id: 2220391788200892,
24
- payload: JSON.stringify({
25
- label: 3,
26
- payload: JSON.stringify({
27
- thread_key: tid,
28
- is_group_thread: isGroup,
29
- is_typing: +sendTyping,
30
- attribution: 0
31
- }),
32
- version: 5849951561777440
33
- }),
34
- request_id: count_req,
35
- type: 4
36
- };
37
-
38
- try {
39
- await new Promise(function(resolve, reject) {
40
- mqttClient.publish("/ls_req", JSON.stringify(wsContent), {}, function(err) {
41
- if (err) return reject(err);
42
- resolve();
43
- });
44
- });
45
- cb(null);
46
- } catch (e) {
47
- log.warn("sendTypingIndicatorV2", "Failed: " + (e && e.message ? e.message : e));
48
- cb(e);
15
+ // Delegate to the main sendTypingIndicator implementation
16
+ if (typeof api.sendTypingIndicator === "function") {
17
+ return api.sendTypingIndicator(sendTyping, threadID, callback);
49
18
  }
19
+ // Fallback if called before sendTypingIndicator is registered (should not happen)
20
+ if (typeof callback === "function") callback(null);
50
21
  };
51
22
  };
@@ -2,17 +2,22 @@
2
2
 
3
3
  var log = require("npmlog");
4
4
 
5
+ /**
6
+ * toru-fca v1.1.0 — stopListenMqtt.js
7
+ *
8
+ * FIX v1.1.0: Now also clears ctx._deadTimer (the 8-minute dead-connection
9
+ * watchdog). Previously the watchdog timer kept running after stopListenMqtt()
10
+ * was called, which could trigger spurious reconnect attempts on a stopped bot.
11
+ */
5
12
  module.exports = function (defaultFuncs, api, ctx) {
6
13
  return function stopListenMqtt() {
7
- // Guard: if already stopped or never started, silently return instead of throwing.
8
- if (!ctx.mqttClient) {
9
- log.warn("stopListenMqtt", "MQTT client is not active; nothing to stop.");
10
- return;
14
+ // Stop the dead-connection watchdog first so it cannot fire during teardown
15
+ if (ctx._deadTimer) {
16
+ clearTimeout(ctx._deadTimer);
17
+ ctx._deadTimer = null;
11
18
  }
12
19
 
13
- log.info("stopListenMqtt", "Stopping MQTT listener...");
14
-
15
- // Halt any pending auto-reconnect so the close event does not trigger a new attempt.
20
+ // Halt any pending auto-reconnect
16
21
  if (ctx._reconnectState) {
17
22
  ctx._reconnectState.stopped = true;
18
23
  if (ctx._reconnectState.timer) {
@@ -21,9 +26,14 @@ module.exports = function (defaultFuncs, api, ctx) {
21
26
  }
22
27
  }
23
28
 
24
- // Graceful teardown: unsubscribe from real-time topics, signal browser close,
25
- // then end the connection. Wrap each call so a partially-closed client cannot
26
- // throw and leave the bot in a broken state.
29
+ if (!ctx.mqttClient) {
30
+ log.warn("stopListenMqtt", "MQTT client is not active; nothing to stop.");
31
+ return;
32
+ }
33
+
34
+ log.info("stopListenMqtt", "Stopping MQTT listener...");
35
+
36
+ // Graceful teardown: unsubscribe real-time topics, signal browser close, end.
27
37
  try { ctx.mqttClient.unsubscribe("/webrtc"); } catch (_) {}
28
38
  try { ctx.mqttClient.unsubscribe("/rtc_multi"); } catch (_) {}
29
39
  try { ctx.mqttClient.unsubscribe("/onevc"); } catch (_) {}
package/utils.js CHANGED
@@ -2607,8 +2607,20 @@ function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
2607
2607
  var retryTime = Math.floor(Math.random() * 5000);
2608
2608
  log.warn("parseAndCheckLogin", "Got status code " + data.statusCode + " - " + retryCount + ". attempt to retry in " + retryTime + " milliseconds...");
2609
2609
  var url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.request.uri.pathname;
2610
- if (data.request.headers["Content-Type"].split(";")[0] === "multipart/form-data") return bluebird.delay(retryTime).then(() => defaultFuncs.postFormData(url, ctx.jar, data.request.formData, {})).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
2611
- else return bluebird.delay(retryTime).then(() => defaultFuncs.post(url, ctx.jar, data.request.formData)).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
2610
+ // FIX v1.1.0: standard POST requests use data.request.form, not formData.
2611
+ // formData is only set for multipart/form-data uploads. Sending an empty
2612
+ // body on retry caused silent failures for all non-upload 5xx retries.
2613
+ var contentType = (data.request.headers && data.request.headers["Content-Type"]) || "";
2614
+ if (contentType.split(";")[0].trim() === "multipart/form-data") {
2615
+ return bluebird.delay(retryTime)
2616
+ .then(function() { return defaultFuncs.postFormData(url, ctx.jar, data.request.formData, {}); })
2617
+ .then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
2618
+ } else {
2619
+ var retryBody = data.request.form || data.request.formData || {};
2620
+ return bluebird.delay(retryTime)
2621
+ .then(function() { return defaultFuncs.post(url, ctx.jar, retryBody); })
2622
+ .then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
2623
+ }
2612
2624
  }
2613
2625
  if (data.statusCode !== 200) throw new Error("parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.");
2614
2626
 
@@ -1 +0,0 @@
1
- []