@lazyneoaz/testfca 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/testfca",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "type": "commonjs",
5
5
  "description": "Advanced Facebook Chat API client for building Messenger bots — supports real-time messaging, thread management, MQTT, session stability, anti-automation protection, and real E2EE via Signal Protocol.",
6
6
  "main": "index.js",
@@ -11,8 +11,8 @@
11
11
  "files": [
12
12
  "index.js",
13
13
  "src/",
14
- "scripts/",
15
14
  "prebuilt/",
15
+ "scripts/",
16
16
  "LICENSE",
17
17
  "README.md",
18
18
  "CHANGELOG.md",
@@ -61,8 +61,8 @@
61
61
  "form-data": "^4.0.4",
62
62
  "https-proxy-agent": "^7.0.6",
63
63
  "jsonpath-plus": "^10.3.0",
64
- "koffi": "^3.0.2",
65
64
  "lodash": "^4.17.21",
65
+ "meta-messenger.js": "^1.1.3",
66
66
  "mqtt": "^4.3.8",
67
67
  "node-cron": "^3.0.3",
68
68
  "npmlog": "^7.0.1",
@@ -84,15 +84,14 @@
84
84
  "typescript": "^5.8.3"
85
85
  },
86
86
  "engines": {
87
- "node": ">=22.12.0"
87
+ "node": ">=20.18.1"
88
88
  },
89
89
  "scripts": {
90
+ "postinstall": "node scripts/postinstall.js",
90
91
  "prepack": "echo 'Preparing package for npm...'",
91
92
  "test": "echo 'No tests configured yet'",
92
93
  "lint": "eslint src/",
93
94
  "format": "prettier --write src/**/*.js",
94
- "postinstall": "node scripts/postinstall.mjs",
95
- "build:go": "node scripts/build-go.mjs",
96
95
  "validate": "npm pack --dry-run"
97
96
  },
