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 +107 -35
- package/checkUpdate.js +32 -12
- package/config.json +11 -20
- package/examples/basic-bot.js +58 -25
- package/fca-config.example.json +35 -26
- package/index.js +112 -82
- package/package.json +19 -4
- package/src/listenMqtt.js +46 -8
- package/src/refreshFb_dtsg.js +13 -7
- package/src/sendMessage.js +3 -2
- package/src/sendTypingIndicator.js +60 -46
- package/src/sendTypingIndicatorV2.js +13 -42
- package/src/stopListenMqtt.js +20 -10
- package/utils.js +14 -2
- package/src/data/getThreadInfo.json +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,42 +1,114 @@
|
|
|
1
|
-
#
|
|
1
|
+
# toru-fca Changelog
|
|
2
2
|
|
|
3
|
-
## v1.0
|
|
3
|
+
## v1.1.0 — Audit, Repair & Stability Upgrade
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> Full professional audit and repair of all critical, high, and medium issues.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Facebook checkpoint এ "This was me" auto-submit করত
|
|
9
|
-
- Facebook নতুন session তৈরি করত → পুরনো browser session invalidate → **browser logout**
|
|
7
|
+
### 🔴 Critical Fixes
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
| `src/
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
log.warn(
|
|
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
|
|
3
|
-
"version": "1.0
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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":
|
|
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
|
|
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
|
|
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
|
|
47
|
+
"_note": "Bot replies in both inbox/DM and group threads."
|
|
56
48
|
},
|
|
57
|
-
|
|
58
49
|
"enableTypingIndicator": false,
|
|
59
50
|
"typingDuration": 4000
|
|
60
51
|
}
|
package/examples/basic-bot.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* toru-fca v1.0
|
|
2
|
+
* toru-fca v1.1.0 — basic-bot.js (GoatBot compatible example)
|
|
3
3
|
*
|
|
4
4
|
* Features:
|
|
5
|
-
* ✅ Cookie login
|
|
6
|
-
* ✅ Inbox + Group
|
|
7
|
-
* ✅ Session guard
|
|
8
|
-
* ✅ Anti-logout
|
|
9
|
-
* ✅ Facebook protection
|
|
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]
|
|
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
|
|
33
|
+
selfListen : false, // bot does not listen to its own messages
|
|
34
34
|
autoReconnect : true,
|
|
35
|
-
online : false, // automation detection
|
|
35
|
+
online : false, // reduces automation detection risk
|
|
36
36
|
updatePresence : false,
|
|
37
|
-
forceLogin : false, // browser logout
|
|
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) {
|
|
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]
|
|
51
|
+
console.log("[toru-fca] \u2705 Bot running as:", botID);
|
|
49
52
|
|
|
50
|
-
// Session guard — auto-
|
|
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) {
|
|
58
|
+
if (err) {
|
|
59
|
+
console.error("[listen]", err);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
56
62
|
|
|
57
|
-
// Group
|
|
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
|
-
//
|
|
69
|
+
// Simple echo bot
|
|
64
70
|
if (body && body.toLowerCase() === "ping") {
|
|
65
|
-
api.sendMessage("
|
|
71
|
+
api.sendMessage("\uD83C\uDFD3 pong! \u2014 toru-fca v1.1.0", threadID);
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
// Reaction
|
|
75
|
+
// Reaction events
|
|
70
76
|
if (event.type === "message_reaction") {
|
|
71
|
-
console.log(
|
|
77
|
+
console.log(
|
|
78
|
+
"[Reaction]",
|
|
79
|
+
event.senderID,
|
|
80
|
+
event.off ? "removed reaction" : "reacted:",
|
|
81
|
+
event.reaction
|
|
82
|
+
);
|
|
72
83
|
}
|
|
73
84
|
|
|
74
|
-
//
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
});
|
package/fca-config.example.json
CHANGED
|
@@ -1,56 +1,66 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "
|
|
2
|
+
"_comment": "toru-fca v1.1.0 — Example Config. Copy to config.json and customize.",
|
|
3
3
|
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
|
|
6
6
|
"automation": {
|
|
7
7
|
"enabled": false,
|
|
8
|
-
"_comment": "KEEP FALSE. Enabling this allows background task automation
|
|
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
|
-
"
|
|
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":
|
|
19
|
-
"_comment": "
|
|
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":
|
|
24
|
-
"maxDelayMs":
|
|
25
|
-
"_comment": "
|
|
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":
|
|
51
|
+
"enable": false,
|
|
30
52
|
"saveType": "memory",
|
|
31
53
|
"devicePath": "./data/e2ee-device.json",
|
|
32
54
|
"autoReconnect": true,
|
|
33
55
|
"logLevel": "none",
|
|
34
|
-
"
|
|
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
|
|
4
|
-
*
|
|
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
|
|
8
|
-
* ✅ AUTOMATION
|
|
9
|
-
* ✅ INBOX + GROUP
|
|
10
|
-
* ✅ GOATBOT READY
|
|
11
|
-
* ✅ ERROR-
|
|
12
|
-
* ✅ SESSION GUARD
|
|
13
|
-
* ✅ SMOOTH MODE
|
|
14
|
-
* ✅ FACEBOOK PROTECTION
|
|
15
|
-
* ✅ E2EE BRIDGE
|
|
16
|
-
* ✅ MQTT STABLE
|
|
17
|
-
* ✅
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
|
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]
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
508
|
-
var
|
|
509
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
"description": "toru-fca v1.0
|
|
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": [
|
|
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": {
|
|
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 + '
|
|
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
|
-
|
|
177
|
-
|
|
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
|
-
//
|
|
888
|
-
if (utils.getType(err) == "Object" && (err.error === "Not logged in" || err.error === "Not logged in."))
|
|
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) {
|
package/src/refreshFb_dtsg.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
var log = require("npmlog");
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* toru-fca v1.0
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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", "
|
|
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.");
|
package/src/sendMessage.js
CHANGED
|
@@ -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
|
-
|
|
402
|
-
|
|
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
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
11
|
-
|
|
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
|
};
|
package/src/stopListenMqtt.js
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
2611
|
-
|
|
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
|
-
[]
|