@lazyneoaz/testfca 1.0.3 → 1.0.5

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.3",
3
+ "version": "1.0.5",
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",
@@ -87,12 +87,11 @@
87
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": {
Binary file
@@ -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();
@@ -592,130 +592,134 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
592
592
  }
593
593
  while (ctx._e2eePollingActive) {
594
594
  try {
595
- // Yield to event loop before blocking poll (same pattern as yumi-team)
595
+ // Yield to event loop before blocking poll
596
596
  await new Promise(resolve => setImmediate(resolve));
597
597
  if (!ctx._e2eePollingActive) break;
598
598
 
599
- // Poll with 1 s timeoutreturns a typed event object, NOT an array
600
- const ev = await api.e2ee.pollEvents(1000);
599
+ // pollEvents returns { events: [...] } iterate the array
600
+ const result = await api.e2ee.pollEvents(1000);
601
601
  if (!ctx._e2eePollingActive) break;
602
602
 
603
- // No event / empty result
604
- if (!ev) continue;
603
+ // No events returned (timeout or empty)
604
+ if (!result || !Array.isArray(result.events) || result.events.length === 0) continue;
605
605
 
606
- const evType = (ev.type || '').toLowerCase();
606
+ for (const ev of result.events) {
607
+ if (!ctx._e2eePollingActive) break;
607
608
 
608
- // ── timeout: no event arrived during the poll window ──
609
- if (evType === 'timeout') continue;
609
+ const evType = (ev.type || '').toLowerCase();
610
610
 
611
- // ── closed: Go bridge shut down ──
612
- if (evType === 'closed') {
613
- utils.warn("E2EE", "Bridge closed. Stopping DM poll loop.");
614
- ctx._e2eePollingActive = false;
615
- break;
616
- }
611
+ // ── timeout: no event arrived during the poll window ──
612
+ if (evType === 'timeout') continue;
617
613
 
618
- // ── permanent error (e.g. session invalid) ──
619
- if (evType === 'error') {
620
- const msg = ev.data && ev.data.message ? ev.data.message : JSON.stringify(ev.data);
621
- utils.warn("E2EE", "Bridge error event:", msg);
622
- if (ev.data && ev.data.code === 1) {
614
+ // ── closed: bridge shut down ──
615
+ if (evType === 'closed') {
616
+ utils.warn("E2EE", "Bridge closed. Stopping DM poll loop.");
623
617
  ctx._e2eePollingActive = false;
624
618
  break;
625
619
  }
626
- continue;
627
- }
628
620
 
629
- // ── E2EE connection confirmed by the bridge ──
630
- if (evType === 'e2eeconnected') {
631
- utils.log("E2EE", "E2EE fully connected (bridge event).");
632
- continue;
633
- }
621
+ // ── permanent error (e.g. session invalid) ──
622
+ if (evType === 'error') {
623
+ const msg = ev.data && ev.data.message ? ev.data.message : JSON.stringify(ev.data);
624
+ utils.warn("E2EE", "Bridge error event:", msg);
625
+ if (ev.data && ev.data.code === 1) {
626
+ ctx._e2eePollingActive = false;
627
+ break;
628
+ }
629
+ continue;
630
+ }
634
631
 
635
- // ── Incoming E2EE DM ─────────────────────────────────
636
- if (evType === 'e2eemessage') {
637
- const d = ev.data;
638
- if (!d) continue;
639
- // threadId may be 0 for pure-JID threads — fall back to chatJid
640
- const chatJid = d.chatJid || '';
641
- const senderJid = d.senderJid || '';
642
- const threadID = String(d.threadId || chatJid.replace(/@.*/, '') || '');
643
- const senderID = String(d.senderId || senderJid.replace(/@.*/, '') || '');
644
- if (!threadID || !senderID) {
645
- utils.log("E2EE", "e2eeMessage missing IDs — raw:", JSON.stringify(d).slice(0, 300));
632
+ // ── E2EE connection confirmed by the bridge ──
633
+ if (evType === 'e2eeconnected') {
634
+ utils.log("E2EE", "E2EE fully connected (bridge event).");
646
635
  continue;
647
636
  }
648
- if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
649
-
650
- utils.log("E2EE", "Incoming E2EE DM from", senderID, "→ thread", threadID, "body:", JSON.stringify(d.text || '').slice(0, 80));
651
-
652
- const fmtMsg = {
653
- type: 'message',
654
- senderID: utils.formatID(senderID),
655
- body: d.text || '',
656
- threadID: utils.formatID(threadID),
657
- messageID: d.id || '',
658
- isGroup: false,
659
- attachments: [],
660
- mentions: {},
661
- timestamp: Number(d.timestampMs || Date.now()),
662
- isUnread: true,
663
- };
664
- if (ctx.globalOptions.autoMarkDelivery) {
665
- try { api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID); } catch (_) {}
637
+
638
+ // ── Incoming E2EE DM ─────────────────────────────────
639
+ if (evType === 'e2eemessage') {
640
+ const d = ev.data;
641
+ if (!d) continue;
642
+ // threadId may be 0 for pure-JID threads — fall back to chatJid
643
+ const chatJid = d.chatJid || '';
644
+ const senderJid = d.senderJid || '';
645
+ const threadID = String(d.threadId || chatJid.replace(/@.*/, '') || '');
646
+ const senderID = String(d.senderId || senderJid.replace(/@.*/, '') || '');
647
+ if (!threadID || !senderID) {
648
+ utils.log("E2EE", "e2eeMessage missing IDs — raw:", JSON.stringify(d).slice(0, 300));
649
+ continue;
650
+ }
651
+ if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
652
+
653
+ utils.log("E2EE", "Incoming E2EE DM from", senderID, "→ thread", threadID, "body:", JSON.stringify(d.text || '').slice(0, 80));
654
+
655
+ const fmtMsg = {
656
+ type: 'message',
657
+ senderID: utils.formatID(senderID),
658
+ body: d.text || '',
659
+ threadID: utils.formatID(threadID),
660
+ messageID: d.id || '',
661
+ isGroup: false,
662
+ attachments: [],
663
+ mentions: {},
664
+ timestamp: Number(d.timestampMs || Date.now()),
665
+ isUnread: true,
666
+ };
667
+ if (ctx.globalOptions.autoMarkDelivery) {
668
+ try { api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID); } catch (_) {}
669
+ }
670
+ if (ctx.globalOptions.autoMarkRead) {
671
+ try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
672
+ }
673
+ globalCallback(null, fmtMsg);
674
+ continue;
666
675
  }
667
- if (ctx.globalOptions.autoMarkRead) {
668
- try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
676
+
677
+ // ── Incoming regular (non-E2EE) message via bridge ───
678
+ if (evType === 'message') {
679
+ const d = ev.data;
680
+ if (!d) continue;
681
+ const threadID = String(d.threadId || '');
682
+ const senderID = String(d.senderId || '');
683
+ if (!threadID || !senderID) continue;
684
+ if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
685
+ globalCallback(null, {
686
+ type: 'message',
687
+ senderID: utils.formatID(senderID),
688
+ body: d.text || '',
689
+ threadID: utils.formatID(threadID),
690
+ messageID: d.id || '',
691
+ isGroup: !!(d.isGroup),
692
+ attachments: [],
693
+ mentions: {},
694
+ timestamp: Number(d.timestampMs || Date.now()),
695
+ isUnread: true,
696
+ });
697
+ continue;
669
698
  }
670
- globalCallback(null, fmtMsg);
671
- continue;
672
- }
673
699
 
674
- // ── Incoming regular (non-E2EE) message via bridge ───
675
- if (evType === 'message') {
676
- const d = ev.data;
677
- if (!d) continue;
678
- const threadID = String(d.threadId || '');
679
- const senderID = String(d.senderId || '');
680
- if (!threadID || !senderID) continue;
681
- if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
682
- globalCallback(null, {
683
- type: 'message',
684
- senderID: utils.formatID(senderID),
685
- body: d.text || '',
686
- threadID: utils.formatID(threadID),
687
- messageID: d.id || '',
688
- isGroup: !!(d.isGroup),
689
- attachments: [],
690
- mentions: {},
691
- timestamp: Number(d.timestampMs || Date.now()),
692
- isUnread: true,
693
- });
694
- continue;
695
- }
700
+ // ── E2EE reaction ────────────────────────────────────
701
+ if (evType === 'e2eereaction') {
702
+ if (!ctx.globalOptions.listenEvents) continue;
703
+ const d = ev.data;
704
+ if (!d) continue;
705
+ const chatJid = d.chatJid || '';
706
+ const senderJid = d.senderJid || '';
707
+ globalCallback(null, {
708
+ type: 'message_reaction',
709
+ threadID: utils.formatID(chatJid.replace(/@.*/, '')),
710
+ messageID: d.messageId || '',
711
+ reaction: d.reaction || '',
712
+ senderID: utils.formatID(senderJid.replace(/@.*/, '')),
713
+ offlineThreadingId: null,
714
+ timestamp: Date.now(),
715
+ isGroup: false,
716
+ });
717
+ continue;
718
+ }
696
719
 
697
- // ── E2EE reaction ────────────────────────────────────
698
- if (evType === 'e2eereaction') {
699
- if (!ctx.globalOptions.listenEvents) continue;
700
- const d = ev.data;
701
- if (!d) continue;
702
- const chatJid = d.chatJid || '';
703
- const senderJid = d.senderJid || '';
704
- globalCallback(null, {
705
- type: 'message_reaction',
706
- threadID: utils.formatID(chatJid.replace(/@.*/, '')),
707
- messageID: d.messageId || '',
708
- reaction: d.reaction || '',
709
- senderID: utils.formatID(senderJid.replace(/@.*/, '')),
710
- offlineThreadingId: null,
711
- timestamp: Date.now(),
712
- isGroup: false,
713
- });
714
- continue;
720
+ // ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
715
721
  }
716
722
 
717
- // ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
718
-
719
723
  } catch (pollErr) {
720
724
  if (!ctx._e2eePollingActive) break;
721
725
  utils.warn("E2EE", "pollEvents error:", pollErr && pollErr.message ? pollErr.message : pollErr);
@@ -1,275 +1,404 @@
1
1
  "use strict";
2
2
 
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
+ * Railway, etc.). We also patch at runtime (ensureTlsFixedBinary) so the fix
9
+ * applies even when postinstall was skipped (npm ci --ignore-scripts, Railway
10
+ * production installs, etc.).
11
+ *
12
+ * Key design decisions:
13
+ * - enableE2EE: false → we call connectE2EE() explicitly; no auto-start race
14
+ * - e2eeMemoryOnly: false → Signal sessions are persisted to devicePath on disk
15
+ * - devicePath defaults to ".nkxfca_e2ee_device.json" in cwd when not provided
16
+ */
17
+
18
+ const fs = require('fs');
3
19
  const path = require('path');
4
- const fs = require('fs');
5
20
 
6
- let koffi;
7
- let lib;
8
- let fns;
9
- let initialized = false;
21
+ // ── Runtime binary patcher ────────────────────────────────────────────────────
22
+ // Ensures the TLS-fixed binary is in place before meta-messenger.js is loaded.
23
+ // Runs once; safe to call multiple times.
24
+
25
+ let _patchDone = false;
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
+ }
10
37
 
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');
38
+ function _binaryExt() {
39
+ if (process.platform === 'win32') return '.dll';
40
+ if (process.platform === 'darwin') return '.dylib';
41
+ return '.so';
16
42
  }
17
43
 
18
- const LIB_PATH = libPath();
44
+ function ensureTlsFixedBinary() {
45
+ if (_patchDone) return;
46
+ _patchDone = true;
19
47
 
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.`
27
- );
28
- }
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
- }
48
+ if (process.env.NKXFCA_SKIP_POSTINSTALL === 'true') return;
76
49
 
77
- function serialize(obj) {
78
- return JSON.stringify(obj, (_, v) =>
79
- typeof v === 'bigint' ? Number(v) : v
80
- );
81
- }
50
+ const key = _platformKey();
51
+ if (!key) return;
82
52
 
83
- function deserialize(str) {
84
- return JSON.parse(str);
85
- }
53
+ const ext = _binaryExt();
54
+ // bridge.js lives at src/e2ee/bridge.js → prebuilt/ is two levels up
55
+ const ourBin = path.join(__dirname, '..', '..', 'prebuilt', key, 'messagix' + ext);
56
+ if (!fs.existsSync(ourBin)) return;
86
57
 
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
- }
58
+ try {
59
+ const pkgJson = require.resolve('meta-messenger.js/package.json');
60
+ const buildDir = path.join(path.dirname(pkgJson), 'build');
61
+ const mmBin = path.join(buildDir, 'messagix' + ext);
95
62
 
96
- function callAsync(fn, payload) {
97
- return new Promise((resolve, reject) => {
98
- setImmediate(() => {
99
- try { resolve(call(fn, payload)); }
100
- catch (err) { reject(err); }
101
- });
102
- });
103
- }
63
+ fs.mkdirSync(buildDir, { recursive: true });
104
64
 
105
- module.exports = {
106
- isAvailable() {
107
- try { init(); return true; } catch (_) { return false; }
108
- },
65
+ // Only copy if sizes differ (avoids unnecessary writes on every boot)
66
+ let needsCopy = true;
67
+ if (fs.existsSync(mmBin)) {
68
+ needsCopy = fs.statSync(ourBin).size !== fs.statSync(mmBin).size;
69
+ }
109
70
 
110
- newClient(cfg) {
111
- return call('MxNewClient', cfg);
112
- },
71
+ if (needsCopy) {
72
+ fs.copyFileSync(ourBin, mmBin);
73
+ fs.chmodSync(mmBin, 0o755);
74
+ console.log('[nkxfca] runtime patch: applied TLS-fixed binary (' + key + ')');
75
+ }
76
+ } catch (err) {
77
+ // Non-fatal — warn so the user knows why E2EE may not work
78
+ console.warn('[nkxfca] runtime patch failed (' + err.message + '). E2EE TLS errors may occur in sandboxed environments.');
79
+ }
80
+ }
113
81
 
114
- connect(handle) {
115
- return callAsync('MxConnect', { handle });
116
- },
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+
84
+ let _Client = null;
85
+ let _loadError = null;
86
+
87
+ function loadClient() {
88
+ if (_Client) return _Client;
89
+ if (_loadError) throw _loadError;
90
+
91
+ // Patch binary BEFORE the first require('meta-messenger.js') so the
92
+ // TLS-fixed .so/.dylib/.dll is loaded by Node rather than the original.
93
+ ensureTlsFixedBinary();
94
+
95
+ try {
96
+ const mm = require('meta-messenger.js');
97
+ _Client = mm.Client || mm.default?.Client;
98
+ if (!_Client) throw new Error('meta-messenger.js: Client class not found in exports');
99
+ return _Client;
100
+ } catch (err) {
101
+ _loadError = new Error(
102
+ 'E2EE requires meta-messenger.js and its native binary.\n' +
103
+ 'Install with: npm install meta-messenger.js\n' +
104
+ 'Error: ' + err.message
105
+ );
106
+ throw _loadError;
107
+ }
108
+ }
117
109
 
118
- connectE2EE(handle) {
119
- return callAsync('MxConnectE2EE', { handle });
120
- },
110
+ const EVENT_TYPES = [
111
+ 'message', 'messageEdit', 'messageUnsend', 'reaction',
112
+ 'typing', 'readReceipt', 'e2eeMessage', 'e2eeReaction',
113
+ 'e2eeReceipt', 'e2eeConnected', 'ready', 'reconnected',
114
+ 'disconnected', 'error', 'deviceDataChanged', 'raw',
115
+ ];
116
+
117
+ // Registry: handle (string) -> { client, eventBuffer, resolveWaiter, lastDeviceData }
118
+ const registry = new Map();
119
+ let seq = 1;
120
+
121
+ function newClient(cfg) {
122
+ const Client = loadClient();
123
+ const cookies = cfg.cookies || {};
124
+
125
+ const opts = {
126
+ platform: cfg.platform || 'facebook',
127
+ logLevel: cfg.logLevel || 'warn',
128
+ enableE2EE: false, // we call connectE2EE() explicitly
129
+ e2eeMemoryOnly: false, // always persist sessions to disk
130
+ autoReconnect: cfg.autoReconnect !== false,
131
+ };
121
132
 
122
- disconnect(handle) {
123
- try { return call('MxDisconnect', { handle }); } catch (_) {}
124
- },
133
+ // Device storage: prefer deviceData (in-memory JSON) > devicePath > default file
134
+ if (cfg.deviceData) {
135
+ opts.deviceData = cfg.deviceData;
136
+ } else {
137
+ opts.devicePath = cfg.devicePath || '.nkxfca_e2ee_device.json';
138
+ }
125
139
 
126
- isConnected(handle) {
127
- try { return call('MxIsConnected', { handle }); }
128
- catch (_) { return { connected: false, e2eeConnected: false }; }
129
- },
140
+ const client = new Client(cookies, opts);
141
+ const handle = String(seq++);
142
+ const entry = { client, eventBuffer: [], resolveWaiter: null, lastDeviceData: null };
143
+
144
+ for (const type of EVENT_TYPES) {
145
+ client.on(type, (data) => {
146
+ if (type === 'deviceDataChanged' && data?.deviceData) {
147
+ entry.lastDeviceData = data.deviceData;
148
+ }
149
+ entry.eventBuffer.push({ type, data: data ?? null, timestamp: Date.now() });
150
+ if (entry.resolveWaiter) {
151
+ const resolve = entry.resolveWaiter;
152
+ entry.resolveWaiter = null;
153
+ resolve();
154
+ }
155
+ });
156
+ }
130
157
 
131
- getDeviceData(handle) {
132
- return call('MxGetDeviceData', { handle });
133
- },
158
+ registry.set(handle, entry);
159
+ return { handle };
160
+ }
134
161
 
135
- pollEvents(handle, timeoutMs) {
136
- return callAsync('MxPollEvents', { handle, timeoutMs: timeoutMs || 5000 });
137
- },
162
+ function getEntry(handle) {
163
+ const entry = registry.get(String(handle));
164
+ if (!entry) throw new Error(`E2EE bridge: unknown handle "${handle}"`);
165
+ return entry;
166
+ }
138
167
 
139
- sendMessage(handle, options) {
140
- return callAsync('MxSendMessage', { handle, options });
141
- },
168
+ async function connect(handle) {
169
+ return getEntry(handle).client.connect();
170
+ }
142
171
 
143
- sendReaction(handle, threadId, messageId, emoji) {
144
- return callAsync('MxSendReaction', { handle, threadId, messageId, emoji });
145
- },
172
+ /**
173
+ * Initiate E2EE handshake. Returns after the background connect is kicked
174
+ * off — listen for the 'e2eeConnected' event (via pollEvents) to know when
175
+ * the channel is fully ready.
176
+ */
177
+ async function connectE2EE(handle) {
178
+ return getEntry(handle).client.connectE2EE();
179
+ }
146
180
 
147
- editMessage(handle, messageId, newText) {
148
- return callAsync('MxEditMessage', { handle, messageId, newText });
149
- },
181
+ function disconnect(handle) {
182
+ const entry = registry.get(String(handle));
183
+ if (!entry) return;
184
+ try { entry.client.disconnect(); } catch (_) {}
185
+ registry.delete(String(handle));
186
+ }
150
187
 
151
- unsendMessage(handle, messageId) {
152
- return callAsync('MxUnsendMessage', { handle, messageId });
153
- },
188
+ function isConnected(handle) {
189
+ try {
190
+ const { client } = getEntry(handle);
191
+ return { connected: !!client.isConnected, e2eeConnected: !!client.isE2EEConnected };
192
+ } catch (_) {
193
+ return { connected: false, e2eeConnected: false };
194
+ }
195
+ }
154
196
 
155
- sendTyping(handle, threadId, isTyping, isGroup, threadType) {
156
- return callAsync('MxSendTyping', { handle, threadId, isTyping, isGroup, threadType });
157
- },
197
+ function getDeviceData(handle) {
198
+ return { deviceData: getEntry(handle).lastDeviceData };
199
+ }
158
200
 
159
- markRead(handle, threadId, watermarkTs) {
160
- return callAsync('MxMarkRead', { handle, threadId, watermarkTs: watermarkTs || 0 });
161
- },
201
+ async function pollEvents(handle, timeoutMs = 5000) {
202
+ const entry = getEntry(handle);
203
+ if (entry.eventBuffer.length > 0) {
204
+ return { events: entry.eventBuffer.splice(0) };
205
+ }
206
+ await new Promise((resolve) => {
207
+ const timer = setTimeout(() => {
208
+ entry.resolveWaiter = null;
209
+ resolve();
210
+ }, timeoutMs);
211
+ entry.resolveWaiter = () => {
212
+ clearTimeout(timer);
213
+ resolve();
214
+ };
215
+ });
216
+ return { events: entry.eventBuffer.splice(0) };
217
+ }
162
218
 
163
- sendE2EEMessage(handle, chatJid, text, replyToId, replyToSenderJid) {
164
- return callAsync('MxSendE2EEMessage', { handle, chatJid, text, replyToId, replyToSenderJid });
165
- },
219
+ function toBuffer(data) {
220
+ if (Buffer.isBuffer(data)) return data;
221
+ if (typeof data === 'string') return Buffer.from(data, 'base64');
222
+ return Buffer.from(data);
223
+ }
166
224
 
167
- sendE2EEReaction(handle, chatJid, messageId, senderJid, emoji) {
168
- return callAsync('MxSendE2EEReaction', { handle, chatJid, messageId, senderJid, emoji });
169
- },
225
+ // Convert JID string ("12345@msgr") or plain number/string to BigInt
226
+ function toBigInt(val) {
227
+ if (typeof val === 'bigint') return val;
228
+ return BigInt(String(val).split('@')[0]);
229
+ }
170
230
 
171
- sendE2EETyping(handle, chatJid, isTyping) {
172
- return callAsync('MxSendE2EETyping', { handle, chatJid, isTyping: !!isTyping });
173
- },
231
+ // ── E2EE operations ────────────────────────────────────────────────────────────
174
232
 
175
- editE2EEMessage(handle, chatJid, messageId, newText) {
176
- return callAsync('MxEditE2EEMessage', { handle, chatJid, messageId, newText });
177
- },
233
+ async function sendE2EEMessage(handle, chatJid, text, replyToId, replyToSenderJid) {
234
+ const opts = replyToId ? { replyToId, replyToSenderJid } : undefined;
235
+ return getEntry(handle).client.sendE2EEMessage(chatJid, text, opts);
236
+ }
178
237
 
179
- unsendE2EEMessage(handle, chatJid, messageId) {
180
- return callAsync('MxUnsendE2EEMessage', { handle, chatJid, messageId });
181
- },
238
+ async function sendE2EEReaction(handle, chatJid, messageId, senderJid, emoji) {
239
+ return getEntry(handle).client.sendE2EEReaction(chatJid, messageId, senderJid, emoji);
240
+ }
182
241
 
183
- sendE2EEImage(handle, options) {
184
- return callAsync('MxSendE2EEImage', { handle, options });
185
- },
242
+ async function sendE2EETyping(handle, chatJid, isTyping) {
243
+ return getEntry(handle).client.sendE2EETyping(chatJid, isTyping);
244
+ }
186
245
 
187
- sendE2EEVideo(handle, options) {
188
- return callAsync('MxSendE2EEVideo', { handle, options });
189
- },
246
+ async function editE2EEMessage(handle, chatJid, messageId, newText) {
247
+ return getEntry(handle).client.editE2EEMessage(chatJid, messageId, newText);
248
+ }
190
249
 
191
- sendE2EEAudio(handle, options) {
192
- return callAsync('MxSendE2EEAudio', { handle, options });
193
- },
250
+ async function unsendE2EEMessage(handle, chatJid, messageId) {
251
+ return getEntry(handle).client.unsendE2EEMessage(chatJid, messageId);
252
+ }
194
253
 
195
- sendE2EEDocument(handle, options) {
196
- return callAsync('MxSendE2EEDocument', { handle, options });
197
- },
254
+ async function sendE2EEImage(handle, options) {
255
+ const { chatJid, data, mimeType, ...rest } = options;
256
+ return getEntry(handle).client.sendE2EEImage(chatJid, toBuffer(data), mimeType, rest);
257
+ }
198
258
 
199
- sendE2EESticker(handle, options) {
200
- return callAsync('MxSendE2EESticker', { handle, options });
201
- },
259
+ async function sendE2EEVideo(handle, options) {
260
+ const { chatJid, data, mimeType, ...rest } = options;
261
+ return getEntry(handle).client.sendE2EEVideo(chatJid, toBuffer(data), mimeType, rest);
262
+ }
202
263
 
203
- downloadE2EEMedia(handle, options) {
204
- return callAsync('MxDownloadE2EEMedia', { handle, options });
205
- },
264
+ async function sendE2EEAudio(handle, options) {
265
+ const { chatJid, data, mimeType, ...rest } = options;
266
+ return getEntry(handle).client.sendE2EEAudio(chatJid, toBuffer(data), mimeType, rest);
267
+ }
206
268
 
207
- getCookies(handle) {
208
- return call('MxGetCookies', { handle });
209
- },
269
+ async function sendE2EEDocument(handle, options) {
270
+ const { chatJid, data, filename, mimeType, ...rest } = options;
271
+ return getEntry(handle).client.sendE2EEDocument(chatJid, toBuffer(data), filename, mimeType, rest);
272
+ }
210
273
 
211
- uploadMedia(handle, options) {
212
- return callAsync('MxUploadMedia', { handle, options });
213
- },
274
+ async function sendE2EESticker(handle, options) {
275
+ const { chatJid, data, mimeType, ...rest } = options;
276
+ return getEntry(handle).client.sendE2EESticker(chatJid, toBuffer(data), mimeType, rest);
277
+ }
214
278
 
215
- sendImage(handle, options) {
216
- return callAsync('MxSendImage', { handle, options });
217
- },
279
+ async function downloadE2EEMedia(handle, options) {
280
+ return getEntry(handle).client.downloadE2EEMedia(options);
281
+ }
218
282
 
219
- sendVideo(handle, options) {
220
- return callAsync('MxSendVideo', { handle, options });
221
- },
283
+ // ── Regular (non-E2EE) Messenger methods ──────────────────────────────────────
222
284
 
223
- sendVoice(handle, options) {
224
- return callAsync('MxSendVoice', { handle, options });
225
- },
285
+ async function uploadMedia(handle, options) {
286
+ const { chatJid, data, filename, mimeType, isVoice } = options;
287
+ return getEntry(handle).client.uploadMedia(
288
+ toBigInt(chatJid), toBuffer(data), filename || 'file', mimeType || 'application/octet-stream', !!isVoice
289
+ );
290
+ }
226
291
 
227
- sendFile(handle, options) {
228
- return callAsync('MxSendFile', { handle, options });
229
- },
292
+ async function sendImage(handle, options) {
293
+ const { chatJid, data, filename, caption, replyToId } = options;
294
+ const opts = {};
295
+ if (caption) opts.caption = caption;
296
+ if (replyToId) opts.replyToId = replyToId;
297
+ return getEntry(handle).client.sendImage(
298
+ toBigInt(chatJid), toBuffer(data), filename || 'image.jpg', Object.keys(opts).length ? opts : undefined
299
+ );
300
+ }
230
301
 
231
- sendSticker(handle, options) {
232
- return callAsync('MxSendSticker', { handle, options });
233
- },
302
+ async function sendVideo(handle, options) {
303
+ const { chatJid, data, filename, caption, replyToId } = options;
304
+ const opts = {};
305
+ if (caption) opts.caption = caption;
306
+ if (replyToId) opts.replyToId = replyToId;
307
+ return getEntry(handle).client.sendVideo(
308
+ toBigInt(chatJid), toBuffer(data), filename || 'video.mp4', Object.keys(opts).length ? opts : undefined
309
+ );
310
+ }
234
311
 
235
- getUserInfo(handle, userId) {
236
- return callAsync('MxGetUserInfo', { handle, options: { userId } });
237
- },
312
+ async function sendVoice(handle, options) {
313
+ const { chatJid, data, filename, replyToId } = options;
314
+ const opts = replyToId ? { replyToId } : undefined;
315
+ return getEntry(handle).client.sendVoice(
316
+ toBigInt(chatJid), toBuffer(data), filename || 'audio.ogg', opts
317
+ );
318
+ }
238
319
 
239
- createThread(handle, userId) {
240
- return callAsync('MxCreateThread', { handle, options: { userId } });
241
- },
320
+ async function sendFile(handle, options) {
321
+ const { chatJid, data, filename, mimeType, caption, replyToId } = options;
322
+ const opts = {};
323
+ if (caption) opts.caption = caption;
324
+ if (replyToId) opts.replyToId = replyToId;
325
+ return getEntry(handle).client.sendFile(
326
+ toBigInt(chatJid), toBuffer(data), filename || 'file', mimeType || 'application/octet-stream',
327
+ Object.keys(opts).length ? opts : undefined
328
+ );
329
+ }
242
330
 
243
- searchUsers(handle, query) {
244
- return callAsync('MxSearchUsers', { handle, options: { query } });
245
- },
331
+ async function sendSticker(handle, options) {
332
+ const { chatJid, stickerId, replyToId } = options;
333
+ const opts = replyToId ? { replyToId } : undefined;
334
+ return getEntry(handle).client.sendSticker(
335
+ toBigInt(chatJid), toBigInt(stickerId), opts
336
+ );
337
+ }
246
338
 
247
- renameThread(handle, threadId, newName) {
248
- return callAsync('MxRenameThread', { handle, options: { threadId, newName } });
249
- },
339
+ async function getUserInfo(handle, userId) {
340
+ return getEntry(handle).client.getUserInfo(toBigInt(userId));
341
+ }
250
342
 
251
- muteThread(handle, threadId, muteSeconds) {
252
- return callAsync('MxMuteThread', { handle, options: { threadId, muteSeconds } });
253
- },
343
+ async function createThread(handle, userId) {
344
+ return getEntry(handle).client.createThread(toBigInt(userId));
345
+ }
254
346
 
255
- deleteThread(handle, threadId) {
256
- return callAsync('MxDeleteThread', { handle, options: { threadId } });
257
- },
347
+ async function searchUsers(handle, query) {
348
+ return getEntry(handle).client.searchUsers(query);
349
+ }
258
350
 
259
- setGroupPhoto(handle, threadId, data, mimeType) {
260
- return callAsync('MxSetGroupPhoto', { handle, threadId, data, mimeType });
261
- },
351
+ async function markRead(handle, chatJid, watermarkTs) {
352
+ return getEntry(handle).client.markAsRead(toBigInt(chatJid), watermarkTs);
353
+ }
262
354
 
263
- registerPushNotifications(handle, options) {
264
- return callAsync('MxRegisterPushNotifications', { handle, options });
265
- },
355
+ function getCookies(handle) {
356
+ return { cookies: {} };
357
+ }
266
358
 
267
- unload() {
268
- if (lib) {
269
- try { lib.unload(); } catch (_) {}
270
- lib = null;
271
- fns = null;
272
- initialized = false;
273
- }
359
+ function unload() {
360
+ for (const entry of registry.values()) {
361
+ try { entry.client.disconnect(); } catch (_) {}
274
362
  }
363
+ registry.clear();
364
+ _Client = null;
365
+ _loadError = null;
366
+ }
367
+
368
+ module.exports = {
369
+ isAvailable() {
370
+ try { loadClient(); return true; } catch (_) { return false; }
371
+ },
372
+ newClient,
373
+ connect,
374
+ connectE2EE,
375
+ disconnect,
376
+ isConnected,
377
+ getDeviceData,
378
+ pollEvents,
379
+ // E2EE operations
380
+ sendE2EEMessage,
381
+ sendE2EEReaction,
382
+ sendE2EETyping,
383
+ editE2EEMessage,
384
+ unsendE2EEMessage,
385
+ sendE2EEImage,
386
+ sendE2EEVideo,
387
+ sendE2EEAudio,
388
+ sendE2EEDocument,
389
+ sendE2EESticker,
390
+ downloadE2EEMedia,
391
+ // Regular Messenger operations
392
+ uploadMedia,
393
+ sendImage,
394
+ sendVideo,
395
+ sendVoice,
396
+ sendFile,
397
+ sendSticker,
398
+ getUserInfo,
399
+ createThread,
400
+ searchUsers,
401
+ markRead,
402
+ getCookies,
403
+ unload,
275
404
  };
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 || {}),