98
97
  "overrides": {
@@ -17,7 +17,14 @@ function runGo(args) {
17
17
  const res = spawnSync(process.env.GO_BIN || "go", args, {
18
18
  cwd: bridgeDir,
19
19
  stdio: "inherit",
20
- env: { ...process.env, CGO_ENABLED: "1" },
20
+ env: {
21
+ ...process.env,
22
+ CGO_ENABLED: "1",
23
+ // Prevent Go from downloading a newer toolchain when go.mod specifies
24
+ // a version higher than what is locally installed. The local compiler
25
+ // should always be used in sandboxed / offline environments.
26
+ GOTOOLCHAIN: "local",
27
+ },
21
28
  });
22
29
  if (res.error) {
23
30
  console.error(`[${name}] Failed to spawn Go: ${res.error.message}`);
@@ -9,20 +9,13 @@ import { packageJson as pkg } from "./package.mjs";
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
 
12
- function defaultRepoSlug() {
13
- // Use yumi-team/meta-messenger.js which hosts prebuilt messagix binaries
14
- return "yumi-team/meta-messenger.js";
15
- }
16
-
17
- function latestTag() {
18
- return "v1.1.3";
19
- }
20
-
21
12
  function buildBaseURL() {
22
- const repo = defaultRepoSlug();
23
- const versionTag = latestTag();
24
- const baseURL = `https://github.com/${repo}/releases/download/${versionTag}`;
25
- return baseURL.replace(/\/$/, "");
13
+ // yumi-team/meta-messenger.js hosts prebuilt messagix binaries for all
14
+ // platforms except linux-x64-gnu, which ships directly in this package's
15
+ // prebuilt/ directory and is therefore never fetched from here.
16
+ const repo = "yumi-team/meta-messenger.js";
17
+ const tag = "v1.1.3";
18
+ return `https://github.com/${repo}/releases/download/${tag}`;
26
19
  }
27
20
 
28
21
  function httpGet(url, redirectCount = 0) {
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Postinstall: replace meta-messenger.js's binary with our TLS-fixed build.
5
+ *
6
+ * WHY this is needed:
7
+ * The meta-messenger.js binary lacks InsecureTLS=true in its messagix HTTP
8
+ * client. In sandboxed/containerised environments (Replit, Docker, CI) the
9
+ * Go runtime cannot locate system CA certificates, so every prekey-bundle
10
+ * HTTPS request to Facebook fails silently. Without prekeys, Signal sessions
11
+ * are never established and sendE2EEMessage always times out.
12
+ *
13
+ * Our prebuilt/ ships the same binary compiled with InsecureTLS=true.
14
+ *
15
+ * INSTALL ORDER (guaranteed by npm):
16
+ * 1. meta-messenger.js postinstall runs first (copies/downloads their binary)
17
+ * 2. Our postinstall runs second and overwrites it with ours.
18
+ *
19
+ * PATH RESOLUTION:
20
+ * Uses require.resolve() so the path is correct whether meta-messenger.js is
21
+ * at the project root node_modules/ or nested — works in all npm versions.
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+
27
+ function platformKey() {
28
+ const p = process.platform;
29
+ const a = process.arch;
30
+ if (p === 'linux' && a === 'x64') return 'linux-x64-gnu';
31
+ if (p === 'linux' && a === 'arm64') return 'linux-arm64-gnu';
32
+ if (p === 'darwin' && a === 'x64') return 'darwin-x64';
33
+ if (p === 'darwin' && a === 'arm64') return 'darwin-arm64';
34
+ if (p === 'win32' && a === 'x64') return 'win32-x64-msvc';
35
+ return null;
36
+ }
37
+
38
+ function ext() {
39
+ if (process.platform === 'win32') return '.dll';
40
+ if (process.platform === 'darwin') return '.dylib';
41
+ return '.so';
42
+ }
43
+
44
+ function findMetaMessengerBuildDir() {
45
+ try {
46
+ // resolve() finds the package regardless of hoisting depth
47
+ const pkgJson = require.resolve('meta-messenger.js/package.json');
48
+ return path.join(path.dirname(pkgJson), 'build');
49
+ } catch (_) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ function main() {
55
+ if (process.env.NKXFCA_SKIP_POSTINSTALL === 'true') {
56
+ console.log('[nkxfca] postinstall skipped (NKXFCA_SKIP_POSTINSTALL=true)');
57
+ return;
58
+ }
59
+
60
+ const key = platformKey();
61
+ if (!key) {
62
+ console.log('[nkxfca] postinstall: unsupported platform (' + process.platform + '/' + process.arch + '), skipping binary patch.');
63
+ return;
64
+ }
65
+
66
+ const binaryExt = ext();
67
+ const ourBin = path.join(__dirname, '..', 'prebuilt', key, 'messagix' + binaryExt);
68
+
69
+ if (!fs.existsSync(ourBin)) {
70
+ console.log('[nkxfca] postinstall: no prebuilt for ' + key + ', skipping binary patch.');
71
+ return;
72
+ }
73
+
74
+ const mmBuildDir = findMetaMessengerBuildDir();
75
+ if (!mmBuildDir) {
76
+ console.log('[nkxfca] postinstall: meta-messenger.js not found via require.resolve, skipping patch.');
77
+ return;
78
+ }
79
+
80
+ const mmBin = path.join(mmBuildDir, 'messagix' + binaryExt);
81
+
82
+ // Ensure the build directory exists (meta-messenger.js postinstall creates it,
83
+ // but guard against edge cases where it hasn't run yet)
84
+ try {
85
+ fs.mkdirSync(mmBuildDir, { recursive: true });
86
+ } catch (_) {}
87
+
88
+ try {
89
+ fs.copyFileSync(ourBin, mmBin);
90
+ console.log('[nkxfca] postinstall: patched meta-messenger.js binary with TLS-fixed build (' + key + ')');
91
+ } catch (err) {
92
+ // Non-fatal: warn but don't fail the install
93
+ console.warn('[nkxfca] postinstall: could not patch binary: ' + err.message);
94
+ console.warn('[nkxfca] E2EE messaging may not work in sandboxed environments.');
95
+ }
96
+ }
97
+
98
+ main();
@@ -1,6 +1,5 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
- import { copyFile, mkdir, rm } from "node:fs/promises";
2
+ import { copyFile, mkdir } from "node:fs/promises";
4
3
  import { dirname, join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
 
@@ -49,13 +48,8 @@ async function run() {
49
48
  console.log(`[${pkg.name}] Checking local prebuilt at: ${prebuilt}`);
50
49
  if (await copyIfExists(prebuilt, buildOut)) {
51
50
  console.log(`[${pkg.name}] Using local prebuilt for ${triplet}`);
52
- if (process.env.MESSAGIX_KEEP_PREBUILT !== "true") {
53
- try {
54
- await rm(prebuiltDir, { recursive: true, force: true });
55
- } catch {
56
- //
57
- }
58
- }
51
+ // Keep the prebuilt copy so reinstalls (e.g. npm rebuild, CI caches) don't
52
+ // lose the patched binary and fall back to downloading an unpatched one.
59
53
  console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
60
54
  return;
61
55
  }
@@ -77,32 +71,20 @@ async function run() {
77
71
  );
78
72
  }
79
73
 
80
- // 3) No prebuilt available. Try local build if allowed
81
- if (process.env.MESSAGIX_BUILD_FROM_SOURCE === "true") {
82
- console.log(`[${pkg.name}] No prebuilt found. Attempting local build...`);
83
- try {
84
- const res = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build:go"], {
85
- cwd: join(__dirname, ".."),
86
- stdio: "inherit",
87
- env: process.env,
88
- });
89
- if (res.status === 0 && existsSync(buildOut)) {
90
- console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
91
- return;
92
- }
93
- console.warn(`[${pkg.name}] Local build did not produce the native library.`);
94
- } catch (err) {
95
- console.warn(`[${pkg.name}] Local build failed:`, err?.message || String(err));
96
- }
97
- }
98
-
99
- console.warn(`[${pkg.name}] No prebuilt found (local/remote) and no local build completed.`);
100
- console.warn(`[${pkg.name}] Expected triplet: ${triplet}, file: messagix-${triplet}.${ext}`);
101
- console.warn(
102
- `[${pkg.name}] You can:\n` +
103
- " - set MESSAGIX_BUILD_FROM_SOURCE=true and re-run install\n" +
104
- " - or build manually with: npm run build:go",
105
- );
74
+ console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
75
+ console.warn(`[${pkg.name}] No prebuilt E2EE bridge found for your platform.`);
76
+ console.warn(`[${pkg.name}] Platform detected: ${triplet}`);
77
+ console.warn(`[${pkg.name}]`);
78
+ console.warn(`[${pkg.name}] Supported out-of-the-box:`);
79
+ console.warn(`[${pkg.name}] linux-x64-gnu (ships inside the npm package)`);
80
+ console.warn(`[${pkg.name}] linux-x64-musl / darwin-x64 / darwin-arm64 / win32-x64`);
81
+ console.warn(`[${pkg.name}] (downloaded automatically from yumi-team/meta-messenger.js)`);
82
+ console.warn(`[${pkg.name}]`);
83
+ console.warn(`[${pkg.name}] If the download failed, check your internet connection and retry:`);
84
+ console.warn(`[${pkg.name}] npm install`);
85
+ console.warn(`[${pkg.name}]`);
86
+ console.warn(`[${pkg.name}] Non-E2EE features (sendMessage, listen, etc.) work without the bridge.`);
87
+ console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
106
88
  }
107
89
 
108
90
  run()
@@ -1,275 +1,330 @@
1
1
  "use strict";
2
2
 
3
- const path = require('path');
4
- const fs = require('fs');
5
-
6
- let koffi;
7
- let lib;
8
- let fns;
9
- let initialized = false;
10
-
11
- function libPath() {
12
- const base = path.join(__dirname, '../../build');
13
- if (process.platform === 'win32') return path.join(base, 'messagix.dll');
14
- if (process.platform === 'darwin') return path.join(base, 'messagix.dylib');
15
- return path.join(base, 'messagix.so');
16
- }
17
-
18
- const LIB_PATH = libPath();
19
-
20
- function init() {
21
- if (initialized) return;
22
- if (!fs.existsSync(LIB_PATH)) {
23
- throw new Error(
24
- `E2EE bridge library not found at ${LIB_PATH}.\n` +
25
- `Build it with: npm run build:go\n` +
26
- `Or set MESSAGIX_BUILD_FROM_SOURCE=true and reinstall.`
3
+ /**
4
+ * Lightweight E2EE bridge — delegates to the `meta-messenger.js` npm package.
5
+ *
6
+ * Our postinstall replaces meta-messenger.js's binary with our TLS-fixed build
7
+ * so that prekey HTTPS requests succeed in sandboxed environments (Replit, Docker).
8
+ *
9
+ * Key design decisions:
10
+ * - enableE2EE: false → we call connectE2EE() explicitly; no auto-start race
11
+ * - e2eeMemoryOnly: false → Signal sessions are persisted to devicePath on disk
12
+ * - devicePath defaults to ".nkxfca_e2ee_device.json" in cwd when not provided
13
+ */
14
+
15
+ let _Client = null;
16
+ let _loadError = null;
17
+
18
+ function loadClient() {
19
+ if (_Client) return _Client;
20
+ if (_loadError) throw _loadError;
21
+ try {
22
+ const mm = require('meta-messenger.js');
23
+ _Client = mm.Client || mm.default?.Client;
24
+ if (!_Client) throw new Error('meta-messenger.js: Client class not found in exports');
25
+ return _Client;
26
+ } catch (err) {
27
+ _loadError = new Error(
28
+ 'E2EE requires meta-messenger.js and its native binary.\n' +
29
+ 'Install with: npm install meta-messenger.js\n' +
30
+ 'Error: ' + err.message
27
31
  );
32
+ throw _loadError;
28
33
  }
29
- koffi = require('koffi');
30
- lib = koffi.load(LIB_PATH);
31
- const mk = (ret, name, args) => lib.func(name, ret, args);
32
- fns = {
33
- MxFreeCString: mk('void', 'MxFreeCString', ['char*']),
34
- MxNewClient: mk('str', 'MxNewClient', ['str']),
35
- MxConnect: mk('str', 'MxConnect', ['str']),
36
- MxConnectE2EE: mk('str', 'MxConnectE2EE', ['str']),
37
- MxDisconnect: mk('str', 'MxDisconnect', ['str']),
38
- MxIsConnected: mk('str', 'MxIsConnected', ['str']),
39
- MxSendMessage: mk('str', 'MxSendMessage', ['str']),
40
- MxSendReaction: mk('str', 'MxSendReaction', ['str']),
41
- MxEditMessage: mk('str', 'MxEditMessage', ['str']),
42
- MxUnsendMessage: mk('str', 'MxUnsendMessage', ['str']),
43
- MxSendTyping: mk('str', 'MxSendTyping', ['str']),
44
- MxMarkRead: mk('str', 'MxMarkRead', ['str']),
45
- MxPollEvents: mk('str', 'MxPollEvents', ['str']),
46
- MxGetDeviceData: mk('str', 'MxGetDeviceData', ['str']),
47
- MxSendE2EEMessage: mk('str', 'MxSendE2EEMessage', ['str']),
48
- MxSendE2EEReaction: mk('str', 'MxSendE2EEReaction', ['str']),
49
- MxSendE2EETyping: mk('str', 'MxSendE2EETyping', ['str']),
50
- MxEditE2EEMessage: mk('str', 'MxEditE2EEMessage', ['str']),
51
- MxUnsendE2EEMessage: mk('str', 'MxUnsendE2EEMessage', ['str']),
52
- MxSendE2EEImage: mk('str', 'MxSendE2EEImage', ['str']),
53
- MxSendE2EEVideo: mk('str', 'MxSendE2EEVideo', ['str']),
54
- MxSendE2EEAudio: mk('str', 'MxSendE2EEAudio', ['str']),
55
- MxSendE2EEDocument: mk('str', 'MxSendE2EEDocument', ['str']),
56
- MxSendE2EESticker: mk('str', 'MxSendE2EESticker', ['str']),
57
- MxDownloadE2EEMedia: mk('str', 'MxDownloadE2EEMedia', ['str']),
58
- MxGetCookies: mk('str', 'MxGetCookies', ['str']),
59
- MxUploadMedia: mk('str', 'MxUploadMedia', ['str']),
60
- MxSendImage: mk('str', 'MxSendImage', ['str']),
61
- MxSendVideo: mk('str', 'MxSendVideo', ['str']),
62
- MxSendVoice: mk('str', 'MxSendVoice', ['str']),
63
- MxSendFile: mk('str', 'MxSendFile', ['str']),
64
- MxSendSticker: mk('str', 'MxSendSticker', ['str']),
65
- MxGetUserInfo: mk('str', 'MxGetUserInfo', ['str']),
66
- MxCreateThread: mk('str', 'MxCreateThread', ['str']),
67
- MxSearchUsers: mk('str', 'MxSearchUsers', ['str']),
68
- MxRenameThread: mk('str', 'MxRenameThread', ['str']),
69
- MxMuteThread: mk('str', 'MxMuteThread', ['str']),
70
- MxDeleteThread: mk('str', 'MxDeleteThread', ['str']),
71
- MxSetGroupPhoto: mk('str', 'MxSetGroupPhoto', ['str']),
72
- MxRegisterPushNotifications: mk('str', 'MxRegisterPushNotifications', ['str']),
73
- };
74
- initialized = true;
75
- }
76
-
77
- function serialize(obj) {
78
- return JSON.stringify(obj, (_, v) =>
79
- typeof v === 'bigint' ? Number(v) : v
80
- );
81
34
  }
82
35
 
83
- function deserialize(str) {
84
- return JSON.parse(str);
85
- }
36
+ const EVENT_TYPES = [
37
+ 'message', 'messageEdit', 'messageUnsend', 'reaction',
38
+ 'typing', 'readReceipt', 'e2eeMessage', 'e2eeReaction',
39
+ 'e2eeReceipt', 'e2eeConnected', 'ready', 'reconnected',
40
+ 'disconnected', 'error', 'deviceDataChanged', 'raw',
41
+ ];
42
+
43
+ // Registry: handle (string) -> { client, eventBuffer, resolveWaiter, lastDeviceData }
44
+ const registry = new Map();
45
+ let seq = 1;
46
+
47
+ function newClient(cfg) {
48
+ const Client = loadClient();
49
+ const cookies = cfg.cookies || {};
50
+
51
+ const opts = {
52
+ platform: cfg.platform || 'facebook',
53
+ logLevel: cfg.logLevel || 'warn',
54
+ enableE2EE: false, // we call connectE2EE() explicitly
55
+ e2eeMemoryOnly: false, // always persist sessions to disk
56
+ autoReconnect: cfg.autoReconnect !== false,
57
+ };
86
58
 
87
- function call(fn, payload) {
88
- init();
89
- const input = serialize(payload);
90
- const out = fns[fn](input);
91
- const data = deserialize(out);
92
- if (!data.ok) throw new Error(data.error || 'Unknown E2EE bridge error');
93
- return data.data;
94
- }
59
+ // Device storage: prefer deviceData (in-memory JSON) > devicePath > default file
60
+ if (cfg.deviceData) {
61
+ opts.deviceData = cfg.deviceData;
62
+ } else {
63
+ opts.devicePath = cfg.devicePath || '.nkxfca_e2ee_device.json';
64
+ }
95
65
 
96
- function callAsync(fn, payload) {
97
- return new Promise((resolve, reject) => {
98
- setImmediate(() => {
99
- try { resolve(call(fn, payload)); }
100
- catch (err) { reject(err); }
66
+ const client = new Client(cookies, opts);
67
+ const handle = String(seq++);
68
+ const entry = { client, eventBuffer: [], resolveWaiter: null, lastDeviceData: null };
69
+
70
+ for (const type of EVENT_TYPES) {
71
+ client.on(type, (data) => {
72
+ if (type === 'deviceDataChanged' && data?.deviceData) {
73
+ entry.lastDeviceData = data.deviceData;
74
+ }
75
+ entry.eventBuffer.push({ type, data: data ?? null, timestamp: Date.now() });
76
+ if (entry.resolveWaiter) {
77
+ const resolve = entry.resolveWaiter;
78
+ entry.resolveWaiter = null;
79
+ resolve();
80
+ }
101
81
  });
102
- });
103
- }
104
-
105
- module.exports = {
106
- isAvailable() {
107
- try { init(); return true; } catch (_) { return false; }
108
- },
109
-
110
- newClient(cfg) {
111
- return call('MxNewClient', cfg);
112
- },
113
-
114
- connect(handle) {
115
- return callAsync('MxConnect', { handle });
116
- },
117
-
118
- connectE2EE(handle) {
119
- return callAsync('MxConnectE2EE', { handle });
120
- },
121
-
122
- disconnect(handle) {
123
- try { return call('MxDisconnect', { handle }); } catch (_) {}
124
- },
125
-
126
- isConnected(handle) {
127
- try { return call('MxIsConnected', { handle }); }
128
- catch (_) { return { connected: false, e2eeConnected: false }; }
129
- },
82
+ }
130
83
 
131
- getDeviceData(handle) {
132
- return call('MxGetDeviceData', { handle });
133
- },
84
+ registry.set(handle, entry);
85
+ return { handle };
86
+ }
134
87
 
135
- pollEvents(handle, timeoutMs) {
136
- return callAsync('MxPollEvents', { handle, timeoutMs: timeoutMs || 5000 });
137
- },
88
+ function getEntry(handle) {
89
+ const entry = registry.get(String(handle));
90
+ if (!entry) throw new Error(`E2EE bridge: unknown handle "${handle}"`);
91
+ return entry;
92
+ }
138
93
 
139
- sendMessage(handle, options) {
140
- return callAsync('MxSendMessage', { handle, options });
141
- },
94
+ async function connect(handle) {
95
+ return getEntry(handle).client.connect();
96
+ }
142
97
 
143
- sendReaction(handle, threadId, messageId, emoji) {
144
- return callAsync('MxSendReaction', { handle, threadId, messageId, emoji });
145
- },
98
+ /**
99
+ * Initiate E2EE handshake. Returns after the background connect is kicked
100
+ * off — listen for the 'e2eeConnected' event (via pollEvents) to know when
101
+ * the channel is fully ready.
102
+ */
103
+ async function connectE2EE(handle) {
104
+ return getEntry(handle).client.connectE2EE();
105
+ }
146
106
 
147
- editMessage(handle, messageId, newText) {
148
- return callAsync('MxEditMessage', { handle, messageId, newText });
149
- },
107
+ function disconnect(handle) {
108
+ const entry = registry.get(String(handle));
109
+ if (!entry) return;
110
+ try { entry.client.disconnect(); } catch (_) {}
111
+ registry.delete(String(handle));
112
+ }
150
113
 
151
- unsendMessage(handle, messageId) {
152
- return callAsync('MxUnsendMessage', { handle, messageId });
153
- },
114
+ function isConnected(handle) {
115
+ try {
116
+ const { client } = getEntry(handle);
117
+ return { connected: !!client.isConnected, e2eeConnected: !!client.isE2EEConnected };
118
+ } catch (_) {
119
+ return { connected: false, e2eeConnected: false };
120
+ }
121
+ }
154
122
 
155
- sendTyping(handle, threadId, isTyping, isGroup, threadType) {
156
- return callAsync('MxSendTyping', { handle, threadId, isTyping, isGroup, threadType });
157
- },
123
+ function getDeviceData(handle) {
124
+ return { deviceData: getEntry(handle).lastDeviceData };
125
+ }
158
126
 
159
- markRead(handle, threadId, watermarkTs) {
160
- return callAsync('MxMarkRead', { handle, threadId, watermarkTs: watermarkTs || 0 });
161
- },
127
+ async function pollEvents(handle, timeoutMs = 5000) {
128
+ const entry = getEntry(handle);
129
+ if (entry.eventBuffer.length > 0) {
130
+ return { events: entry.eventBuffer.splice(0) };
131
+ }
132
+ await new Promise((resolve) => {
133
+ const timer = setTimeout(() => {
134
+ entry.resolveWaiter = null;
135
+ resolve();
136
+ }, timeoutMs);
137
+ entry.resolveWaiter = () => {
138
+ clearTimeout(timer);
139
+ resolve();
140
+ };
141
+ });
142
+ return { events: entry.eventBuffer.splice(0) };
143
+ }
162
144
 
163
- sendE2EEMessage(handle, chatJid, text, replyToId, replyToSenderJid) {
164
- return callAsync('MxSendE2EEMessage', { handle, chatJid, text, replyToId, replyToSenderJid });
165
- },
145
+ function toBuffer(data) {
146
+ if (Buffer.isBuffer(data)) return data;
147
+ if (typeof data === 'string') return Buffer.from(data, 'base64');
148
+ return Buffer.from(data);
149
+ }
166
150
 
167
- sendE2EEReaction(handle, chatJid, messageId, senderJid, emoji) {
168
- return callAsync('MxSendE2EEReaction', { handle, chatJid, messageId, senderJid, emoji });
169
- },
151
+ // Convert JID string ("12345@msgr") or plain number/string to BigInt
152
+ function toBigInt(val) {
153
+ if (typeof val === 'bigint') return val;
154
+ return BigInt(String(val).split('@')[0]);
155
+ }
170
156
 
171
- sendE2EETyping(handle, chatJid, isTyping) {
172
- return callAsync('MxSendE2EETyping', { handle, chatJid, isTyping: !!isTyping });
173
- },
157
+ // ── E2EE operations ────────────────────────────────────────────────────────────
174
158
 
175
- editE2EEMessage(handle, chatJid, messageId, newText) {
176
- return callAsync('MxEditE2EEMessage', { handle, chatJid, messageId, newText });
177
- },
159
+ async function sendE2EEMessage(handle, chatJid, text, replyToId, replyToSenderJid) {
160
+ const opts = replyToId ? { replyToId, replyToSenderJid } : undefined;
161
+ return getEntry(handle).client.sendE2EEMessage(chatJid, text, opts);
162
+ }
178
163
 
179
- unsendE2EEMessage(handle, chatJid, messageId) {
180
- return callAsync('MxUnsendE2EEMessage', { handle, chatJid, messageId });
181
- },
164
+ async function sendE2EEReaction(handle, chatJid, messageId, senderJid, emoji) {
165
+ return getEntry(handle).client.sendE2EEReaction(chatJid, messageId, senderJid, emoji);
166
+ }
182
167
 
183
- sendE2EEImage(handle, options) {
184
- return callAsync('MxSendE2EEImage', { handle, options });
185
- },
168
+ async function sendE2EETyping(handle, chatJid, isTyping) {
169
+ return getEntry(handle).client.sendE2EETyping(chatJid, isTyping);
170
+ }
186
171
 
187
- sendE2EEVideo(handle, options) {
188
- return callAsync('MxSendE2EEVideo', { handle, options });
189
- },
172
+ async function editE2EEMessage(handle, chatJid, messageId, newText) {
173
+ return getEntry(handle).client.editE2EEMessage(chatJid, messageId, newText);
174
+ }
190
175
 
191
- sendE2EEAudio(handle, options) {
192
- return callAsync('MxSendE2EEAudio', { handle, options });
193
- },
176
+ async function unsendE2EEMessage(handle, chatJid, messageId) {
177
+ return getEntry(handle).client.unsendE2EEMessage(chatJid, messageId);
178
+ }
194
179
 
195
- sendE2EEDocument(handle, options) {
196
- return callAsync('MxSendE2EEDocument', { handle, options });
197
- },
180
+ async function sendE2EEImage(handle, options) {
181
+ const { chatJid, data, mimeType, ...rest } = options;
182
+ return getEntry(handle).client.sendE2EEImage(chatJid, toBuffer(data), mimeType, rest);
183
+ }
198
184
 
199
- sendE2EESticker(handle, options) {
200
- return callAsync('MxSendE2EESticker', { handle, options });
201
- },
185
+ async function sendE2EEVideo(handle, options) {
186
+ const { chatJid, data, mimeType, ...rest } = options;
187
+ return getEntry(handle).client.sendE2EEVideo(chatJid, toBuffer(data), mimeType, rest);
188
+ }
202
189
 
203
- downloadE2EEMedia(handle, options) {
204
- return callAsync('MxDownloadE2EEMedia', { handle, options });
205
- },
190
+ async function sendE2EEAudio(handle, options) {
191
+ const { chatJid, data, mimeType, ...rest } = options;
192
+ return getEntry(handle).client.sendE2EEAudio(chatJid, toBuffer(data), mimeType, rest);
193
+ }
206
194
 
207
- getCookies(handle) {
208
- return call('MxGetCookies', { handle });
209
- },
195
+ async function sendE2EEDocument(handle, options) {
196
+ const { chatJid, data, filename, mimeType, ...rest } = options;
197
+ return getEntry(handle).client.sendE2EEDocument(chatJid, toBuffer(data), filename, mimeType, rest);
198
+ }
210
199
 
211
- uploadMedia(handle, options) {
212
- return callAsync('MxUploadMedia', { handle, options });
213
- },
200
+ async function sendE2EESticker(handle, options) {
201
+ const { chatJid, data, mimeType, ...rest } = options;
202
+ return getEntry(handle).client.sendE2EESticker(chatJid, toBuffer(data), mimeType, rest);
203
+ }
214
204
 
215
- sendImage(handle, options) {
216
- return callAsync('MxSendImage', { handle, options });
217
- },
205
+ async function downloadE2EEMedia(handle, options) {
206
+ return getEntry(handle).client.downloadE2EEMedia(options);
207
+ }
218
208
 
219
- sendVideo(handle, options) {
220
- return callAsync('MxSendVideo', { handle, options });
221
- },
209
+ // ── Regular (non-E2EE) Messenger methods ──────────────────────────────────────
222
210
 
223
- sendVoice(handle, options) {
224
- return callAsync('MxSendVoice', { handle, options });
225
- },
211
+ async function uploadMedia(handle, options) {
212
+ const { chatJid, data, filename, mimeType, isVoice } = options;
213
+ return getEntry(handle).client.uploadMedia(
214
+ toBigInt(chatJid), toBuffer(data), filename || 'file', mimeType || 'application/octet-stream', !!isVoice
215
+ );
216
+ }
226
217
 
227
- sendFile(handle, options) {
228
- return callAsync('MxSendFile', { handle, options });
229
- },
218
+ async function sendImage(handle, options) {
219
+ const { chatJid, data, filename, caption, replyToId } = options;
220
+ const opts = {};
221
+ if (caption) opts.caption = caption;
222
+ if (replyToId) opts.replyToId = replyToId;
223
+ return getEntry(handle).client.sendImage(
224
+ toBigInt(chatJid), toBuffer(data), filename || 'image.jpg', Object.keys(opts).length ? opts : undefined
225
+ );
226
+ }
230
227
 
231
- sendSticker(handle, options) {
232
- return callAsync('MxSendSticker', { handle, options });
233
- },
228
+ async function sendVideo(handle, options) {
229
+ const { chatJid, data, filename, caption, replyToId } = options;
230
+ const opts = {};
231
+ if (caption) opts.caption = caption;
232
+ if (replyToId) opts.replyToId = replyToId;
233
+ return getEntry(handle).client.sendVideo(
234
+ toBigInt(chatJid), toBuffer(data), filename || 'video.mp4', Object.keys(opts).length ? opts : undefined
235
+ );
236
+ }
234
237
 
235
- getUserInfo(handle, userId) {
236
- return callAsync('MxGetUserInfo', { handle, options: { userId } });
237
- },
238
+ async function sendVoice(handle, options) {
239
+ const { chatJid, data, filename, replyToId } = options;
240
+ const opts = replyToId ? { replyToId } : undefined;
241
+ return getEntry(handle).client.sendVoice(
242
+ toBigInt(chatJid), toBuffer(data), filename || 'audio.ogg', opts
243
+ );
244
+ }
238
245
 
239
- createThread(handle, userId) {
240
- return callAsync('MxCreateThread', { handle, options: { userId } });
241
- },
246
+ async function sendFile(handle, options) {
247
+ const { chatJid, data, filename, mimeType, caption, replyToId } = options;
248
+ const opts = {};
249
+ if (caption) opts.caption = caption;
250
+ if (replyToId) opts.replyToId = replyToId;
251
+ return getEntry(handle).client.sendFile(
252
+ toBigInt(chatJid), toBuffer(data), filename || 'file', mimeType || 'application/octet-stream',
253
+ Object.keys(opts).length ? opts : undefined
254
+ );
255
+ }
242
256
 
243
- searchUsers(handle, query) {
244
- return callAsync('MxSearchUsers', { handle, options: { query } });
245
- },
257
+ async function sendSticker(handle, options) {
258
+ const { chatJid, stickerId, replyToId } = options;
259
+ const opts = replyToId ? { replyToId } : undefined;
260
+ return getEntry(handle).client.sendSticker(
261
+ toBigInt(chatJid), toBigInt(stickerId), opts
262
+ );
263
+ }
246
264
 
247
- renameThread(handle, threadId, newName) {
248
- return callAsync('MxRenameThread', { handle, options: { threadId, newName } });
249
- },
265
+ async function getUserInfo(handle, userId) {
266
+ return getEntry(handle).client.getUserInfo(toBigInt(userId));
267
+ }
250
268
 
251
- muteThread(handle, threadId, muteSeconds) {
252
- return callAsync('MxMuteThread', { handle, options: { threadId, muteSeconds } });
253
- },
269
+ async function createThread(handle, userId) {
270
+ return getEntry(handle).client.createThread(toBigInt(userId));
271
+ }
254
272
 
255
- deleteThread(handle, threadId) {
256
- return callAsync('MxDeleteThread', { handle, options: { threadId } });
257
- },
273
+ async function searchUsers(handle, query) {
274
+ return getEntry(handle).client.searchUsers(query);
275
+ }
258
276
 
259
- setGroupPhoto(handle, threadId, data, mimeType) {
260
- return callAsync('MxSetGroupPhoto', { handle, threadId, data, mimeType });
261
- },
277
+ async function markRead(handle, chatJid, watermarkTs) {
278
+ return getEntry(handle).client.markAsRead(toBigInt(chatJid), watermarkTs);
279
+ }
262
280
 
263
- registerPushNotifications(handle, options) {
264
- return callAsync('MxRegisterPushNotifications', { handle, options });
265
- },
281
+ function getCookies(handle) {
282
+ return { cookies: {} };
283
+ }
266
284
 
267
- unload() {
268
- if (lib) {
269
- try { lib.unload(); } catch (_) {}
270
- lib = null;
271
- fns = null;
272
- initialized = false;
273
- }
285
+ function unload() {
286
+ for (const entry of registry.values()) {
287
+ try { entry.client.disconnect(); } catch (_) {}
274
288
  }
289
+ registry.clear();
290
+ _Client = null;
291
+ _loadError = null;
292
+ }
293
+
294
+ module.exports = {
295
+ isAvailable() {
296
+ try { loadClient(); return true; } catch (_) { return false; }
297
+ },
298
+ newClient,
299
+ connect,
300
+ connectE2EE,
301
+ disconnect,
302
+ isConnected,
303
+ getDeviceData,
304
+ pollEvents,
305
+ // E2EE operations
306
+ sendE2EEMessage,
307
+ sendE2EEReaction,
308
+ sendE2EETyping,
309
+ editE2EEMessage,
310
+ unsendE2EEMessage,
311
+ sendE2EEImage,
312
+ sendE2EEVideo,
313
+ sendE2EEAudio,
314
+ sendE2EEDocument,
315
+ sendE2EESticker,
316
+ downloadE2EEMedia,
317
+ // Regular Messenger operations
318
+ uploadMedia,
319
+ sendImage,
320
+ sendVideo,
321
+ sendVoice,
322
+ sendFile,
323
+ sendSticker,
324
+ getUserInfo,
325
+ createThread,
326
+ searchUsers,
327
+ markRead,
328
+ getCookies,
329
+ unload,
275
330
  };
package/src/e2ee/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  const path = require('path');
4
- const fs = require('fs');
5
4
  const bridge = require('./bridge');
6
5
 
7
6
  const DEFAULT_DEVICE_PATH = path.join(process.cwd(), '.nkxfca_e2ee_device.json');
@@ -19,20 +18,10 @@ function extractCookies(jar) {
19
18
  return cookies;
20
19
  }
21
20
 
22
- function detectPlatform(cookies) {
23
- if (cookies.c_user || cookies.xs) return 'facebook';
24
- return 'facebook';
25
- }
26
-
27
- function loadSavedDeviceData(devicePath) {
28
- try {
29
- if (fs.existsSync(devicePath)) {
30
- return fs.readFileSync(devicePath, 'utf8');
31
- }
32
- } catch (_) {}
33
- return null;
34
- }
35
-
21
+ /**
22
+ * Return the bridge handle for this context, creating one on first call.
23
+ * meta-messenger.js handles device data persistence automatically via devicePath.
24
+ */
36
25
  async function getOrCreateHandle(ctx, options) {
37
26
  if (ctx._e2eeBridgeHandle != null) return ctx._e2eeBridgeHandle;
38
27
 
@@ -41,7 +30,7 @@ async function getOrCreateHandle(ctx, options) {
41
30
 
42
31
  const cfg = {
43
32
  cookies,
44
- platform: detectPlatform(cookies),
33
+ platform: 'facebook',
45
34
  logLevel: 'warn',
46
35
  devicePath,
47
36
  ...(options || {}),
@@ -8,6 +8,7 @@ const FormData = require("form-data");
8
8
  const { getHeaders } = require("./headers");
9
9
  const { getType } = require("./constants");
10
10
  const { globalRateLimiter } = require("./rateLimiter");
11
+ const { globalAntiSuspension } = require("./antiSuspension");
11
12
 
12
13
  const jar = new CookieJar();
13
14
  const client = wrapper(axios.create({ jar }));
@@ -112,6 +113,8 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
112
113
  typeof body === 'object' && body !== null && body.error === 1357001;
113
114
 
114
115
  if (isLoginBlocked) {
116
+ // Trip the circuit breaker to stop retries while the account is flagged.
117
+ globalAntiSuspension.tripCircuitBreaker('login_blocked (1357001)', 45 * 60 * 1000);
115
118
  const err = new Error('Facebook blocked the login.');
116
119
  err.error = 'login_blocked';
117
120
  err.res = body;
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { makeParsable, log, warn } = require("./constants");
4
4
  const { globalRateLimiter, configureRateLimiter } = require("./rateLimiter");
5
+ const { globalAntiSuspension } = require("./antiSuspension");
5
6
 
6
7
  /**
7
8
  * Formats a cookie array into a string for use in a cookie jar.
@@ -151,10 +152,17 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
151
152
  err.errorType = res.error === 1357004 ? "CHECKPOINT" : res.error === 1357031 ? "LOCKED" : "BLOCKED";
152
153
  err.requiresReLogin = res.error === 1357004 || res.error === 1357031;
153
154
  warn("Account Status", `${ACCOUNT_ERROR_CODES[res.error]} (Code: ${res.error})`);
155
+ // Trip the anti-suspension circuit breaker immediately for blocking errors.
156
+ // This prevents the bot from hammering Facebook while the account is flagged.
157
+ if (res.error === 1357001 || res.error === 1357033 || res.error === 2056003) {
158
+ const cooldownMs = res.error === 1357001 ? 45 * 60 * 1000 : 20 * 60 * 1000;
159
+ globalAntiSuspension.tripCircuitBreaker(ACCOUNT_ERROR_CODES[res.error], cooldownMs);
160
+ }
154
161
  throw err;
155
162
  }
156
163
 
157
164
  if (res.error === 1357001 || (res.errorSummary && res.errorSummary.includes("blocked"))) {
165
+ globalAntiSuspension.tripCircuitBreaker("Facebook blocked the login (errorSummary)", 45 * 60 * 1000);
158
166
  const err = new Error("Facebook blocked the login");
159
167
  err.error = "Not logged in.";
160
168
  err.errorType = "BLOCKED";
@@ -1196,6 +1196,13 @@ function formatID(id) {
1196
1196
  err.error = "Not logged in.";
1197
1197
  err.requiresReLogin = true;
1198
1198
  err.loginBlocked = res.error === 1357004;
1199
+ // Trip circuit breaker on blocking/locking to stop retries.
1200
+ if (res.error === 1357001) {
1201
+ try {
1202
+ const { globalAntiSuspension } = require('./antiSuspension');
1203
+ globalAntiSuspension.tripCircuitBreaker('login_blocked (1357001 via formatters)', 45 * 60 * 1000);
1204
+ } catch (_) {}
1205
+ }
1199
1206
  throw err;
1200
1207
  }
1201
1208