@lazyneoaz/testfca 1.0.0 → 1.0.1

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.0",
3
+ "version": "1.0.1",
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",
@@ -10,21 +10,17 @@ import { packageJson as pkg } from "./package.mjs";
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
 
12
12
  function defaultRepoSlug() {
13
- const repo = pkg.repository;
14
- if (typeof repo === "string") {
15
- const m = repo.match(/github:(.+)/i);
16
- if (m) return m[1];
17
- if (/^[\w-]+\/[\w.-]+$/.test(repo)) return repo;
18
- } else if (repo && typeof repo === "object" && repo.url) {
19
- const m = repo.url.match(/github\.com[:/]+([^#]+?)(?:\.git)?$/i);
20
- if (m) return m[1];
21
- }
22
- return "NeoKEX/nkxfca";
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";
23
19
  }
24
20
 
25
21
  function buildBaseURL() {
26
22
  const repo = defaultRepoSlug();
27
- const versionTag = `v${pkg.version}`;
23
+ const versionTag = latestTag();
28
24
  const baseURL = `https://github.com/${repo}/releases/download/${versionTag}`;
29
25
  return baseURL.replace(/\/$/, "");
30
26
  }
@@ -40,6 +40,65 @@ function formatEventReminders(reminder) {
40
40
  * @returns {Object | null} A formatted thread object or null if data is invalid.
41
41
  * @throws {Error} If Facebook returns a GraphQL error
42
42
  */
43
+ /**
44
+ * Returns a minimal synthetic thread-info object for E2EE DM threads that
45
+ * GraphQL does not expose. Goat Bot needs certain fields to avoid crashing.
46
+ */
47
+ function buildE2EEThreadInfo(threadID, botUserID) {
48
+ const tid = String(threadID);
49
+ const participants = [tid];
50
+ if (botUserID && String(botUserID) !== tid) participants.push(String(botUserID));
51
+ return {
52
+ threadID: tid,
53
+ threadName: "",
54
+ name: "",
55
+ participantIDs: participants,
56
+ userInfo: participants.map(id => ({
57
+ id, name: "", firstName: "", vanity: "", url: "",
58
+ thumbSrc: "", profileUrl: "", gender: 0,
59
+ type: "User", isFriend: false, isBirthday: false,
60
+ })),
61
+ unreadCount: 0,
62
+ messageCount: 0,
63
+ timestamp: String(Date.now()),
64
+ serverTimestamp: String(Date.now()),
65
+ muteUntil: null,
66
+ isGroup: false,
67
+ threadType: 1,
68
+ isSubscribed: true,
69
+ isArchived: false,
70
+ folder: "INBOX",
71
+ cannotReplyReason: null,
72
+ canReply: true,
73
+ eventReminders: null,
74
+ emoji: null,
75
+ color: null,
76
+ threadTheme: null,
77
+ nicknames: {},
78
+ adminIDs: [],
79
+ approvalMode: false,
80
+ approvalQueue: [],
81
+ reactionsMuteMode: "reactions_not_muted",
82
+ mentionsMuteMode: "mentions_not_muted",
83
+ isPinProtected: false,
84
+ relatedPageThread: null,
85
+ snippet: null,
86
+ snippetSender: null,
87
+ snippetAttachments: [],
88
+ imageSrc: null,
89
+ isCanonicalUser: true,
90
+ isCanonical: true,
91
+ recipientsLoadable: true,
92
+ hasEmailParticipant: false,
93
+ readOnly: false,
94
+ lastMessageTimestamp: null,
95
+ lastMessageType: "message",
96
+ lastReadTimestamp: null,
97
+ inviteLink: { enable: false, link: null },
98
+ _isE2EESynthetic: true,
99
+ };
100
+ }
101
+
43
102
  function formatThreadGraphQLResponse(data) {
44
103
  // Check for GraphQL errors and throw with details instead of silently returning null
45
104
  if (data.errors) {
@@ -52,14 +111,15 @@ function formatThreadGraphQLResponse(data) {
52
111
  utils.error("formatThreadGraphQLResponse", error);
53
112
  throw error;
54
113
  }
55
-
114
+
56
115
  const messageThread = data.message_thread;
57
116
  if (!messageThread) {
117
+ // E2EE DM threads are not exposed via GraphQL — signal the caller to use synthetic fallback
58
118
  const error = new Error("No message_thread in GraphQL response - thread may not exist or access may be restricted");
59
119
  Object.assign(error, {
60
- details: "The GraphQL query returned successfully but contained no message_thread data"
120
+ details: "The GraphQL query returned successfully but contained no message_thread data",
121
+ isE2EEThread: true,
61
122
  });
62
- utils.error("formatThreadGraphQLResponse", error);
63
123
  throw error;
64
124
  }
65
125
 
@@ -185,7 +245,7 @@ module.exports = function (defaultFuncs, api, ctx) {
185
245
  if (!ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
186
246
  throw new Error("Invalid thread ID(s)");
187
247
  }
188
-
248
+
189
249
  let form = {};
190
250
  threadIDs.forEach((t, i) => {
191
251
  form["o" + i] = {
@@ -217,7 +277,7 @@ module.exports = function (defaultFuncs, api, ctx) {
217
277
  const threadInfos = {};
218
278
  for (let i = resData.length - 2; i >= 0; i--) {
219
279
  const res = resData[i];
220
-
280
+
221
281
  // Check for error_results and throw instead of silently continuing
222
282
  if (res.error_results) {
223
283
  const error = new Error(`Facebook returned error_results for thread query: ${res.error_results} errors`);
@@ -228,10 +288,10 @@ module.exports = function (defaultFuncs, api, ctx) {
228
288
  utils.error("getThreadInfo", error);
229
289
  throw error;
230
290
  }
231
-
291
+
232
292
  const oKey = Object.keys(res)[0];
233
293
  const responseData = res[oKey];
234
-
294
+
235
295
  // Check for errors in the response object
236
296
  if (responseData.errors || responseData.error_results) {
237
297
  const details = responseData.errors
@@ -246,10 +306,22 @@ module.exports = function (defaultFuncs, api, ctx) {
246
306
  utils.error("getThreadInfo", error);
247
307
  throw error;
248
308
  }
249
-
250
- const threadInfo = formatThreadGraphQLResponse(responseData.data);
309
+
310
+ let threadInfo;
311
+ try {
312
+ threadInfo = formatThreadGraphQLResponse(responseData.data);
313
+ } catch (fmtErr) {
314
+ if (fmtErr.isE2EEThread) {
315
+ // E2EE DM thread — GraphQL has no data for it, use synthetic fallback
316
+ const tid = threadIDs[threadIDs.length - 1 - i] || threadIDs[0];
317
+ utils.log("getThreadInfo", "E2EE DM thread " + tid + " not in GraphQL, using synthetic fallback.");
318
+ threadInfo = buildE2EEThreadInfo(tid, ctx.userID);
319
+ } else {
320
+ throw fmtErr;
321
+ }
322
+ }
251
323
  if (threadInfo) {
252
- threadInfos[threadInfo.threadID || threadID[threadID.length - 1 - i]] = threadInfo;
324
+ threadInfos[threadInfo.threadID || threadIDs[threadIDs.length - 1 - i]] = threadInfo;
253
325
  }
254
326
  }
255
327
 
@@ -49,7 +49,7 @@ function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
49
49
  } catch (error) {
50
50
  lastError = error;
51
51
  if (i === maxRetries - 1) throw lastError;
52
-
52
+
53
53
  const delay = Math.min(baseDelay * Math.pow(2, i) + Math.random() * 500, 10000);
54
54
  await new Promise(resolve => setTimeout(resolve, delay));
55
55
  }
@@ -206,16 +206,16 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
206
206
 
207
207
  if (ctx.globalOptions.proxy) options.wsOptions.agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
208
208
  ctx._mqttLastConnectAttemptAt = Date.now();
209
-
209
+
210
210
  // Create WebSocket stream - using exact fca-unofficial implementation
211
211
  let mqttClient;
212
-
212
+
213
213
  try {
214
214
  const mqtt = require('mqtt');
215
215
  const WebSocket = require('ws');
216
216
  const { PassThrough, Writable } = require('stream');
217
217
  const Duplexify = require('duplexify');
218
-
218
+
219
219
  // Exact buildProxy from fca-unofficial
220
220
  function buildProxy() {
221
221
  let target = null;
@@ -268,7 +268,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
268
268
  };
269
269
  return Proxy;
270
270
  }
271
-
271
+
272
272
  // Exact buildStream from fca-unofficial
273
273
  function buildStream(opts, ws, Proxy) {
274
274
  const readable = new PassThrough();
@@ -280,18 +280,18 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
280
280
  let attached = false;
281
281
  let style = "prop";
282
282
  let closed = false;
283
-
283
+
284
284
  const toBuffer = d => {
285
285
  if (Buffer.isBuffer(d)) return d;
286
286
  if (d instanceof ArrayBuffer) return Buffer.from(d);
287
287
  if (ArrayBuffer.isView(d)) return Buffer.from(d.buffer, d.byteOffset, d.byteLength);
288
288
  return Buffer.from(String(d));
289
289
  };
290
-
290
+
291
291
  const swapToNoopWritable = () => {
292
292
  try { Stream.setWritable(NoopWritable); } catch { }
293
293
  };
294
-
294
+
295
295
  const onOpen = () => {
296
296
  if (closed) return;
297
297
  Proxy.setTarget(ws);
@@ -316,16 +316,16 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
316
316
  }
317
317
  }, 10000);
318
318
  };
319
-
319
+
320
320
  const onMessage = data => {
321
321
  lastActivity = Date.now();
322
322
  readable.write(toBuffer(style === "dom" && data && data.data !== undefined ? data.data : data));
323
323
  };
324
-
324
+
325
325
  const onPong = () => {
326
326
  lastActivity = Date.now();
327
327
  };
328
-
328
+
329
329
  const cleanup = () => {
330
330
  if (closed) return;
331
331
  closed = true;
@@ -346,18 +346,18 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
346
346
  }
347
347
  readable.end();
348
348
  };
349
-
349
+
350
350
  const onError = err => {
351
351
  cleanup();
352
352
  Stream.destroy(err);
353
353
  };
354
-
354
+
355
355
  const onClose = () => {
356
356
  cleanup();
357
357
  Stream.end();
358
358
  if (!Stream.destroyed) Stream.destroy();
359
359
  };
360
-
360
+
361
361
  const attach = w => {
362
362
  if (attached || !w) return;
363
363
  attached = true;
@@ -382,7 +382,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
382
382
  w.onclose = onClose;
383
383
  }
384
384
  };
385
-
385
+
386
386
  const detach = w => {
387
387
  if (!attached || !w) return;
388
388
  attached = false;
@@ -404,24 +404,24 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
404
404
  w.onclose = null;
405
405
  }
406
406
  };
407
-
407
+
408
408
  attach(ws);
409
409
  if (ws && ws.readyState === 1) onOpen();
410
-
410
+
411
411
  Stream.on("prefinish", swapToNoopWritable);
412
412
  Stream.on("finish", cleanup);
413
413
  Stream.on("close", cleanup);
414
414
  Proxy.on("close", swapToNoopWritable);
415
-
415
+
416
416
  return Stream;
417
417
  }
418
-
418
+
419
419
  // Create MQTT client exactly like fca-unofficial
420
420
  mqttClient = new mqtt.Client(
421
421
  () => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()),
422
422
  options
423
423
  );
424
-
424
+
425
425
  mqttClient.publishSync = mqttClient.publish.bind(mqttClient);
426
426
  mqttClient.publish = (topic, message, opts = {}, callback = () => {}) => new Promise((resolve, reject) => {
427
427
  mqttClient.publishSync(topic, message, opts, (err, data) => {
@@ -543,10 +543,188 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
543
543
  queue.sync_token = ctx.syncToken;
544
544
  }
545
545
  mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
546
+
547
+ // Also subscribe to E2EE DM sync group (sync_group 2) for direct messages
548
+ const e2eeQueue = {
549
+ sync_api_version: 11,
550
+ max_deltas_able_to_process: 200,
551
+ delta_batch_size: 200,
552
+ encoding: "JSON",
553
+ entity_fbid: ctx.userID,
554
+ initial_titan_sequence_id: ctx.lastSeqId,
555
+ device_params: null,
556
+ sync_group: 2
557
+ };
558
+ const e2eeTopic = ctx.e2eeSyncToken ? "/messenger_sync_get_diffs" : "/messenger_sync_create_queue";
559
+ if (ctx.e2eeSyncToken) {
560
+ e2eeQueue.last_seq_id = ctx.lastSeqId;
561
+ e2eeQueue.sync_token = ctx.e2eeSyncToken;
562
+ }
563
+ mqttClient.publish(e2eeTopic, JSON.stringify(e2eeQueue), { qos: 1, retain: false });
564
+
546
565
  mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
547
566
  mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
548
-
549
- utils.log("MQTT", "Queue setup messages sent");
567
+
568
+ utils.log("MQTT", "Queue setup messages sent (sync_group 1 + 2)");
569
+
570
+ // E2EE bridge polling loop — receives incoming DMs via messagix.so
571
+ if (api.e2ee && api.e2ee.isAvailable() && !ctx._e2eePollingActive) {
572
+ ctx._e2eePollingActive = true;
573
+ (async function bridgePollLoop() {
574
+ try {
575
+ const status = api.e2ee.isConnected();
576
+ if (!status.connected) {
577
+ utils.log("E2EE", "Connecting bridge for DM receive...");
578
+ await api.e2ee.connect();
579
+ utils.log("E2EE", "Bridge transport connected.");
580
+ }
581
+ if (!api.e2ee.isConnected().e2eeConnected) {
582
+ utils.log("E2EE", "Registering E2EE device keys...");
583
+ await api.e2ee.connectE2EE();
584
+ utils.log("E2EE", "E2EE ready. Starting DM poll loop.");
585
+ } else {
586
+ utils.log("E2EE", "Bridge already E2EE-ready. Starting DM poll loop.");
587
+ }
588
+ } catch (bridgeErr) {
589
+ utils.warn("E2EE", "Bridge connect failed (DMs via MQTT only):", bridgeErr && bridgeErr.message ? bridgeErr.message : bridgeErr);
590
+ ctx._e2eePollingActive = false;
591
+ return;
592
+ }
593
+ while (ctx._e2eePollingActive) {
594
+ try {
595
+ // Yield to event loop before blocking poll (same pattern as yumi-team)
596
+ await new Promise(resolve => setImmediate(resolve));
597
+ if (!ctx._e2eePollingActive) break;
598
+
599
+ // Poll with 1 s timeout — returns a typed event object, NOT an array
600
+ const ev = await api.e2ee.pollEvents(1000);
601
+ if (!ctx._e2eePollingActive) break;
602
+
603
+ // No event / empty result
604
+ if (!ev) continue;
605
+
606
+ const evType = (ev.type || '').toLowerCase();
607
+
608
+ // ── timeout: no event arrived during the poll window ──
609
+ if (evType === 'timeout') continue;
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
+ }
617
+
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) {
623
+ ctx._e2eePollingActive = false;
624
+ break;
625
+ }
626
+ continue;
627
+ }
628
+
629
+ // ── E2EE connection confirmed by the bridge ──
630
+ if (evType === 'e2eeconnected') {
631
+ utils.log("E2EE", "E2EE fully connected (bridge event).");
632
+ continue;
633
+ }
634
+
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));
646
+ continue;
647
+ }
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 (_) {}
666
+ }
667
+ if (ctx.globalOptions.autoMarkRead) {
668
+ try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
669
+ }
670
+ globalCallback(null, fmtMsg);
671
+ continue;
672
+ }
673
+
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
+ }
696
+
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;
715
+ }
716
+
717
+ // ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
718
+
719
+ } catch (pollErr) {
720
+ if (!ctx._e2eePollingActive) break;
721
+ utils.warn("E2EE", "pollEvents error:", pollErr && pollErr.message ? pollErr.message : pollErr);
722
+ await new Promise(r => setTimeout(r, 3000));
723
+ }
724
+ }
725
+ utils.log("E2EE", "Poll loop stopped.");
726
+ })();
727
+ }
550
728
 
