toru-fca 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +319 -0
  3. package/advancedSystem.js +95 -0
  4. package/build/messagix.dll +0 -0
  5. package/build/messagix.so +0 -0
  6. package/checkUpdate.js +37 -0
  7. package/config.json +45 -0
  8. package/e2ee.js +719 -0
  9. package/index.js +715 -0
  10. package/lib/index.mjs +1412 -0
  11. package/package.json +69 -0
  12. package/src/OldMessage.js +357 -0
  13. package/src/addExternalModule.js +25 -0
  14. package/src/addUserToGroup.js +117 -0
  15. package/src/changeAdminStatus.js +105 -0
  16. package/src/changeArchivedStatus.js +57 -0
  17. package/src/changeAvatar.js +138 -0
  18. package/src/changeAvatarV2.js +86 -0
  19. package/src/changeAvt.js +85 -0
  20. package/src/changeBio.js +78 -0
  21. package/src/changeBlockedStatus.js +51 -0
  22. package/src/changeBlockedStatusMqtt.js +80 -0
  23. package/src/changeCover.js +72 -0
  24. package/src/changeGroupImage.js +137 -0
  25. package/src/changeName.js +79 -0
  26. package/src/changeNickname.js +61 -0
  27. package/src/changeThreadColor.js +67 -0
  28. package/src/changeThreadEmoji.js +57 -0
  29. package/src/changeUsername.js +59 -0
  30. package/src/createCommentPost.js +230 -0
  31. package/src/createNewGroup.js +90 -0
  32. package/src/createPoll.js +73 -0
  33. package/src/createPost.js +276 -0
  34. package/src/data/getThreadInfo.json +1 -0
  35. package/src/deleteMessage.js +58 -0
  36. package/src/deleteThread.js +58 -0
  37. package/src/editMessage.js +81 -0
  38. package/src/editMessageOld.js +67 -0
  39. package/src/follow.js +74 -0
  40. package/src/forwardAttachment.js +62 -0
  41. package/src/friendList.js +104 -0
  42. package/src/getAccess.js +112 -0
  43. package/src/getAvatarUser.js +78 -0
  44. package/src/getBotInitialData.js +42 -0
  45. package/src/getCtx.js +5 -0
  46. package/src/getCurrentUserID.js +7 -0
  47. package/src/getEmojiUrl.js +29 -0
  48. package/src/getFriendsList.js +85 -0
  49. package/src/getMessage.js +849 -0
  50. package/src/getOptions.js +5 -0
  51. package/src/getRegion.js +7 -0
  52. package/src/getThreadHistory.js +682 -0
  53. package/src/getThreadHistoryDeprecated.js +71 -0
  54. package/src/getThreadInfo.js +232 -0
  55. package/src/getThreadInfoDeprecated.js +56 -0
  56. package/src/getThreadList.js +213 -0
  57. package/src/getThreadListDeprecated.js +46 -0
  58. package/src/getThreadPictures.js +59 -0
  59. package/src/getThreadTheme.js +81 -0
  60. package/src/getUID.js +119 -0
  61. package/src/getUserID.js +61 -0
  62. package/src/getUserInfo.js +66 -0
  63. package/src/handleFriendRequest.js +46 -0
  64. package/src/handleMessageRequest.js +47 -0
  65. package/src/httpGet.js +49 -0
  66. package/src/httpPost.js +48 -0
  67. package/src/httpPostFormData.js +70 -0
  68. package/src/listenMqtt.js +955 -0
  69. package/src/listenNotification.js +85 -0
  70. package/src/logout.js +19 -0
  71. package/src/markAsDelivered.js +47 -0
  72. package/src/markAsRead.js +72 -0
  73. package/src/markAsReadAll.js +40 -0
  74. package/src/markAsSeen.js +48 -0
  75. package/src/metaTheme.js +189 -0
  76. package/src/muteThread.js +45 -0
  77. package/src/note.js +228 -0
  78. package/src/pinMessage.js +59 -0
  79. package/src/refreshFb_dtsg.js +91 -0
  80. package/src/removeSuspiciousAccount.js +78 -0
  81. package/src/removeUserFromGroup.js +81 -0
  82. package/src/reply.js +442 -0
  83. package/src/resolvePhotoUrl.js +47 -0
  84. package/src/searchForThread.js +53 -0
  85. package/src/searchFriends.js +140 -0
  86. package/src/searchStickers.js +53 -0
  87. package/src/send.js +46 -0
  88. package/src/sendButtons.js +163 -0
  89. package/src/sendComment.js +159 -0
  90. package/src/sendFriendRequest.js +112 -0
  91. package/src/sendMessage.js +440 -0
  92. package/src/sendMessageMqtt.js +323 -0
  93. package/src/sendTypingIndicator.js +54 -0
  94. package/src/setActiveStatus.js +92 -0
  95. package/src/setMessageReaction.js +149 -0
  96. package/src/setMessageReactionMqtt.js +62 -0
  97. package/src/setPostReaction.js +114 -0
  98. package/src/setProfileGuard.js +44 -0
  99. package/src/setProfileLock.js +97 -0
  100. package/src/setStoryReaction.js +133 -0
  101. package/src/setStorySeen.js +108 -0
  102. package/src/setThreadTheme.js +102 -0
  103. package/src/setTitle.js +92 -0
  104. package/src/shareContact.js +110 -0
  105. package/src/shareLink.js +59 -0
  106. package/src/stopListenMqtt.js +37 -0
  107. package/src/storyManager.js +357 -0
  108. package/src/suggestFriend.js +132 -0
  109. package/src/threadColors.js +131 -0
  110. package/src/unfriend.js +54 -0
  111. package/src/unsendMessage.js +68 -0
  112. package/src/uploadAttachment.js +115 -0
  113. package/utils.js +3044 -0
