hridoy-ultimate 9.0.0 โ†’ 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +346 -69
  2. package/{docs/UPSTREAM_API_DOCS.md โ†’ DOCS.md} +3 -3
  3. package/README.md +536 -161
  4. package/func/checkUpdate.js +1 -0
  5. package/index.d.ts +13 -74
  6. package/index.js +0 -6
  7. package/module/config.js +2 -5
  8. package/module/login.js +1 -2
  9. package/module/loginHelper.js +9 -41
  10. package/module/options.js +6 -6
  11. package/package.json +44 -39
  12. package/src/api/action/logout.js +0 -19
  13. package/src/api/messaging/sendMessage.js +1 -1
  14. package/src/api/messaging/sessionGuard.js +8 -0
  15. package/src/api/messaging/setMessageReaction.js +2 -7
  16. package/src/api/socket/core/connectMqtt.js +72 -99
  17. package/src/api/socket/core/emitAuth.js +17 -9
  18. package/src/api/socket/e2ee/index.js +15 -33
  19. package/src/api/socket/e2ee/native/NOTICE.md +2 -2
  20. package/src/api/socket/e2ee/native/lib/index.mjs +1 -2
  21. package/src/api/socket/e2ee/native/nativeMediaBridge.js +4 -4
  22. package/src/api/socket/listenE2EE.js +1 -3
  23. package/src/api/socket/listenMqtt.js +60 -67
  24. package/src/utils/antiSuspension.js +3 -3
  25. package/src/utils/fingerprint.js +35 -16
  26. package/src/utils/headers.js +1 -1
  27. package/src/utils/versionCheck.js +47 -0
  28. package/LICENSE +0 -23
  29. package/MERGE_REPORT.md +0 -54
  30. package/THIRD_PARTY_NOTICES.md +0 -36
  31. package/docs/TEST_RESULTS.md +0 -68
  32. package/scripts/run-tests.js +0 -19
  33. package/scripts/syntax-check.js +0 -22
  34. package/src/api/socket/core/lifecycle.js +0 -117
  35. package/src/api/socket/e2ee/proto/proto-writer.ts +0 -76
  36. package/src/utils/typingIndicator.js +0 -202
  37. package/test/helpers/stub-modules.js +0 -122
  38. package/test/integration/logout.test.js +0 -69
  39. package/test/integration/mqtt-lifecycle.test.js +0 -195
  40. package/test/integration/package.test.js +0 -116
  41. package/test/integration/typing-wiring.test.js +0 -71
  42. package/test/unit/fingerprint.test.js +0 -19
  43. package/test/unit/humanizer.test.js +0 -32
  44. package/test/unit/lifecycle.test.js +0 -92
  45. package/test/unit/parsers.test.js +0 -43
  46. package/test/unit/typing.test.js +0 -136
package/README.md CHANGED
@@ -1,179 +1,554 @@
1
- # hridoy-ultimate
1
+ <div align="center">
2
2
 
3
- **Version 9.0.0 - Owner/maintainer: HR ID OY**
3
+ # ๐Ÿ’ฌ hridoy-ultimate
4
4
 
5
- An unofficial Facebook Chat API (FCA) for Node.js Messenger bots, unified from the
6
- `@eryxenx/fca` 1.0.0 - 1.1.9 and `fca-eryxenx` 1.x - 39.x lineages (83 published builds
7
- analysed, see `MERGE_REPORT.md`; itself derived from fca-unofficial) with a hardened
8
- MQTT lifecycle: bounded exponential backoff, a reconnect budget, clean shutdown and no
9
- timer left behind after `stopListening()` / `logout()`.
5
+ **Unofficial Facebook Messenger Bot API for Node.js**
6
+ NEXCA MQTT ยท Signal Protocol E2EE (mautrix-go powered) ยท sessionGuard ยท 90+ API Methods ยท Zero TypeScript
10
7
 