551
729
  // Disable T_MS timeout to prevent connection cycling
552
730
  if (ctx._tmsTimeout) {
@@ -564,7 +742,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
564
742
  }
565
743
  delete ctx.tmsWait;
566
744
  };
567
-
745
+
568
746
  // Immediately mark as ready since we're connected
569
747
  if (ctx.tmsWait && typeof ctx.tmsWait === "function") ctx.tmsWait();
570
748
  }));
@@ -592,7 +770,11 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
592
770
 
593
771
  if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
594
772
  ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
595
- ctx.syncToken = jsonMessage.syncToken;
773
+ if (!ctx.syncToken) {
774
+ ctx.syncToken = jsonMessage.syncToken;
775
+ } else if (!ctx.e2eeSyncToken && jsonMessage.syncToken !== ctx.syncToken) {
776
+ ctx.e2eeSyncToken = jsonMessage.syncToken;
777
+ }
596
778
  }
597
779
  if (jsonMessage.lastIssuedSeqId) {
598
780
  ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
@@ -600,8 +782,11 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
600
782
 
601
783
  if (jsonMessage.deltas) {
602
784
  for (const delta of jsonMessage.deltas) {
785
+ utils.log("MQTT_DELTA", "class=" + (delta.class || "none") + " threadFbId=" + (delta.messageMetadata?.threadKey?.threadFbId || "null") + " otherUserFbId=" + (delta.messageMetadata?.threadKey?.otherUserFbId || "null") + " body=" + (delta.body !== undefined ? JSON.stringify(String(delta.body).slice(0,40)) : "undefined"));
603
786
  parseDelta(defaultFuncs, api, ctx, globalCallback, { delta });
604
787
  }
788
+ } else if (jsonMessage.firstDeltaSeqId) {
789
+ utils.log("MQTT_DELTA", "Queue ACK: syncToken=" + (jsonMessage.syncToken ? jsonMessage.syncToken.slice(0,20) + "..." : "none") + " firstDeltaSeqId=" + jsonMessage.firstDeltaSeqId);
605
790
  }
606
791
  } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