package/e2ee.js ADDED
@@ -0,0 +1,719 @@
1
+ "use strict";
2
+ /* ═══════════════════════════════════════════════════════════════════════════
3
+ * alpha-fca/e2ee.js — Ultimate E2EE Bridge (v1.0.0)
4
+ * Merged best-of-all from: hridoy-custom, ST-FCA, hridoy-akash
5
+ *
6
+ * Sections:
7
+ * §0 Node.js < 22 undici webidl compatibility patch
8
+ * §1 Thread / JID detection helpers
9
+ * §2 Local media server (decrypted E2EE attachment cache)
10
+ * §3 E2EE Bridge (native Labyrinth client lifecycle)
11
+ * §4 API patch (helpers exposed on api object)
12
+ * ═══════════════════════════════════════════════════════════════════════════
13
+ */
14
+
15
+ var log = require("npmlog");
16
+ var path = require("path");
17
+ var urlMod = require("url");
18
+ var http = require("http");
19
+ var crypto = require("crypto");
20
+ var fs = require("fs");
21
+
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+ // §0 NODE.JS < 22 UNDICI COMPATIBILITY PATCH
24
+ // undici v6+ calls webidl.util.markAsUncloneable() in its CacheStorage
25
+ // constructor. That v8 binding was only introduced in Node.js v22.1.0.
26
+ // On Node.js v18/v20 the call throws "not a function" and prevents the
27
+ // E2EE ESM bundle (lib/index.mjs) from loading entirely.
28
+ //
29
+ // Fix: pre-load undici's CJS webidl module and inject a no-op shim BEFORE
30
+ // the ESM bundle imports undici. CJS modules are shared between the CJS
31
+ // and ESM loaders (same Module._cache), so the ESM bundle picks up the
32
+ // already-patched object automatically.
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+ ;(function _patchUndiciWebidlForNode20() {
35
+ try {
36
+ var v8mod = require("v8");
37
+ if (typeof v8mod.markAsUncloneable === "function") return; // Node 22+ — no patch needed
38
+
39
+ // Resolve undici relative to this file's directory so we hit the right node_modules
40
+ var searchPaths = [__dirname];
41
+ try { searchPaths = searchPaths.concat(require.resolve.paths(__dirname) || []); } catch (_) {}
42
+
43
+ var wPath = require.resolve("undici/lib/web/webidl", { paths: searchPaths });
44
+ var webidl = require(wPath);
45
+
46
+ if (webidl && webidl.util) {
47
+ if (typeof webidl.util.markAsUncloneable !== "function") {
48
+ webidl.util.markAsUncloneable = function() {};
49
+ log.info("e2ee", "Applied undici webidl.markAsUncloneable patch for Node.js < 22");
50
+ }
51
+ if (typeof webidl.util.markAsUncloneableFast !== "function") {
52
+ webidl.util.markAsUncloneableFast = function() {};
53
+ }
54
+ }
55
+ } catch (patchErr) {
56
+ // Non-fatal: undici may not be installed yet (first npm install), or Node 22+
57
+ // where the patch is not needed.
58
+ }
59
+ })();
60
+
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+ // §1 THREAD — detect E2EE JIDs (any string containing "@")
63
+ // ─────────────────────────────────────────────────────────────────────────────
64
+ function isE2EEChatJid(value) {
65
+ return typeof value === "string" && value.indexOf("@") !== -1;
66
+ }
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // §2 MEDIA SERVER — local HTTP cache for decrypted E2EE attachments
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+ var _mediaCache = new Map();
72
+ var _mediaServer = null;
73
+ var _mediaPort = null;
74
+
75
+ function _cleanExpired() {
76
+ var now = Date.now();
77
+ _mediaCache.forEach(function(entry, id) {
78
+ if (entry.expiry < now) _mediaCache.delete(id);
79
+ });
80
+ }
81
+
82
+ function _startMediaServer() {
83
+ if (_mediaServer && _mediaPort) return Promise.resolve(_mediaPort);
84
+ return new Promise(function(resolve, reject) {
85
+ var s = http.createServer(function(req, res) {
86
+ var id = req.url.replace(/^\/e2ee\//, "").split("?")[0];
87
+ var entry = _mediaCache.get(id);
88
+ if (!entry) { res.writeHead(404); return res.end("Not found"); }
89
+ res.writeHead(200, {
90
+ "Content-Type" : entry.mimeType || "application/octet-stream",
91
+ "Content-Length": entry.buffer.length,
92
+ "Cache-Control" : "no-cache"
93
+ });
94
+ res.end(entry.buffer);
95
+ });
96
+ s.listen(0, "127.0.0.1", function() {
97
+ _mediaPort = s.address().port;
98
+ _mediaServer = s;
99
+ // Register exit handlers exactly once so the port is freed on shutdown
100
+ if (!_mediaServer._exitRegistered) {
101
+ _mediaServer._exitRegistered = true;
102
+ function _closeMediaServer() {
103
+ try {
104
+ if (_mediaServer) { _mediaServer.close(); _mediaServer = null; _mediaPort = null; }
105
+ } catch (_) {}
106
+ }
107
+ process.once("exit", _closeMediaServer);
108
+ process.once("SIGINT", _closeMediaServer);
109
+ process.once("SIGTERM", _closeMediaServer);
110
+ }
111
+ resolve(_mediaPort);
112
+ });
113
+ s.on("error", reject);
114
+ });
115
+ }
116
+
117
+ async function storeMedia(buffer, mimeType) {
118
+ var port = await _startMediaServer();
119
+ _cleanExpired();
120
+ var id = crypto.randomBytes(10).toString("hex");
121
+ _mediaCache.set(id, {
122
+ buffer : buffer,
123
+ mimeType: mimeType || "application/octet-stream",
124
+ expiry : Date.now() + 10 * 60 * 1000 // 10 minutes TTL
125
+ });
126
+ return "http://127.0.0.1:" + port + "/e2ee/" + id;
127
+ }
128
+
129
+ // ─────────────────────────────────────────────────────────────────────────────
130
+ // §3 BRIDGE — native Labyrinth E2EE client lifecycle
131
+ // ─────────────────────────────────────────────────────────────────────────────
132
+ var _dynamicImport = null;
133
+ function _getDynamicImport() {
134
+ if (!_dynamicImport)
135
+ _dynamicImport = new Function("specifier", "return import(specifier);");
136
+ return _dynamicImport;
137
+ }
138
+
139
+ // ESM bundle + native binary are relative to this file's directory
140
+ var _E2EE_LIB_URL = urlMod.pathToFileURL(
141
+ path.join(__dirname, "lib", "index.mjs")
142
+ ).href;
143
+
144
+ // Polyfill File / Blob for Node < 20 before the ESM bundle initialises
145
+ ;(function _polyfillFileGlobal() {
146
+ try {
147
+ if (typeof globalThis.File === "undefined") {
148
+ var b = require("buffer");
149
+ if (b && typeof b.File === "function") globalThis.File = b.File;
150
+ }
151
+ if (typeof globalThis.Blob === "undefined") {
152
+ var b2 = require("buffer");
153
+ if (b2 && typeof b2.Blob === "function") globalThis.Blob = b2.Blob;
154
+ }
155
+ } catch (_) {}
156
+ })();
157
+
158
+ // ── Size-capped Map insert ────────────────────────────────────────────────────
159
+ // Prevents unbounded memory growth on long-running bots.
160
+ var _E2EE_MAP_CAP = 2000;
161
+ function _cappedMapSet(map, key, value) {
162
+ if (map.size >= _E2EE_MAP_CAP) {
163
+ // Delete the oldest entry (Maps preserve insertion order)
164
+ map.delete(map.keys().next().value);
165
+ }
166
+ map.set(String(key), String(value));
167
+ }
168
+
169
+ // ── Bridge internal helpers ───────────────────────────────────────────────────
170
+ function _isPromiseLike(v) { return v && typeof v.then === "function"; }
171
+
172
+ function _callUserCallback(cb, err, msg) {
173
+ if (typeof cb !== "function") return;
174
+ try {
175
+ var r = cb(err, msg);
176
+ if (_isPromiseLike(r)) r.catch(function(e) { log.error("e2ee", e); });
177
+ } catch (e) { log.error("e2ee", e); }
178
+ }
179
+
180
+ function _parseMentions(arr, text) {
181
+ var out = {};
182
+ if (!Array.isArray(arr) || !text) return out;
183
+ arr.forEach(function(m) {
184
+ if (!m || m.userId == null) return;
185
+ var o = Number(m.offset || 0), l = Number(m.length || 0);
186
+ out[String(m.userId)] = text.substring(o, o + l);
187
+ });
188
+ return out;
189
+ }
190
+
191
+ function _normalizeAttType(t) {
192
+ if (!t) return t;
193
+ t = String(t).toLowerCase();
194
+ if (t === "image") return "photo";
195
+ if (t === "document") return "file";
196
+ if (t === "voice" || t === "ptt") return "audio";
197
+ return t;
198
+ }
199
+
200
+ function _normalizeAtt(a) {
201
+ if (!a || typeof a !== "object") return a;
202
+ return {
203
+ type : _normalizeAttType(a.type),
204
+ ID : a.stickerId != null ? String(a.stickerId) : undefined,
205
+ url : a.url, filename: a.fileName, mimeType: a.mimeType,
206
+ fileSize : a.fileSize != null ? String(a.fileSize) : undefined,
207
+ width : a.width, height: a.height, duration: a.duration,
208
+ previewUrl : a.previewUrl, description: a.description, source: a.sourceText,
209
+ mediaKey : a.mediaKey, mediaSha256: a.mediaSha256,
210
+ mediaEncSha256: a.mediaEncSha256, directPath: a.directPath,
211
+ latitude : a.latitude, longitude: a.longitude, isE2EE: true
212
+ };
213
+ }
214
+
215
+ // Extract the numeric prefix from an E2EE JID like "61568577897207:69@msgr" → "61568577897207"
216
+ // Returns "" when the JID has no numeric prefix (e.g. "@msgr", "@group.facebook.com").
217
+ function _numericId(jid) {
218
+ if (!jid) return "";
219
+ var s = String(jid);
220
+ var m = s.match(/^(\d+)/);
221
+ return m ? m[1] : "";
222
+ }
223
+
224
+ function _mapMsg(ev) {
225
+ var text = ev && ev.text ? String(ev.text) : "";
226
+ var sid = ev && ev.senderId != null ? _numericId(String(ev.senderId)) : "";
227
+ var tid = ev && ev.chatJid ? String(ev.chatJid)
228
+ : (ev && ev.threadId != null ? String(ev.threadId) : "");
229
+
230
+ var messageReply = null;
231
+ if (ev && ev.replyTo) {
232
+ var _rtId = ev.replyTo.messageId != null ? ev.replyTo.messageId
233
+ : ev.replyTo.id != null ? ev.replyTo.id : undefined;
234
+ // senderId can arrive as an empty protobuf object {} — treat that as unknown
235
+ var _rtSender = (ev.replyTo.senderId != null &&
236
+ typeof ev.replyTo.senderId !== "object")
237
+ ? _numericId(String(ev.replyTo.senderId)) : "";
238
+ messageReply = {
239
+ messageID: _rtId != null ? String(_rtId) : undefined,
240
+ senderID : _rtSender,
241
+ body : ev.replyTo.text != null ? String(ev.replyTo.text) : "",
242
+ isE2EE : true
243
+ };
244
+ }
245
+
246
+ return {
247
+ // Use the standard FCA/GoatBot message type so existing command/event
248
+ // handlers receive E2EE DM and group messages without a separate adapter.
249
+ type : "message",
250
+ e2eeType : "e2ee_message",
251
+ senderID : sid,
252
+ body : text,
253
+ threadID : tid,
254
+ messageID : ev.id != null ? String(ev.id) : ev.id,
255
+ messageReply: messageReply,
256
+ attachments: Array.isArray(ev.attachments) ? ev.attachments.map(_normalizeAtt) : [],
257
+ mentions : _parseMentions(ev.mentions, text),
258
+ timestamp : ev.timestampMs != null ? Number(ev.timestampMs) : Date.now(),
259
+ isGroup : /@group\.facebook\.com$/i.test(ev.chatJid || ""),
260
+ isE2EE : true,
261
+ e2ee : {
262
+ chatJid : ev.chatJid,
263
+ senderJid : ev.senderJid,
264
+ replyTo : ev.replyTo || null,
265
+ rawMentions: ev.mentions || []
266
+ },
267
+ args: text.trim() ? text.trim().split(/\s+/) : []
268
+ };
269
+ }
270
+
271
+ function _mapEdit(ev) {
272
+ var text = ev && ev.text ? String(ev.text) : "";
273
+ return {
274
+ type : "e2ee_message_edit",
275
+ senderID : ev && ev.senderId != null ? String(ev.senderId) : "",
276
+ body : text,
277
+ threadID : ev && ev.chatJid ? String(ev.chatJid) : "",
278
+ messageID: ev ? ev.messageId : undefined,
279
+ timestamp: ev && ev.timestampMs != null ? Number(ev.timestampMs) : Date.now(),
280
+ isGroup : /@group\.facebook\.com$/i.test(ev && ev.chatJid ? ev.chatJid : ""),
281
+ isE2EE : true,
282
+ e2ee : { chatJid: ev ? ev.chatJid : undefined, senderJid: ev ? ev.senderJid : undefined },
283
+ args : text.trim() ? text.trim().split(/\s+/) : []
284
+ };
285
+ }
286
+
287
+ function _mapReaction(ev) {
288
+ return {
289
+ type : "e2ee_message_reaction",
290
+ threadID : ev && ev.chatJid ? String(ev.chatJid) : "",
291
+ messageID: ev ? ev.messageId : undefined,
292
+ reaction : ev ? ev.reaction : undefined,
293
+ senderID : ev && ev.senderId != null ? String(ev.senderId) : undefined,
294
+ userID : ev && ev.senderId != null ? String(ev.senderId) : undefined,
295
+ isE2EE : true,
296
+ e2ee : { chatJid: ev ? ev.chatJid : undefined, senderJid: ev ? ev.senderJid : undefined }
297
+ };
298
+ }
299
+
300
+ function _mapReceipt(ev) {
301
+ return {
302
+ type : "e2ee_receipt",
303
+ isE2EE: true,
304
+ e2ee : {
305
+ receiptType: ev ? ev.type : undefined,
306
+ chatJid : ev ? ev.chat : undefined,
307
+ senderJid : ev ? ev.sender : undefined,
308
+ messageIds : ev ? ev.messageIds : []
309
+ }
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Collect cookies from all relevant FB/Messenger domains.
315
+ * i_user is the page-scoped UID; treat it as c_user when c_user is missing.
316
+ * Always sets c_user from ctx.userID as a final fallback.
317
+ */
318
+ function _cookiesFromJar(ctx) {
319
+ var out = {};
320
+ var domains = [
321
+ "https://www.facebook.com",
322
+ "https://facebook.com",
323
+ "https://www.messenger.com",
324
+ "https://messenger.com"
325
+ ];
326
+ domains.forEach(function(d) {
327
+ try {
328
+ var cks = ctx.jar.getCookies(d);
329
+ if (Array.isArray(cks)) {
330
+ cks.forEach(function(c) {
331
+ if (c && c.key && !(c.key in out)) out[c.key] = c.value;
332
+ });
333
+ }
334
+ } catch (_) {}
335
+ });
336
+ if (!out.c_user && out.i_user) out.c_user = out.i_user;
337
+ if (!out.c_user && ctx && ctx.userID) out.c_user = String(ctx.userID);
338
+ return out;
339
+ }
340
+
341
+ function _normalizeMediaInput(input) {
342
+ if (Buffer.isBuffer(input)) return input;
343
+ if (Array.isArray(input)) return Buffer.from(input);
344
+ if (input && input.type === "Buffer" && Array.isArray(input.data)) return Buffer.from(input.data);
345
+ if (typeof input === "string") return Buffer.from(input, "base64");
346
+ throw new Error("E2EE media data must be Buffer, byte array, Buffer-JSON, or base64 string");
347
+ }
348
+
349
+ // ── createBridge ──────────────────────────────────────────────────────────────
350
+ function createBridge(ctx) {
351
+ // Return cached bridge instance if already created for this ctx
352
+ if (ctx._e2eeBridge) return ctx._e2eeBridge;
353
+
354
+ var state = {
355
+ client : null,
356
+ connected : false,
357
+ connectingPromise: null,
358
+ listenerAttached: false,
359
+ lastGlobalCallback: null,
360
+ lastReadyPayload: null,
361
+ fullyReady : false,
362
+ stopped : false // set by disconnect() to prevent reconnect loops
363
+ };
364
+
365
+ function _ensureEnabled() {
366
+ if (ctx.globalOptions.enableE2EE === false)
367
+ throw new Error("E2EE is disabled. Set enableE2EE:true in options or config.json.");
368
+ }
369
+
370
+ async function _loadClient() {
371
+ var mod;
372
+ try { mod = await _getDynamicImport()(_E2EE_LIB_URL); }
373
+ catch (err) {
374
+ throw new Error("Cannot load E2EE bundle (" + _E2EE_LIB_URL + "): " +
375
+ (err && err.message ? err.message : String(err)));
376
+ }
377
+ if (!mod || !mod.Client)
378
+ throw new Error("E2EE bundle loaded but Client export not found");
379
+ return mod.Client;
380
+ }
381
+
382
+ function _attachEvents(initCb) {
383
+ if (!state.client || state.listenerAttached) return;
384
+ state.listenerAttached = true;
385
+ if (typeof initCb === "function") state.lastGlobalCallback = initCb;
386
+
387
+ state.client.on("ready", function(p) {
388
+ state.lastReadyPayload = p;
389
+ _callUserCallback(state.lastGlobalCallback, null, { type: "e2ee_ready", isE2EE: true, data: p || null });
390
+ });
391
+ state.client.on("fullyReady", function() {
392
+ state.fullyReady = true;
393
+ _callUserCallback(state.lastGlobalCallback, null, { type: "e2ee_fully_ready", isE2EE: true });
394
+ });
395
+ state.client.on("e2eeConnected", function() {
396
+ _callUserCallback(state.lastGlobalCallback, null, { type: "e2ee_connected", isE2EE: true });
397
+ });
398
+ state.client.on("deviceDataChanged", function(p) {
399
+ if (p && p.deviceData) ctx._e2eeDeviceData = p.deviceData;
400
+ _callUserCallback(state.lastGlobalCallback, null, {
401
+ type : "e2ee_device_data_changed",
402
+ isE2EE : true,
403
+ deviceData: p ? p.deviceData : undefined
404
+ });
405
+ });
406
+ state.client.on("e2eeMessage", function(ev) {
407
+ var mapped = _mapMsg(ev);
408
+
409
+ // ── Self-listen filter ─────────────────────────────────────────────────
410
+ // When selfListen is false (default), suppress messages the bot sent.
411
+ // Guard 1: sendMessage records outgoing IDs in global._e2eeBotSentMsgIds.
412
+ // Guard 2: UID-based cookie comparison as a fallback.
413
+ if (!ctx.globalOptions.selfListen) {
414
+ if (mapped.messageID &&
415
+ global._e2eeBotSentMsgIds instanceof Set &&
416
+ global._e2eeBotSentMsgIds.has(mapped.messageID)) {
417
+ return;
418
+ }
419
+ // UID-based fallback filter via c_user / i_user cookie
420
+ var _cookieUid = (function() {
421
+ try {
422
+ var _jar = ctx.jar.getCookies("https://www.facebook.com");
423
+ for (var _ci = 0; _ci < _jar.length; _ci++) {
424
+ var _ck = _jar[_ci];
425
+ if (_ck.key === "c_user" || _ck.key === "i_user") return String(_ck.value);
426
+ }
427
+ } catch (_) {}
428
+ return String(ctx.userID || "");
429
+ })();
430
+ if (_cookieUid && mapped.senderID && mapped.senderID === _cookieUid) return;
431
+ }
432
+ // ── End self-listen filter ─────────────────────────────────────────────
433
+
434
+ global._e2eeMessageMap = global._e2eeMessageMap || new Map();
435
+ global._e2eeSenderJidMap = global._e2eeSenderJidMap || new Map();
436
+
437
+ if (mapped.messageID && mapped.threadID)
438
+ _cappedMapSet(global._e2eeMessageMap, mapped.messageID, mapped.threadID);
439
+ if (mapped.messageID && ev.senderJid)
440
+ _cappedMapSet(global._e2eeSenderJidMap, mapped.messageID, ev.senderJid);
441
+
442
+ // Register replyTo message IDs so unsend/react works on replied messages
443
+ if (ev.replyTo && ev.chatJid) {
444
+ var _rtReg = ev.replyTo.messageId || ev.replyTo.id;
445
+ if (_rtReg) _cappedMapSet(global._e2eeMessageMap, _rtReg, ev.chatJid);
446
+ }
447
+
448
+ _callUserCallback(state.lastGlobalCallback, null, mapped);
449
+ });
450
+
451
+ state.client.on("e2eeMessageEdit", function(ev) {
452
+ _callUserCallback(state.lastGlobalCallback, null, _mapEdit(ev));
453
+ });
454
+ state.client.on("e2eeReaction", function(ev) {
455
+ _callUserCallback(state.lastGlobalCallback, null, _mapReaction(ev));
456
+ });
457
+ state.client.on("e2eeReceipt", function(ev) {
458
+ _callUserCallback(state.lastGlobalCallback, null, _mapReceipt(ev));
459
+ });
460
+
461
+ state.client.on("error", function(err) {
462
+ var msg = err && err.message ? err.message : String(err || "");
463
+ // Transient network errors — the bridge will reconnect automatically
464
+ if (/close 1006|unexpected EOF|ECONNRESET|ETIMEDOUT|read loop/i.test(msg)) {
465
+ log.warn("e2ee", "Transient network error — bridge will reconnect: " + msg);
466
+ return;
467
+ }
468
+ _callUserCallback(state.lastGlobalCallback, err || new Error("Unknown E2EE error"));
469
+ });
470
+
471
+ state.client.on("disconnected", function(info) {
472
+ state.connected = false;
473
+ state.fullyReady = false;
474
+ // Jittered reconnect delay: 3–4 seconds (avoids thundering-herd on restarts)
475
+ var delay = 3000 + Math.floor(Math.random() * 1000);
476
+ log.warn("e2ee", "E2EE disconnected — reconnecting in " + Math.round(delay / 1000) + "s");
477
+ setTimeout(function() {
478
+ if (!state.stopped && !state.connectingPromise) {
479
+ var cb = (ctx && ctx._globalCallback) || state.lastGlobalCallback;
480
+ if (typeof cb === "function") state.lastGlobalCallback = cb;
481
+ connect(cb).catch(function(e) {
482
+ log.error("e2ee", "E2EE reconnect failed: " + (e && e.message ? e.message : String(e)));
483
+ });
484
+ }
485
+ }, delay);
486
+ _callUserCallback(state.lastGlobalCallback, null, {
487
+ type : "e2ee_disconnected",
488
+ isE2EE: true,
489
+ data : info || null
490
+ });
491
+ });
492
+ }
493
+
494
+ /**
495
+ * Connect the E2EE bridge.
496
+ * @param {Function} [globalCallback] message handler (err, event)
497
+ * @returns {Promise<Client>}
498
+ */
499
+ async function connect(globalCallback) {
500
+ _ensureEnabled();
501
+ state.stopped = false;
502
+ if (typeof globalCallback === "function") state.lastGlobalCallback = globalCallback;
503
+ if (state.connected && state.client) return state.client;
504
+ if (state.connectingPromise) return state.connectingPromise;
505
+
506
+ state.connectingPromise = (async function() {
507
+ var Client = await _loadClient();
508
+ if (!state.client) {
509
+ var cookies = _cookiesFromJar(ctx);
510
+ if (!cookies.c_user) {
511
+ throw new Error("Cannot start E2EE: c_user cookie missing — is the bot logged in?");
512
+ }
513
+ if (!cookies.xs) {
514
+ log.warn("e2ee", "xs cookie not found — E2EE bridge may fail if Facebook requires it");
515
+ }
516
+
517
+ var _logLevel = (ctx.globalOptions && ctx.globalOptions.e2eeLogLevel) || "none";
518
+ var _autoReconnect = (ctx.globalOptions && ctx.globalOptions.e2eeAutoReconnect !== false);
519
+
520
+ var opts = {
521
+ enableE2EE : true,
522
+ e2eeMemoryOnly: ctx.globalOptions.e2eeMemoryOnly !== false,
523
+ autoReconnect: _autoReconnect,
524
+ logLevel : _logLevel
525
+ };
526
+
527
+ // Device key persistence (saveType: 'path')
528
+ if (ctx.globalOptions.e2eeDevicePath) {
529
+ opts.devicePath = path.resolve(String(ctx.globalOptions.e2eeDevicePath));
530
+ try { fs.mkdirSync(path.dirname(opts.devicePath), { recursive: true }); } catch (_) {}
531
+ }
532
+ if (ctx.globalOptions.e2eeDeviceData) opts.deviceData = ctx.globalOptions.e2eeDeviceData;
533
+ if (!opts.deviceData && global._pendingE2eeDeviceData) {
534
+ opts.deviceData = global._pendingE2eeDeviceData;
535
+ delete global._pendingE2eeDeviceData;
536
+ }
537
+
538
+ state.client = new Client(cookies, opts);
539
+ _attachEvents(globalCallback);
540
+ }
541
+ await state.client.connect();
542
+ state.connected = true;
543
+ state.fullyReady = false;
544
+ return state.client;
545
+ })();
546
+
547
+ try { return await state.connectingPromise; }
548
+ finally { state.connectingPromise = null; }
549
+ }
550
+
551
+ /** Cleanly disconnect and prevent auto-reconnect. */
552
+ async function disconnect() {
553
+ state.stopped = true;
554
+ if (!state.client) { state.connected = false; return; }
555
+ try { await state.client.disconnect(); }
556
+ finally {
557
+ state.connected = false;
558
+ state.connectingPromise = null;
559
+ state.listenerAttached = false;
560
+ state.client = null;
561
+ }
562
+ }
563
+
564
+ async function _ensureClient() {
565
+ _ensureEnabled();
566
+ if (state.connected && state.client) return state.client;
567
+ return connect();
568
+ }
569
+
570
+ var bridge = {
571
+ connect,
572
+ disconnect,
573
+ isConnected : function() { return !!(state.client && state.connected); },
574
+ isFullyReady: function() {
575
+ if (!state.client || !state.connected) return false;
576
+ if (typeof state.client.isFullyReady === "function") {
577
+ try { return !!state.client.isFullyReady(); } catch (_) {}
578
+ }
579
+ return !!state.fullyReady;
580
+ },
581
+ getState : function() { return state; },
582
+ getDeviceData: async function() { return (await _ensureClient()).getDeviceData(); },
583
+ sendMessage : async function(jid, text, opts) {
584
+ return (await _ensureClient()).sendE2EEMessage(jid, text, opts || {});
585
+ },
586
+ sendReaction : async function(jid, msgId, senderJid, emoji) {
587
+ return (await _ensureClient()).sendE2EEReaction(jid, msgId, senderJid, emoji);
588
+ },
589
+ sendTyping : async function(jid, isTyping) {
590
+ return (await _ensureClient()).sendE2EETyping(jid, isTyping !== false);
591
+ },
592
+ unsendMessage: async function(jid, msgId) {
593
+ return (await _ensureClient()).unsendE2EEMessage(jid, msgId);
594
+ },
595
+ editMessage : async function(jid, msgId, text) {
596
+ return (await _ensureClient()).editE2EEMessage(jid, msgId, text);
597
+ },
598
+ downloadMedia: async function(opts) {
599
+ var client = await _ensureClient();
600
+ var size = opts.fileSize != null ? Number(opts.fileSize) : undefined;
601
+ var res = await client.downloadE2EEMedia({
602
+ directPath : opts.directPath,
603
+ mediaKey : opts.mediaKey,
604
+ mediaSha256 : opts.mediaSha256,
605
+ mediaEncSha256: opts.mediaEncSha256,
606
+ mediaType : opts.mediaType,
607
+ mimeType : opts.mimeType,
608
+ fileSize : size
609
+ });
610
+ return { data: res.data, mimeType: res.mimeType, fileSize: Number(res.fileSize) };
611
+ },
612
+ sendMedia: async function(jid, mediaType, data, opts) {
613
+ var client = await _ensureClient();
614
+ var buf = _normalizeMediaInput(data);
615
+ var o = opts || {};
616
+ var ntype = String(mediaType || "").toLowerCase();
617
+ switch (ntype) {
618
+ case "image":
619
+ return client.sendE2EEImage(jid, buf, o.mimeType || "image/jpeg", {
620
+ caption: o.caption || "", width: o.width, height: o.height,
621
+ replyToId: o.replyToId, replyToSenderJid: o.replyToSenderJid
622
+ });
623
+ case "video":
624
+ return client.sendE2EEVideo(jid, buf, o.mimeType || "video/mp4", {
625
+ caption: o.caption || "", duration: o.duration, width: o.width, height: o.height,
626
+ replyToId: o.replyToId, replyToSenderJid: o.replyToSenderJid
627
+ });
628
+ case "audio":
629
+ case "voice": {
630
+ var mime = o.mimeType || "audio/ogg; codecs=opus";
631
+ var isVoice = ntype === "voice" || !!o.ptt;
632
+ return client.sendE2EEAudio(jid, buf, mime, {
633
+ ptt: isVoice,
634
+ duration: o.duration != null ? Number(o.duration) : undefined,
635
+ replyToId: o.replyToId, replyToSenderJid: o.replyToSenderJid
636
+ });
637
+ }
638
+ case "file":
639
+ case "document":
640
+ return client.sendE2EEDocument(jid, buf, o.filename || "file.bin",
641
+ o.mimeType || "application/octet-stream", {
642
+ replyToId: o.replyToId, replyToSenderJid: o.replyToSenderJid
643
+ });
644
+ case "sticker":
645
+ return client.sendE2EESticker(jid, buf, o.mimeType || "image/webp", {
646
+ replyToId: o.replyToId, replyToSenderJid: o.replyToSenderJid
647
+ });
648
+ default:
649
+ throw new Error("Unsupported E2EE mediaType: " + ntype);
650
+ }
651
+ }
652
+ };
653
+
654
+ ctx._e2eeBridge = bridge;
655
+ return bridge;
656
+ }
657
+
658
+ // ─────────────────────────────────────────────────────────────────────────────
659
+ // §4 PATCH API — shared E2EE helpers exposed on the api object
660
+ // ─────────────────────────────────────────────────────────────────────────────
661
+ global._e2eeMessageMap = global._e2eeMessageMap || new Map();
662
+ global._e2eeSenderJidMap = global._e2eeSenderJidMap || new Map();
663
+
664
+ function _regMsg(msgID, jid) {
665
+ if (msgID && jid) _cappedMapSet(global._e2eeMessageMap, msgID, jid);
666
+ }
667
+
668
+ /**
669
+ * patchApiForE2EE — call once after buildAPI() when E2EE is enabled.
670
+ * Adds: api.downloadE2EEMedia, api.resolveE2EEAttachment, api.sendTypingE2EE
671
+ */
672
+ function patchApiForE2EE(api, ctx) {
673
+ if (typeof api.downloadE2EEMedia !== "function") {
674
+ api.downloadE2EEMedia = function(options) {
675
+ return createBridge(ctx).downloadMedia(options);
676
+ };
677
+ }
678
+
679
+ if (typeof api.resolveE2EEAttachment !== "function") {
680
+ api.resolveE2EEAttachment = async function(att) {
681
+ if (!att || !att.isE2EE) return att;
682
+ if (att.url && /^https?:\/\//.test(att.url)) return att;
683
+ if (!att.directPath || !att.mediaKey || !att.mediaSha256 || !att.mimeType) return att;
684
+ try {
685
+ var rawType = att.type === "photo" ? "image" : (att.type || "image");
686
+ var res = await api.downloadE2EEMedia({
687
+ directPath : att.directPath,
688
+ mediaKey : att.mediaKey,
689
+ mediaSha256 : att.mediaSha256,
690
+ mediaEncSha256: att.mediaEncSha256 || undefined,
691
+ mediaType : rawType,
692
+ mimeType : att.mimeType,
693
+ fileSize : Number(att.fileSize)
694
+ });
695
+ var localUrl = await storeMedia(res.data, res.mimeType || att.mimeType || "image/jpeg");
696
+ return Object.assign({}, att, { url: localUrl });
697
+ } catch (e) {
698
+ log.error("E2EE", "resolveE2EEAttachment failed: " + (e && e.message ? e.message : String(e)));
699
+ return att;
700
+ }
701
+ };
702
+ }
703
+
704
+ if (typeof api.sendTypingE2EE !== "function") {
705
+ api.sendTypingE2EE = function(chatJid, isTyping) {
706
+ if (!isE2EEChatJid(chatJid)) return Promise.resolve();
707
+ return createBridge(ctx).sendTyping(chatJid, isTyping !== false).catch(function() {});
708
+ };
709
+ }
710
+ }
711
+
712
+ module.exports = {
713
+ isE2EEChatJid,
714
+ storeMedia,
715
+ createBridge,
716
+ patchApiForE2EE,
717
+ _cappedMapSet, // exposed for sendMessage.js internal use
718
+ _regMsg // exposed for sendMessage.js to register outgoing E2EE message IDs
719
+ };