11
- > **Unofficial.** This automates a personal Facebook account and may violate Facebook's
12
- > terms. Accounts can be restricted. Use a throwaway account, never one you can't lose.
8
+ [![npm](https://img.shields.io/npm/v/hridoy-ultimate?color=blue)](https://www.npmjs.com/package/hridoy-ultimate)
9
+ [![license](https://img.shields.io/badge/license-MIT-green)](./LICENSE-MIT)
10
+ [![node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
13
11
 
14
- ## Verification status (read this first)
15
- | Area | Status |
16
- |---|---|
17
- | Unit tests (backoff, scheduler, parsers, humanizer, typing indicator, fingerprint) | **PASS** (real code, no mocks) |
18
- | MQTT lifecycle, logout, typing wiring, package surface (incl. pinned binary hashes) | **PASS - MOCKED** (fake mqtt/axios; see `test/helpers/stub-modules.js`) |
19
- | TypeScript typings | **PASS** with `tsc` against a consumer sample (Node types stubbed) |
20
- | Live Facebook login / MQTT / sending / reactions / attachments | **NOT TESTED** (no network or account in the build environment) |
21
- | E2EE (encrypted chats) and the bundled native binaries | **NOT TESTED** - needs optional deps and a live account |
22
- | `npm install` from a clean registry | **NOT TESTED** (build sandbox was offline) |
12
+ [Features](#-features) โ€ข [Installation](#-installation) โ€ข [Quick Start](#-quick-start) โ€ข [E2EE](#-e2ee--encrypted-conversations) โ€ข [sessionGuard](#-sessionguard) โ€ข [sendBroadcast](#-sendbroadcast) โ€ข [API Reference](#-api-reference)
13
+
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## โšก Why hridoy-ultimate?
19
+
20
+ - โœ… **NEXCA MQTT** โ€” stable connection core, autoReconnect, jitter
21
+ - โœ… **Signal Protocol E2EE** โ€” Facebook real encrypted conversations support. Reliable media/reaction/unsend delivery is powered by a bundled **mautrix-go / mautrix-meta** native engine (ยฉ Tulir Asokan, MPL-2.0) โ€” NEXCA's own hand-written E2EE encoder produced protocol-valid messages that Facebook accepted but Messenger clients failed to render for attachments/reactions/unsend, so this fork routes those through the native engine instead. Text messaging still uses the original engine (already reliable). The native engine is opt-in (`FCA_E2EE_NATIVE=1`) โ€” see [Native media engine](#native-media-engine).
22
+ - โœ… **sessionGuard** โ€” appstate corruption and silent logout protection, auto-backup
23
+ - โœ… **sendBroadcast** โ€” rate-limited multi-thread broadcast
24
+ - โœ… **Fixed MQTT subscribe race condition** โ€” no more "Connection refused: No subscription existed"
25
+ - โœ… **`isActiveClient()` guard** โ€” stale MQTT client events no longer processed
26
+ - โœ… **`connectTimeout` extended** โ€” no premature logout on slow networks
27
+ - โœ… **autoReconnect** โ€” auto-reconnect on connection drop
28
+ - โœ… **GoatBot compatible** โ€” all API signatures unchanged (`threadID` optional etc.)
29
+ - โœ… **90+ API methods** โ€” sendMessage, editMessage, setMessageReaction, getThreadInfo and more
30
+
31
+ ---
23
32
 
24
- Full results: `docs/TEST_RESULTS.md`. Honest limits: [Known limitations](#known-limitations).
33
+ ## โœจ Features
34
+
35
+ - โœ… Full Messenger API โ€” messages, reactions, attachments, stickers, polls, pins
36
+ - โœ… NEXCA MQTT โ€” stable connection, autoReconnect, jitter, isActiveClient guard
37
+ - โœ… E2EE โ€” Signal Protocol encrypted threads, auto-connects after login (listenE2EE, e2ee.*)
38
+ - โœ… E2EE media engine โ€” native mautrix-go engine (opt-in via `FCA_E2EE_NATIVE=1`) for reliable image/video/audio/document sending, reactions, and unsend in encrypted threads (falls back to the built-in JS engine automatically)
39
+ - โœ… E2EE incoming media โ€” attachments on encrypted messages (including replies) are auto-decrypted and served locally, so existing commands work unmodified
40
+ - โœ… sessionGuard โ€” appstate auto-save, corruption guard, .bak backup
41
+ - โœ… Command anti-spam humanizer โ€” human-like reply delay, per-thread cooldown, GoatBot no-prefix/onChat/onReply auto-detection
42
+ - โœ… sendBroadcast โ€” parallel/sequential multi-thread sending with rate limit
43
+ - โœ… MessengerBot โ€” Discord.js/Telegraf style (.command, .hears, .launch)
44
+ - โœ… createFcaClient โ€” namespaced facade (client.messages, client.threads etc.)
45
+ - โœ… GoatBot / Mirai compatible โ€” drop-in replacement
46
+
47
+ ---
48
+
49
+ ## ๐Ÿ“ฆ Installation
25
50
 
26
- ## Install
27
- Requires Node.js >= 18.
28
51
  ```bash
29
- npm install hridoy-ultimate # from a registry, or:
30
- npm install ./hridoy-ultimate-9.0.0.tgz
52
+ npm install hridoy-ultimate
31
53
  ```
32
- `sqlite3` compiles/downloads a native module; E2EE-related packages are optional.
33
54
 
34
- ## Quick start
35
- ```js
36
- const login = require("hridoy-ultimate"); // also: require(...).login, .default
37
- const fs = require("fs");
55
+ Node.js >= 20 required.
38
56
 
39
- const appState = JSON.parse(fs.readFileSync("appstate.json", "utf8")); // keep this file secret
57
+ The E2EE media engine ships a precompiled native binary (`src/api/socket/e2ee/native/build/`) for **Linux (.so)** and **Windows (.dll)**, loaded via the `koffi` FFI. `npm install` pulls in `koffi` and `yumi-json-bigint` automatically โ€” no extra setup needed. Note the native engine only runs when `FCA_E2EE_NATIVE=1` is set (see [Native media engine](#native-media-engine)); on platforms other than Linux/Windows x64 (e.g. macOS, ARM) the binary fails to load and E2EE media/reactions/unsend automatically fall back to the built-in JS engine (text messaging is unaffected either way).
40
58
 
41
- login({ appState }, { listenEvents: true, autoReconnect: true }, (err, api) => {
42
- if (err) return console.error("login failed:", err.error || err.message || err);
59
+ The E2EE socket's User-Agent defaults to desktop Windows Chrome 139 to match the rest of the client; override with `FCA_E2EE_UA` if you run with a different UA.
43
60
 
44
- const stop = api.listenMqtt((err, event) => {
45
- if (err) {
46
- if (err.type === "stop_listen") return console.error("listener gave up:", err.error);
47
- return console.error(err);
48
- }
49
- if (event.type === "message" && event.body === "ping") {
50
- api.sendMessage("pong", event.threadID, undefined, event.messageID);
51
- api.setMessageReaction("๐Ÿ‘", event.messageID, event.threadID, () => {}, true);
61
+ ---
62
+
63
+ ## ๐Ÿš€ Quick Start
64
+
65
+ ### Classic (GoatBot compatible)
66
+
67
+ ```javascript
68
+ const login = require("hridoy-ultimate");
69
+
70
+ login({ appState: require("./account.json") }, { listenEvents: true }, (err, api) => {
71
+ if (err) throw err;
72
+
73
+ api.sessionGuard("./account.json", {
74
+ interval: 3 * 60 * 1000,
75
+ debounce: 30 * 1000
76
+ });
77
+
78
+ api.listenMqtt((err, event) => {
79
+ if (err) throw err;
80
+ if (event.type === "message") api.sendMessage(event.body, event.threadID);
81
+ });
82
+ });
83
+ ```
84
+
85
+ ### With E2EE (regular + encrypted threads)
86
+
87
+ `connectE2EE()` runs **automatically** right after login โ€” you never call it yourself. Calling it again in your own code opens a second, racing connection on the same bridge. Just check `api.e2ee.isConnected()` if you need to know the status.
88
+
89
+ ```javascript
90
+ login({ appState: require("./account.json") }, { listenEvents: true }, (err, api) => {
91
+ if (err) throw err;
92
+
93
+ api.sessionGuard("./account.json");
94
+ // E2EE is already connecting in the background here โ€” do not call connectE2EE().
95
+
96
+ api.listenE2EE((err, event) => {
97
+ if (err) throw err;
98
+ if (event.type === "message") {
99
+ if (event.isE2EE) {
100
+ api.e2ee.sendMessage(event.threadID, "Got your encrypted message!");
101
+ } else {
102
+ api.sendMessage("Got it!", event.threadID);
103
+ }
52
104
  }
53
- }, { maxReconnectAttempts: 10 });
105
+ });
106
+ });
107
+ ```
108
+
109
+ ### GoatBot login.js
110
+
111
+ ```javascript
112
+ const login = require("hridoy-ultimate");
113
+
114
+ login({ appState }, options, (err, api) => {
115
+ if (err) return;
54
116
 
55
- process.on("SIGINT", async () => {
56
- stop.stopListening(async () => { await api.logout(); process.exit(0); });
117
+ api.sessionGuard(path.join(process.cwd(), "account.txt"), {
118
+ interval: 3 * 60 * 1000,
119
+ debounce: 30 * 1000
57
120
  });
121
+
122
+ // Nothing else needed for E2EE โ€” it auto-connects inside hridoy-ultimate
123
+ // right after login. Do NOT call api.connectE2EE() here.
124
+
125
+ api.listenMqtt(callback);
58
126
  });
59
127
  ```
60
- (Argument orders (`sendMessage(msg, threadID, callback, replyToMessageID)`,
61
- `setMessageReaction(reaction, messageID, threadID, callback, force)`) were read from the source; see
62
- `docs/UPSTREAM_API_DOCS.md` and `index.d.ts`. These calls are **NOT TESTED live**.)
63
-
64
- ## Authentication and session security
65
- - Prefer **appState/cookie login**. Never commit `appstate.json`, cookies or tokens.
66
- - Credential (email/password/2FA) login goes through a **third-party HTTP API that you must
67
- configure** (`apiServer` in `fca-config.json`). It is empty by default because those
68
- secrets would be sent to that host. Only point it at a server you control.
69
- - Log lines mention cookie *counts*, but I did not audit every log call; treat console output as sensitive.
70
- - `api.getAppState()` output equals full account access.
71
-
72
- ## Options
73
- Login options (`setOptions`): `online`, `selfListen`, `listenEvents`, `updatePresence`,
74
- `forceLogin`, `autoMarkRead`, `listenTyping`, `autoReconnect`, `emitReady`,
75
- `selfListenEvent`, `userAgent`, `proxy`, `humanize`, `typingIndicator`.
76
-
77
- `api.listenMqtt(callback, options)` options (all optional):
78
- | option | default | meaning |
79
- |---|---|---|
80
- | `autoReconnect` | `true` | reconnect after socket loss |
81
- | `reconnectDelayMs` | 2000 | first backoff delay (doubles each attempt) |
82
- | `maxReconnectDelayMs` | 300000 | backoff ceiling |
83
- | `reconnectJitter` | 0.3 | +/- fraction randomised |
84
- | `maxReconnectAttempts` | 10 | `0` = unlimited (explicit opt-in); on exhaustion you get `{type:"stop_listen"}` |
85
- | `tmsWaitTimeoutMs` | 8000 | wait for first sync response before cycling |
86
- | `connectTimeoutMs` | 12000 | MQTT connect timeout |
87
- | `cycleMs` | 0 | periodic forced reconnect (off) |
88
- | `reconnectAfterStop` | `false` | restart after `stopListening` |
89
-
90
- The attempt counter resets after the first healthy sync response.
91
-
92
- ## Events and MQTT recovery
93
- - `callback(err, event)`; events include `message`, `message_reply`, `message_edit`,
94
- `message_reaction`, `message_unsend`, `event`, `typ`, `presence`, `share`,
95
- `friend_request_received`, `friend_request_cancel` (type strings found in the source parsers;
96
- per-type shapes are in the upstream docs; none exercised live).
97
- - Control events arrive as `err` objects: `{type:"ready"}`, `{type:"account_inactive"}`
98
- (session lost; everything is stopped), `{type:"stop_listen"}` (retry budget exhausted or stopped).
99
- - Recovery: socket close/error -> backoff reconnect (reuses sync token) -> on repeated
100
- failure refresh session/dtsg -> give up with `stop_listen`. Stale clients are ignored.
101
- - Shutdown: `stopListening(cb)` cancels all timers, ends the client, is idempotent.
102
- `logout()` additionally disconnects E2EE and clears the dtsg refresh timer.
103
-
104
- ## Typing indicator (default ON)
105
- Shows "typing..." in a thread the moment a command-like message arrives (GoatBot prefix,
106
- `/`, `!`, `.`) and clears it when your reply is sent, or after a safety timeout (8 s). It
107
- adds **no latency**: the event is delivered first, the typing signal is fire-and-forget. Works
108
- for normal and E2EE threads.
109
-
110
- ```js
111
- login(cookie, { typingIndicator: false }); // off
112
- login(cookie, { typingIndicator: { onlyCommands: false } }); // type for every text message
113
- login(cookie, { typingIndicator: { commands: ["#"], maxMs: 6000, groups: true, dms: true } });
114
-
115
- api.setTypingIndicator(false); // switch at runtime
116
- await api.withTyping(threadID, async () => { /* slow work */ }); // typing while fn runs
117
- api.sendTypingIndicator(threadID, true, { duration: 5000 }); // manual control
118
- ```
119
- A GoatBot `config.enableTypingIndicator: false` also disables it unless you pass the option
120
- explicitly. When `humanize` is enabled the humanizer shows typing itself and this one stays out of the way.
121
-
122
- ## Opt-in features
123
- - **Humanizer** (`humanize: true`): delays command replies 2-3 s and *drops* commands inside a
124
- 5.5 s cooldown. Drops are intentional and counted; it is off by default. It contains
125
- special handling for GoatBot globals.
126
- - **Native E2EE engine** (group E2EE receive/send and E2EE media): the `messagix` binaries are
127
- bundled in `src/api/socket/e2ee/native/build/` (x86-64 `.so` / `.dll`, SHA-256 pinned in
128
- `THIRD_PARTY_NOTICES.md`). It is still **off by default**: set `FCA_E2EE_NATIVE=1` to enable it
129
- (`FCA_E2EE_NATIVE_LIB=/path` overrides the bundled file, e.g. for ARM or macOS). Why off: the
130
- native and vendored engines register the same E2EE device and kick each other, so enabling it
131
- causes socket churn. Provenance of the binaries is "supplied by the maintainer, not
132
- independently verifiable" - see `THIRD_PARTY_NOTICES.md`.
133
-
134
- ## API surface
135
- Package exports (verified by test): `login`, `default`, `createFcaClient`, `MessengerBot`,
136
- `createMessengerBot`, `MessengerContext`, `attachThreadInfoRealtimeSync`,
137
- `applyThreadInfoRealtimeEvent`, `CommandHumanizer`, `createCommandHumanizer`,
138
- `createHumanizerMiddleware`, `humanizer`.
139
-
140
- Methods attached to `api` after login (one per file in `src/api/`; signatures in upstream docs):
141
- - action: `addExternalModule changeAvatar changeBio dismissScrapingWarning enableAutoSaveAppState getCurrentUserID handleFriendRequest logout refreshFb_dtsg setPostReaction unfriend`
142
- - http: `httpGet httpPost postFormData`
143
- - messaging: `addUserToGroup changeAdminStatus changeArchivedStatus changeBlockedStatus changeGroupImage changeNickname changeThreadColor changeThreadEmoji createNewGroup createPoll createThemeAI deleteMessage deleteThread editMessage forwardAttachment forwardMessage getEmojiUrl getFriendsList getMessage getThemePictures handleMessageRequest markAsDelivered markAsRead markAsReadAll markAsSeen muteThread pinMessage removeUserFromGroup resolvePhotoUrl scheduler searchForThread sendBroadcast sendMessage sendTypingIndicator setMessageReaction setTitle shareContact unsendMessage uploadAttachment`
144
- - threads: `getThreadHistory getThreadInfo getThreadList getThreadPictures`
145
- - users: `getUserID getUserInfo getUserInfoV2`
146
- - socket: `listenMqtt` (= `listen`), `listenE2EE`, `stopListening`, `stopListeningAsync`
147
- (Listed from the source tree; none were exercised against live Facebook.)
148
-
149
- ## Troubleshooting
150
- - `Cannot find module ...` -> run `npm install`; optional E2EE deps may be absent by design.
151
- - Login callback error -> appState expired; re-export cookies. Credential login needs `apiServer`.
152
- - `stop_listen` event -> reconnect budget exhausted; check network/session, then call listen again.
153
- - `account_inactive` -> Facebook invalidated the session (checkpoint/logout); re-login manually.
154
- - A `Fca_Database/` folder appears in the working directory on first use (SQLite cache).
155
-
156
- ## Known limitations
157
- - No live verification of anything that talks to Facebook; Facebook changes endpoints without notice.
158
- - Subscribe-then-publish ordering and the 8 s / 12 s timeouts follow upstream's changelog
159
- reasoning and mqtt.js semantics; not confirmed against live servers.
160
- - Legacy HTTP paths still use the deprecated `request` library (`nexca-utils.js`); porting to axios is future work.
161
- - `sequelize`+`sqlite3` are hard dependencies (heavy); `index.d.ts` is upstream-derived; newer methods are typed loosely (`any`).
162
- - Vendored E2EE bundle has no license header or source.
163
-
164
- ## Migration from eryxenx-fca 1.x
165
- - Package name `hridoy-ultimate`; Node >= 18.
166
- - Humanizer and native E2EE are now **opt-in** (they were default-on in 1.1.x / 1.0.8+).
167
- - 9.0.0: typing indicator is **on by default** (`typingIndicator: false` to disable); the native binaries are bundled again (still opt-in).
168
- - `apiServer` has no default; autoUpdate is off.
169
- - Reconnects are bounded: handle `{type:"stop_listen"}`.
170
- - `api.listenMqtt(cb, options)` gains an optional second argument.
171
-
172
- ## Develop
173
- ```bash
174
- npm install
175
- npm test # unit + integration (mocked) tests
176
- npm run lint # syntax check of every source file
177
- npm pack # builds hridoy-ultimate-9.0.0.tgz
128
+
129
+ **This is the only setup needed โ€” nothing else in `login.js` (or anywhere in GoatBot) has to change for E2EE.** Once login succeeds, `api.e2ee` is already connecting in the background, and every existing GoatBot call โ€” `api.sendMessage(...)` (with or without `attachment`), `api.setMessageReaction(...)`, `api.unsendMessage(...)` โ€” automatically detects when a thread is an encrypted DM and routes through the E2EE engine internally. Commands don't need to know or care whether a thread is encrypted; they call the same functions either way.
130
+
131
+ #### Adding this to your own login.js
132
+
133
+ Your bot's actual `login.js` will look different from the snippet above (dashboard setup, database sync, custom logging, etc.) โ€” you're not replacing the file, just adding/checking two things inside your existing login callback:
134
+
135
+ | Step | What to do |
136
+ |---|---|
137
+ | 1. sessionGuard | Add `api.sessionGuard(accountPath, { interval, debounce })` once, right after login succeeds, if it's not already there. |
138
+ | 2. E2EE | Nothing to add โ€” it's automatic. If your `login.js` already has an explicit `await api.connectE2EE()` call, **delete it** โ€” it's redundant with the automatic connection and the two can race. |
139
+
140
+ To check whether E2EE is already wired into your bot, search for `e2ee.isConnected` in your `login.js` โ€” if it's there, setup is already done.
141
+
142
+ ---
143
+
144
+ ## ๐Ÿ” E2EE โ€” Encrypted Conversations
145
+
146
+ Uses Facebook's real Signal Protocol infrastructure โ€” same as the official Messenger app.
147
+
148
+ ### Setup
149
+
150
+ E2EE connects automatically after login (creating `.nexca/e2ee_device.json` on first run) โ€” there's nothing to call manually. Just check status once connected:
151
+
152
+ ```javascript
153
+ console.log(api.e2ee.isConnected()); // true (may need a moment right after login)
154
+ ```
155
+
156
+ ### Listen (regular + E2EE combined)
157
+
158
+ ```javascript
159
+ api.listenE2EE((err, event) => {
160
+ if (event.type === "message") {
161
+ if (event.isE2EE) {
162
+ api.e2ee.sendMessage(event.threadID, "Encrypted reply!");
163
+ } else {
164
+ api.sendMessage("Normal reply!", event.threadID);
165
+ }
166
+ }
167
+ });
168
+ ```
169
+
170
+ ### E2EE Methods
171
+
172
+ ```javascript
173
+ await api.e2ee.sendMessage(threadID, "Hello!");
174
+ await api.e2ee.sendMessage(threadID, { body: "Photo!", attachment: fs.createReadStream("photo.jpg") });
175
+ await api.e2ee.sendReaction(threadID, messageID, "โค๏ธ");
176
+ await api.e2ee.sendTyping(threadID, true);
177
+ await api.e2ee.unsendMessage(messageID, threadID);
178
+ await api.e2ee.editMessage(threadID, messageID, "Updated!");
179
+ api.e2ee.isConnected();
180
+ await api.e2ee.disconnect();
181
+ ```
182
+
183
+ ### Native media engine
184
+
185
+ Sending image/video/audio/document attachments, reactions, and unsend in E2EE threads can be routed through a precompiled **mautrix-go / mautrix-meta** binary (`src/api/socket/e2ee/native/`) via the `koffi` FFI, instead of this project's own hand-written Signal Protocol encoder. This project's own encoder builds protocol-valid messages that Facebook's servers accept, but real Messenger clients silently fail to render โ€” the native engine avoids that entirely.
186
+
187
+ **โš ๏ธ The native engine is DISABLED by default.** Both the vendored JS engine and the native engine register with Facebook as the *same* E2EE device (same device id), and Facebook's server keeps only one live E2EE socket per device โ€” so when both run at once they kick each other off in a loop (a fresh handshake every few seconds, which looks like automated behavior to Facebook). The vendored JS engine is therefore given exclusive use of the socket unless you opt in:
188
+
189
+ - Set `FCA_E2EE_NATIVE=1` in the environment to enable the native engine. With it on you get E2EE group text receive/send, native media sends, native reactions/unsend/edit/markRead โ€” but you also accept the device-kick socket churn described above (the JS engine reconnects with exponential backoff, so it stays usable, just noisy).
190
+ - With it off (default): E2EE DM text + DM media work through the vendored engine. E2EE **group** send/receive, and E2EE markRead, are unavailable; reactions/unsend/edit in E2EE DMs fall back to the vendored engine.
191
+
192
+ - Text messages always use hridoy-ultimate's own E2EE engine (already reliable, no native dependency).
193
+ - If the native engine fails to load or a send fails (unsupported platform, binary mismatch, etc.), hridoy-ultimate automatically falls back to its own engine and logs `[native-media] ... falling back to legacy vendor engine`.
194
+ - See `src/api/socket/e2ee/native/NOTICE.md` for the required MPL-2.0 attribution to the upstream mautrix-go/mautrix-meta project (ยฉ Tulir Asokan and contributors) โ€” do not remove it if you redistribute this fork.
195
+
196
+ ### Receiving E2EE media (attachments & replies)
197
+
198
+ Incoming encrypted image/video/audio/document messages are automatically decrypted and served over a short-lived local URL (`http://127.0.0.1:<port>/<token>`, 15 min TTL), and populated into `event.attachments` / `event.messageReply.attachments` โ€” same shape as normal (non-E2EE) attachments. Reply-based commands (e.g. "reply to an image with `/imgur`") work the same in E2EE threads as in normal ones, no command code changes needed.
199
+
200
+ ```javascript
201
+ if (event.messageReply && event.messageReply.attachments.length) {
202
+ const url = event.messageReply.attachments[0].url; // works for E2EE too
203
+ }
204
+ ```
205
+
206
+ If media resolution fails, check the console for `[media-resolve]` / `[media-decode]` errors โ€” this usually means the CDN download host (`FB_E2EE_MEDIA_DOWNLOAD_HOST` env var, default `rupload.facebook.com`) needs adjusting for your account/region.
207
+
208
+ ---
209
+
210
+ ## ๐Ÿ›ก๏ธ Command Anti-Spam (Humanizer)
211
+
212
+ Delays and rate-limits command replies so the bot reacts like a person
213
+ typing, not a script firing instantly โ€” and drops commands sent faster
214
+ than a human could plausibly send them (spam bursts).
215
+
216
+ Installed automatically on login (`enabled` by default). No setup needed:
217
+
218
+ ```javascript
219
+ login({ appState }, {}, (err, api) => {
220
+ api.listenMqtt((err, event) => { /* ... */ });
221
+ });
222
+ ```
223
+
224
+ Configure it via the `humanize` login option, or `api.setOptions({ humanize })`:
225
+
226
+ ```javascript
227
+ login({ appState }, {
228
+ humanize: {
229
+ enabled: true,
230
+ reactDelayMs: [2000, 3000], // random delay before a command is let through
231
+ cooldownMs: 5500, // min gap between two accepted commands (per thread)
232
+ resetMs: 10000, // idle gap before cooldown state resets
233
+ maxPerMinute: 20, // global accepted-commands cap
234
+ commands: ["/", "!", "."], // prefixes that count as "this is a command"
235
+ perThread: true // cooldown tracked per thread vs. globally
236
+ }
237
+ }, cb);
238
+ ```
239
+
240
+ `api.setOptions({ humanize: { enabled: false } })` turns it off entirely.
241
+
242
+ ### What counts as a "command"
243
+
244
+ By default, only messages starting with one of the `commands` prefixes are
245
+ treated as commands โ€” everything else (plain chat) passes through
246
+ untouched, with no delay and no typing indicator.
247
+
248
+ If `global.GoatBot` is present (i.e. running under GoatBot-Pro), the
249
+ humanizer **automatically** also treats these as commands, with zero
250
+ config needed:
251
+
252
+ - **No-prefix admin commands** โ€” `config.noPrefix.enable: true` +
253
+ sender in `config.adminBot`, first word matches a real command/alias.
254
+ - **`onChat` triggers** โ€” a message whose full text exactly matches a
255
+ command name registered in `GoatBot.onChat` (e.g. typing `prefix` with
256
+ no `/`, which GoatBot's `prefix.js` reacts to).
257
+ - **`onReply` flows** โ€” a reply to a message tracked in `GoatBot.onReply`
258
+ (e.g. the numeric "1", "2", "3" replies used by GoatBot's `setting.js`
259
+ menu system).
260
+
261
+ This means the whole multi-step `setting` command flow โ€” not just the
262
+ initial `/setting` โ€” gets the same delay/cooldown/typing treatment as a
263
+ normal command.
264
+
265
+ For anything outside these built-ins, give it a custom matcher:
266
+
267
+ ```javascript
268
+ const h = api.getHumanizer();
269
+ h.setMatcher((event) => {
270
+ // your own logic โ€” return true to treat this event as a command
271
+ return event.body === "some no-prefix trigger";
272
+ });
273
+ ```
274
+
275
+ `matcher` can also be passed directly in the `humanize` config
276
+ (`humanize: { matcher: fn }`). A message counts as a command if it matches
277
+ the prefix list **or** a built-in GoatBot check **or** the matcher.
278
+
279
+ ### Typing indicator
280
+
281
+ When a command is accepted, `api.sendTypingIndicator(threadID, true)` is
282
+ called immediately, held for the exact accepted delay, then turned off
283
+ right before the event is forwarded โ€” so the typing indicator and the
284
+ reply delay are the same window, not two stacked waits. Disabled
285
+ automatically if `global.GoatBot.config.enableTypingIndicator` is `false`.
286
+
287
+ ### Inspecting it
288
+
289
+ ```javascript
290
+ const h = api.getHumanizer();
291
+ h.stats(); // { enabled, global: {accepted, dropped, ...}, threads: [...] }
292
+ h.reset(); // clear all cooldown state
293
+ ```
294
+
295
+ > Note: this operates on **incoming** command volume โ€” separate from
296
+ > outgoing send pacing (see `sendBroadcast` below, and any queueing you
297
+ > add yourself around `api.sendMessage`).
298
+
299
+ ---
300
+
301
+ ## ๐Ÿ›ก๏ธ sessionGuard
302
+
303
+ Protects your appstate from corruption and silent logouts.
304
+
305
+ ```javascript
306
+ api.sessionGuard("./account.json");
307
+
308
+ // Custom timing
309
+ api.sessionGuard("./account.json", {
310
+ interval: 3 * 60 * 1000,
311
+ debounce: 30 * 1000
312
+ });
313
+ ```
314
+
315
+ What it does:
316
+ - Auto-saves appstate every N minutes
317
+ - Saves after every successful sendMessage (debounced)
318
+ - Corruption guard โ€” never overwrites a larger appstate with a smaller one
319
+ - Auto-backup โ€” writes `.bak` before every overwrite
320
+
321
+ ```javascript
322
+ api.saveSession(); // force save now
323
+ api.restoreSessionBackup(); // restore from .bak
324
+ api.stopSessionGuard(); // stop the timer
325
+ ```
326
+
327
+ ---
328
+
329
+ ## ๐Ÿ“ก sendBroadcast
330
+
331
+ Rate-limited multi-thread broadcast.
332
+
333
+ ```javascript
334
+ const result = await api.sendBroadcast(
335
+ "Hello everyone!",
336
+ ["THREAD_1", "THREAD_2", "THREAD_3"],
337
+ {
338
+ delay: 2000,
339
+ parallel: 2,
340
+ onEach: (err, info, id) => {
341
+ console.log(err ? "Failed: " + id : "Sent: " + id);
342
+ }
343
+ }
344
+ );
345
+ console.log(result.sent.length + "/" + result.total + " delivered");
346
+ ```
347
+
348
+ ---
349
+
350
+ ## ๐Ÿค– MessengerBot
351
+
352
+ Discord.js/Telegraf style high-level bot class.
353
+
354
+ ```javascript
355
+ const { createMessengerBot } = require("hridoy-ultimate");
356
+
357
+ const bot = await createMessengerBot(
358
+ { appState: require("./account.json") },
359
+ { commandPrefix: "/", stopOnSignals: true }
360
+ );
361
+
362
+ bot.command("ping", async ctx => await ctx.replyAsync("pong ๐Ÿ“"));
363
+ bot.hears(/hello/i, async ctx => await ctx.replyAsync("Hi! ๐Ÿ‘‹"));
364
+ bot.on("messageCreate", event => console.log(event.body));
365
+
366
+ await bot.launch({ stopOnSignals: true });
367
+ ```
368
+
369
+ ---
370
+
371
+ ## ๐ŸŽฏ createFcaClient
372
+
373
+ Namespaced facade grouping all API methods by domain.
374
+
375
+ ```javascript
376
+ const { createFcaClient } = require("hridoy-ultimate");
377
+ const client = createFcaClient(api);
378
+
379
+ await client.messages.send("Hello!", threadID);
380
+ await client.messages.react("โค๏ธ", messageID, threadID);
381
+ await client.threads.getInfo(threadID);
382
+ await client.users.getInfo(userID);
383
+ await client.account.refreshDtsg();
384
+ ```
385
+
386
+ ---
387
+
388
+ ## ๐Ÿ“– API Reference
389
+
390
+ ### Sending Messages
391
+
392
+ ```javascript
393
+ api.sendMessage("Hello!", threadID);
394
+ api.sendMessage({ body: "Photo!", attachment: fs.createReadStream("photo.jpg") }, threadID);
395
+ api.sendMessage({ body: "Hey @John", mentions: [{ id: "uid", tag: "@John", fromIndex: 4 }] }, threadID);
396
+ api.sendMessage({ sticker: "369239263222822" }, threadID);
397
+ api.sendMessage({ location: { latitude: 23.8, longitude: 90.4, current: true } }, threadID);
398
+ api.sendBroadcast("msg", ["tid1", "tid2"], { delay: 2000 });
399
+ api.sendGif("https://media.giphy.com/xyz.gif", threadID);
400
+ api.sendLocation(23.8, 90.4, threadID);
401
+ api.sendImage("./photo.jpg", threadID, "caption");
402
+ api.sendVideo("./video.mp4", threadID);
403
+ api.sendAudio("./voice.ogg", threadID);
404
+ api.sendFile("./doc.pdf", threadID);
405
+ api.shareLink("https://github.com", threadID, "Check this!");
406
+ api.shareContact("Meet my friend!", userID, threadID);
407
+ ```
408
+
409
+ ### Message Actions
410
+
411
+ ```javascript
412
+ api.editMessage("Updated text", messageID);
413
+ api.unsendMessage(messageID);
414
+ api.deleteMessage([messageID]);
415
+ api.setMessageReaction("๐Ÿ˜", messageID, threadID); // threadID optional
416
+ api.setMessageReaction("", messageID); // remove reaction
417
+ api.getMessage(threadID, messageID);
418
+ api.forwardAttachment(attachmentID, [userID]);
419
+ api.uploadAttachment([fs.createReadStream("photo.jpg")]);
420
+ ```
421
+
422
+ ### Read Receipts & Typing
423
+
424
+ ```javascript
425
+ api.markAsRead(threadID);
426
+ api.markAsReadAll();
427
+ api.markAsDelivered(threadID, messageID);
428
+ api.markAsSeen();
429
+ api.sendTypingIndicator(threadID, true);
430
+ ```
431
+
432
+ ### Thread Management
433
+
434
+ ```javascript
435
+ api.getThreadInfo(threadID);
436
+ api.getThreadList(10, null, ["INBOX"]);
437
+ api.getThreadHistory(threadID, 20);
438
+ api.createGroup("Hey!", ["uid1", "uid2"]);
439
+ api.deleteThread(threadID);
440
+ api.muteThread(threadID, 3600);
441
+ api.changeArchivedStatus(threadID, true);
442
+ api.handleMessageRequest(threadID, true);
443
+ api.searchForThread("query");
444
+ ```
445
+
446
+ ### Thread Customization
447
+
448
+ ```javascript
449
+ api.setTitle("New Name", threadID);
450
+ api.changeThreadColor("#0084FF", threadID);
451
+ api.changeThreadEmoji("๐Ÿ”ฅ", threadID);
452
+ api.changeNickname("The Boss", threadID, userID);
453
+ api.changeGroupImage(fs.createReadStream("group.jpg"), threadID);
454
+ api.changeAdminStatus(threadID, userID, true);
455
+ api.addUserToGroup(userID, threadID);
456
+ api.removeUserFromGroup(userID, threadID);
457
+ api.createPoll("Question?", threadID, { "Yes": false, "No": false });
458
+ api.pinMessage(messageID, threadID);
459
+ api.unpinMessage(messageID, threadID);
460
+ ```
461
+
462
+ ### User Info
463
+
464
+ ```javascript
465
+ api.getUserInfo(userID);
466
+ api.getUserID("John Doe", callback);
467
+ api.getUID("https://facebook.com/zuck");
468
+ api.getFriendsList();
469
+ api.getAvatarUser(userID);
470
+ api.getProfileInfo(userID);
471
+ api.getPublicData(userID);
472
+ api.sendFriendRequest(userID);
473
+ api.handleFriendRequest(userID, true);
474
+ api.changeBlockedStatus(userID, true);
475
+ api.followUser(userID);
476
+ api.unfollowUser(userID);
477
+ api.unfriend(userID);
478
+ ```
479
+
480
+ ### Social
481
+
482
+ ```javascript
483
+ api.reactToPost(postID, "love");
484
+ api.reactToComment(commentID, "haha");
485
+ api.postComment(postID, "Great post!");
486
+ api.sharePost(postID, "Check this!");
178
487
  ```
179
- Credits and licenses: `LICENSE`, `THIRD_PARTY_NOTICES.md`. Merge details: `MERGE_REPORT.md`.
488
+
489
+ ### Account & Config
490
+
491
+ ```javascript
492
+ api.getCurrentUserID();
493
+ api.getAppState();
494
+ api.setOptions({ listenTyping: true });
495
+ api.logout();
496
+ api.refreshFb_dtsg();
497
+ api.addExternalModule("myFunc", (defaultFuncs, api, ctx) => {
498
+ return function(text, threadID) {
499
+ return api.sendMessage("[BOT] " + text, threadID);
500
+ };
501
+ });
502
+ ```
503
+
504
+ ### HTTP Utilities
505
+
506
+ ```javascript
507
+ api.httpGet(url, params, callback);
508
+ api.httpPost(url, form, callback);
509
+ api.httpPostFormData(url, form, callback);
510
+ api.uploadImageToImgbb(imageUrl);
511
+ ```
512
+
513
+ ---
514
+
515
+ ## ๐Ÿ“‹ Login Options
516
+
517
+ | Option | Type | Default | Description |
518
+ |--------|------|---------|-------------|
519
+ | `selfListen` | boolean | false | Receive your own sent messages |
520
+ | `listenEvents` | boolean | true | Receive thread/group events |
521
+ | `listenTyping` | boolean | false | Receive typing indicator events |
522
+ | `updatePresence` | boolean | false | Receive online/offline presence events |
523
+ | `autoMarkDelivery` | boolean | false | Auto-mark incoming messages as delivered |
524
+ | `autoMarkRead` | boolean | false | Auto-mark threads as read |
525
+ | `autoReconnect` | boolean | true | Auto-reconnect MQTT on disconnect |
526
+ | `online` | boolean | false | Appear as online to others |
527
+ | `emitReady` | boolean | false | Emit ready event when MQTT connected |
528
+ | `proxy` | string | โ€” | HTTP proxy URL |
529
+ | `userAgent` | string | Safari UA | Override HTTP User-Agent |
530
+
531
+ ---
532
+
533
+ ## ๐Ÿ“„ License
534
+
535
+ MIT License
536
+
537
+ **hridoy-ultimate** v9.0.1 by [HR ID OY](https://github.com/hridoyhossen48360)
538
+
539
+ > The E2EE native media engine (`src/api/socket/e2ee/native/`) bundles a
540
+ > precompiled binary built from **mautrix-go** / **mautrix-meta**, ยฉ Tulir
541
+ > Asokan and contributors, licensed under **MPL-2.0** (not MIT). See
542
+ > `src/api/socket/e2ee/native/NOTICE.md` for full attribution โ€” this notice
543
+ > must be preserved in any redistribution of this fork.
544
+ NEXCA MQTT core by [Deku](https://github.com/dekuzxc) โ€” MIT License
545
+
546
+ > Unauthorized copying or redistribution without credit is prohibited.
547
+
548
+ ---
549
+
550
+ <div align="center">
551
+
552
+ Made with โค๏ธ by HR ID OY
553
+
554
+ </div>