607
792
  const typ = {
@@ -657,7 +842,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
657
842
  ctx._mqttQuickCloseCount = 0;
658
843
  if (!ctx._mqttReauthing && globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
659
844
  ctx._mqttReauthing = true;
660
-
845
+
661
846
  // Try to refresh tokens first before full re-login
662
847
  try {
663
848
  if (api && api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
@@ -674,7 +859,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
674
859
  } catch (refreshErr) {
675
860
  utils.warn("MQTT", "Token refresh failed, proceeding with full re-login:", refreshErr.message);
676
861
  }
677
-
862
+
678
863
  globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "MQTT quick close loop")
679
864
  .then((ok) => {
680
865
  ctx._mqttReauthing = false;
@@ -738,7 +923,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
738
923
  ctx._mqttConnected = false;
739
924
  if (!ctx._ending && !ctx._cycling && ctx.globalOptions.autoReconnect) {
740
925
  try { mqttClient.end(true); } catch (_) { }
741
-
926
+
742
927
  // Try token refresh before reconnecting
743
928
  try {
744
929
  if (api && api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
@@ -746,7 +931,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
746
931
  await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
747
932
  }
748
933
  } catch (_) { /* Ignore refresh errors, will proceed with normal reconnect */ }
749
-
934
+
750
935
  // Schedule a reconnect — without this the bot silently stays offline.
751
936
  const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
752
937
  ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
@@ -799,10 +984,10 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
799
984
  if (isPermanentFailure) {
800
985
  ctx._permanentFailure = true;
801
986
  }
802
-
987
+
803
988
  const msg = detail || reason;
804
989
  utils.error("AUTH", `Authentication error -> ${reason}: ${msg}`);
805
-
990
+
806
991
  if (typeof globalCallback === "function") {
807
992
  globalCallback({
808
993
  type: "account_inactive",
@@ -860,7 +1045,7 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
860
1045
  function postSafe(...args) {
861
1046
  const lastArg = args[args.length - 1];
862
1047
  const hasCallback = typeof lastArg === 'function';
863
-
1048
+
864
1049
  if (hasCallback) {
865
1050
  const originalCallback = args[args.length - 1];
866
1051
  args[args.length - 1] = function(err, ...cbArgs) {
@@ -958,19 +1143,19 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
958
1143
  utils.log("MQTT", "Getting sequence ID...");
959
1144
  ctx.t_mqttCalled = false;
960
1145
  const resData = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
961
-
1146
+
962
1147
  if (utils.getType(resData) !== "Array") {
963
1148
  throw { error: "Not logged in" };
964
1149
  }
965
1150
  if (!Array.isArray(resData) || !resData.length) {
966
1151
  throw { error: "getSeqID: empty response" };
967
1152
  }
968
-
1153
+
969
1154
  const lastRes = resData[resData.length - 1];
970
1155
  if (lastRes && lastRes.successful_results === 0) {
971
1156
  throw { error: "getSeqID: no successful results" };
972
1157
  }
973
-
1158
+
974
1159
  const syncSeqId = resData[0] && resData[0].o0 && resData[0].o0.data && resData[0].o0.data.viewer && resData[0].o0.data.viewer.message_threads && resData[0].o0.data.viewer.message_threads.sync_sequence_id;
975
1160
  if (syncSeqId) {
976
1161
  ctx.lastSeqId = syncSeqId;
@@ -983,13 +1168,13 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
983
1168
  } catch (err) {
984
1169
  const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
985
1170
  const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
986
-
1171
+
987
1172
  if (/blocked the login|checkpoint|security check|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir|banned|disabled/i.test(msg)) {
988
1173
  utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked (permanent)");
989
1174
  ctx._seqIdFailCount = 0;
990
1175
  return emitAuthError("login_blocked", msg);
991
1176
  }
992
-
1177
+
993
1178
  throw err; // Re-throw for retry mechanism
994
1179
  }
995
1180
  }, 3, 1500);
@@ -1063,10 +1248,12 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
1063
1248
  ctx.mqttClient = undefined;
1064
1249
  ctx.lastSeqId = null;
1065
1250
  ctx.syncToken = undefined;
1251
+ ctx.e2eeSyncToken = undefined;
1066
1252
  ctx.t_mqttCalled = false;
1067
1253
  ctx._ending = false;
1068
1254
  ctx._mqttConnected = false;
1069
1255
  ctx._seqIdFailCount = 0;
1256
+ ctx._e2eePollingActive = false;
1070
1257
  next && next();
1071
1258
  };
1072
1259
  try {
@@ -1104,6 +1291,8 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
1104
1291
  utils.log("MQTT", "Stop requested");
1105
1292
  globalCallback = identity;
1106
1293
  ctx._listeningActive = false;
1294
+ ctx._e2eePollingActive = false;
1295
+ try { if (api.e2ee && ctx._e2eeBridgeHandle != null) api.e2ee.disconnect(); } catch (_) {}
1107
1296
 
1108
1297
  if (ctx._autoCycleTimer) {
1109
1298
  clearInterval(ctx._autoCycleTimer);
@@ -318,19 +318,80 @@ module.exports = (defaultFuncs, api, ctx) => {
318
318
  } catch (_) {}
319
319
  }
320
320
 
321
+ // Helper: auto-connect E2EE bridge if available but not yet connected
322
+ async function ensureBridgeConnected() {
323
+ if (!api.e2ee || !api.e2ee.isAvailable()) return false;
324
+ try {
325
+ const status = api.e2ee.isConnected();
326
+ if (status.e2eeConnected) return true;
327
+ if (!status.connected) await api.e2ee.connect();
328
+ if (!api.e2ee.isConnected().e2eeConnected) await api.e2ee.connectE2EE();
329
+ return !!api.e2ee.isConnected().e2eeConnected;
330
+ } catch (err) {
331
+ utils.warn("sendMessage", "E2EE bridge connect failed:", err && err.message ? err.message : err);
332
+ return false;
333
+ }
334
+ }
335
+
321
336
  try {
322
- const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
323
- callback(null, result);
324
- } catch (primaryErr) {
325
- if (api.sendMessageMqtt) {
337
+ if (isSingleUser) {
338
+ // E2EE DMs: bridge → MQTT → HTTP
339
+ const msgBody = typeof msg === 'string' ? msg : (msg.body || '');
340
+ const hasAttachment = msg && typeof msg === 'object' && (msg.attachment || msg.sticker || msg.url);
341
+ const chatJid = String(threadID) + '@msgr';
342
+
343
+ // 1. Bridge path (text messages only; attachments handled by later paths)
344
+ if (!hasAttachment && msgBody) {
345
+ const bridgeReady = await ensureBridgeConnected();
346
+ if (bridgeReady) {
347
+ try {
348
+ const bridgeResult = await api.e2ee.sendE2EEMessage(chatJid, msgBody, replyToMessage || undefined, undefined);
349
+ utils.log("sendMessage", "DM sent via E2EE bridge to " + chatJid);
350
+ return callback(null, bridgeResult || { messageID: null, threadID: String(threadID) });
351
+ } catch (bridgeErr) {
352
+ utils.warn("sendMessage", "Bridge E2EE send failed for " + chatJid + ", trying MQTT:", bridgeErr && bridgeErr.message ? bridgeErr.message : bridgeErr);
353
+ }
354
+ }
355
+ }
356
+
357
+ // 2. MQTT path
358
+ if (api.sendMessageMqtt) {
359
+ try {
360
+ const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
361
+ utils.log("sendMessage", "DM sent via MQTT to thread " + threadID);
362
+ return callback(null, mqttRes);
363
+ } catch (mqttErr) {
364
+ utils.warn("sendMessage", "MQTT send failed for DM " + threadID + ", falling back to HTTP:", mqttErr && mqttErr.message ? mqttErr.message : mqttErr);
365
+ }
366
+ }
367
+
368
+ // 3. HTTP fallback
326
369
  try {
327
- const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
328
- callback(null, mqttRes);
329
- } catch (fallbackErr) {
330
- callback(primaryErr);
370
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
371
+ callback(null, result);
372
+ } catch (httpErr) {
373
+ utils.error("sendMessage", "All send paths failed for DM " + threadID, httpErr);
374
+ callback(httpErr);
331
375
  }
332
376
  } else {
333
- callback(primaryErr);
377
+ // Groups: HTTP first, MQTT fallback
378
+ try {
379
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
380
+ callback(null, result);
381
+ } catch (primaryErr) {
382
+ utils.warn("sendMessage", "HTTP send failed for group " + threadID + ", trying MQTT:", primaryErr && primaryErr.message ? primaryErr.message : primaryErr);
383
+ if (api.sendMessageMqtt) {
384
+ try {
385
+ const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
386
+ callback(null, mqttRes);
387
+ } catch (fallbackErr) {
388
+ utils.error("sendMessage", "Both HTTP and MQTT failed for group " + threadID, fallbackErr);
389
+ callback(primaryErr);
390
+ }
391
+ } else {
392
+ callback(primaryErr);
393
+ }
394
+ }
334
395
  }
335
396
  } finally {
336
397
  if (typingTimeout) clearTimeout(typingTimeout);