ayman-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 (124) hide show
  1. package/README.md +81 -0
  2. package/func/checkUpdate.js +13 -0
  3. package/func/logAdapter.js +30 -0
  4. package/func/logger.js +66 -0
  5. package/index.d.ts +751 -0
  6. package/index.js +15 -0
  7. package/module/config.js +38 -0
  8. package/module/login.js +111 -0
  9. package/module/loginHelper.js +1296 -0
  10. package/module/options.js +37 -0
  11. package/package.json +78 -0
  12. package/src/api/action/addExternalModule.js +19 -0
  13. package/src/api/action/changeAvatar.js +137 -0
  14. package/src/api/action/changeBio.js +48 -0
  15. package/src/api/action/enableAutoSaveAppState.js +72 -0
  16. package/src/api/action/getCurrentUserID.js +11 -0
  17. package/src/api/action/handleFriendRequest.js +33 -0
  18. package/src/api/action/logout.js +76 -0
  19. package/src/api/action/refreshFb_dtsg.js +62 -0
  20. package/src/api/action/setPostReaction.js +106 -0
  21. package/src/api/action/story.js +118 -0
  22. package/src/api/action/unfriend.js +30 -0
  23. package/src/api/http/httpGet.js +28 -0
  24. package/src/api/http/httpPost.js +32 -0
  25. package/src/api/http/postFormData.js +23 -0
  26. package/src/api/messaging/J +1 -0
  27. package/src/api/messaging/addUserToGroup.js +70 -0
  28. package/src/api/messaging/changeAdminStatus.js +72 -0
  29. package/src/api/messaging/changeArchivedStatus.js +31 -0
  30. package/src/api/messaging/changeBlockedStatus.js +27 -0
  31. package/src/api/messaging/changeGroupImage.js +91 -0
  32. package/src/api/messaging/changeNickname.js +70 -0
  33. package/src/api/messaging/changeThreadColor.js +44 -0
  34. package/src/api/messaging/changeThreadEmoji.js +111 -0
  35. package/src/api/messaging/createNewGroup.js +50 -0
  36. package/src/api/messaging/createPoll.js +52 -0
  37. package/src/api/messaging/createThemeAI.js +98 -0
  38. package/src/api/messaging/deleteMessage.js +73 -0
  39. package/src/api/messaging/deleteThread.js +29 -0
  40. package/src/api/messaging/editMessage.js +67 -0
  41. package/src/api/messaging/forwardAttachment.js +55 -0
  42. package/src/api/messaging/forwardMessage.js +73 -0
  43. package/src/api/messaging/getEmojiUrl.js +29 -0
  44. package/src/api/messaging/getFriendsList.js +82 -0
  45. package/src/api/messaging/getMessage.js +829 -0
  46. package/src/api/messaging/getThemePictures.js +62 -0
  47. package/src/api/messaging/groupActions.js +119 -0
  48. package/src/api/messaging/handleMessageRequest.js +31 -0
  49. package/src/api/messaging/markAsDelivered.js +31 -0
  50. package/src/api/messaging/markAsRead.js +88 -0
  51. package/src/api/messaging/markAsReadAll.js +28 -0
  52. package/src/api/messaging/markAsSeen.js +30 -0
  53. package/src/api/messaging/muteThread.js +27 -0
  54. package/src/api/messaging/notes.js +101 -0
  55. package/src/api/messaging/removeUserFromGroup.js +51 -0
  56. package/src/api/messaging/resolvePhotoUrl.js +29 -0
  57. package/src/api/messaging/scheduler.js +100 -0
  58. package/src/api/messaging/searchForThread.js +32 -0
  59. package/src/api/messaging/sendMessage.js +270 -0
  60. package/src/api/messaging/sendTypingIndicator.js +62 -0
  61. package/src/api/messaging/setMessageReaction.js +91 -0
  62. package/src/api/messaging/setTitle.js +86 -0
  63. package/src/api/messaging/shareContact.js +47 -0
  64. package/src/api/messaging/threadColors.js +128 -0
  65. package/src/api/messaging/unsendMessage.js +73 -0
  66. package/src/api/messaging/uploadAttachment.js +492 -0
  67. package/src/api/socket/core/connectMqtt.js +259 -0
  68. package/src/api/socket/core/emitAuth.js +79 -0
  69. package/src/api/socket/core/getSeqID.js +170 -0
  70. package/src/api/socket/core/getTaskResponseData.js +27 -0
  71. package/src/api/socket/core/parseDelta.js +377 -0
  72. package/src/api/socket/detail/buildStream.js +215 -0
  73. package/src/api/socket/detail/constants.js +29 -0
  74. package/src/api/socket/listenMqtt.js +377 -0
  75. package/src/api/socket/middleware/index.js +80 -0
  76. package/src/api/threads/getThreadHistory.js +664 -0
  77. package/src/api/threads/getThreadInfo.js +296 -0
  78. package/src/api/threads/getThreadList.js +293 -0
  79. package/src/api/threads/getThreadPictures.js +43 -0
  80. package/src/api/user/J +1 -0
  81. package/src/api/user/getUserID.js +48 -0
  82. package/src/api/user/getUserInfo.js +402 -0
  83. package/src/api/user/getUserInfoV2.js +134 -0
  84. package/src/core/sendReqMqtt.js +69 -0
  85. package/src/database/helpers.js +36 -0
  86. package/src/database/models/index.js +55 -0
  87. package/src/database/models/thread.js +44 -0
  88. package/src/database/models/user.js +39 -0
  89. package/src/database/threadData.js +92 -0
  90. package/src/database/userData.js +88 -0
  91. package/src/remote/remoteClient.js +71 -0
  92. package/src/utils/broadcast.js +62 -0
  93. package/src/utils/client.js +10 -0
  94. package/src/utils/constants.js +53 -0
  95. package/src/utils/cookies.js +73 -0
  96. package/src/utils/format/attachment.js +357 -0
  97. package/src/utils/format/cookie.js +9 -0
  98. package/src/utils/format/date.js +50 -0
  99. package/src/utils/format/decode.js +44 -0
  100. package/src/utils/format/delta.js +194 -0
  101. package/src/utils/format/ids.js +64 -0
  102. package/src/utils/format/index.js +64 -0
  103. package/src/utils/format/message.js +88 -0
  104. package/src/utils/format/presence.js +132 -0
  105. package/src/utils/format/readTyp.js +44 -0
  106. package/src/utils/format/thread.js +42 -0
  107. package/src/utils/format/utils.js +141 -0
  108. package/src/utils/headers.js +96 -0
  109. package/src/utils/loginParser/autoLogin.js +125 -0
  110. package/src/utils/loginParser/helpers.js +43 -0
  111. package/src/utils/loginParser/index.js +10 -0
  112. package/src/utils/loginParser/parseAndCheckLogin.js +220 -0
  113. package/src/utils/loginParser/textUtils.js +28 -0
  114. package/src/utils/request/H +1 -0
  115. package/src/utils/request/client.js +33 -0
  116. package/src/utils/request/config.js +25 -0
  117. package/src/utils/request/defaults.js +40 -0
  118. package/src/utils/request/helpers.js +31 -0
  119. package/src/utils/request/index.js +12 -0
  120. package/src/utils/request/methods.js +92 -0
  121. package/src/utils/request/proxy.js +23 -0
  122. package/src/utils/request/retry.js +87 -0
  123. package/src/utils/request/sanitize.js +41 -0
  124. package/src/utils/sessionKeeper.js +275 -0
@@ -0,0 +1,377 @@
1
+ "use strict";
2
+ /**
3
+ * Parses MQTT delta payloads into normalized events: NewMessage, ClientPayload (reactions, unsend, reply), read receipts, etc.
4
+ */
5
+ const { formatDeltaEvent, formatMessage, _formatAttachment, formatDeltaMessage, formatDeltaReadReceipt, formatID, getType, decodeClientPayload, getMentionsFromDeltaMessage } = require("../../../utils/format");
6
+ const logger = require("../../../../func/logger");
7
+
8
+ module.exports = function createParseDelta(deps) {
9
+ const { parseAndCheckLogin } = deps;
10
+ return function parseDelta(defaultFuncs, api, ctx, globalCallback, { delta }) {
11
+ if (delta.class === "NewMessage") {
12
+ const resolveAttachmentUrl = i => {
13
+ if (!delta.attachments || i === delta.attachments.length || getType(delta.attachments) !== "Array") {
14
+ let fmtMsg;
15
+ try {
16
+ fmtMsg = formatDeltaMessage(delta);
17
+ } catch (err) {
18
+ return;
19
+ }
20
+ if (fmtMsg) {
21
+ if (!ctx.globalOptions.selfListen && fmtMsg.senderID === ctx.userID) return;
22
+ if (typeof ctx._updateThreadFromMessage === "function") {
23
+ try {
24
+ ctx._updateThreadFromMessage(fmtMsg);
25
+ } catch { }
26
+ }
27
+ globalCallback(null, fmtMsg);
28
+ }
29
+ } else {
30
+ const attachment = delta.attachments[i];
31
+ if (attachment && attachment.mercury && attachment.mercury.attach_type === "photo") {
32
+ api.resolvePhotoUrl(attachment.fbid, (err, url) => {
33
+ if (!err && attachment.mercury && attachment.mercury.metadata) {
34
+ attachment.mercury.metadata.url = url;
35
+ }
36
+ resolveAttachmentUrl(i + 1);
37
+ });
38
+ } else {
39
+ resolveAttachmentUrl(i + 1);
40
+ }
41
+ }
42
+ };
43
+ resolveAttachmentUrl(0);
44
+ } else if (delta.class === "ClientPayload") {
45
+ const clientPayload = decodeClientPayload(delta.payload);
46
+ if (clientPayload && clientPayload.deltas) {
47
+ for (const d of clientPayload.deltas) {
48
+ if (d.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
49
+ const messageReaction = {
50
+ type: "message_reaction",
51
+ threadID: (d.deltaMessageReaction.threadKey.threadFbId ? d.deltaMessageReaction.threadKey.threadFbId : d.deltaMessageReaction.threadKey.otherUserFbId).toString(),
52
+ messageID: d.deltaMessageReaction.messageId,
53
+ reaction: d.deltaMessageReaction.reaction,
54
+ senderID: d.deltaMessageReaction.senderId.toString(),
55
+ userID: d.deltaMessageReaction.userId.toString()
56
+ };
57
+ globalCallback(null, messageReaction);
58
+ } else if (d.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
59
+ const messageUnsend = {
60
+ type: "message_unsend",
61
+ threadID: (d.deltaRecallMessageData.threadKey.threadFbId ? d.deltaRecallMessageData.threadKey.threadFbId : d.deltaRecallMessageData.threadKey.otherUserFbId).toString(),
62
+ messageID: d.deltaRecallMessageData.messageID,
63
+ senderID: d.deltaRecallMessageData.senderID.toString(),
64
+ deletionTimestamp: d.deltaRecallMessageData.deletionTimestamp,
65
+ timestamp: d.deltaRecallMessageData.timestamp
66
+ };
67
+ globalCallback(null, messageUnsend);
68
+ } else if (d.deltaMessageReply) {
69
+ let callbackToReturn;
70
+ try {
71
+ const msg = d.deltaMessageReply.message;
72
+ if (!msg || !msg.messageMetadata) {
73
+ logger("parseDelta: deltaMessageReply.message or messageMetadata is missing", "warn");
74
+ return;
75
+ }
76
+ const mentions = getMentionsFromDeltaMessage(msg);
77
+ const msgMetadata = msg.messageMetadata;
78
+ const threadKey = msgMetadata.threadKey || {};
79
+ callbackToReturn = {
80
+ type: "message_reply",
81
+ threadID: (threadKey.threadFbId ? threadKey.threadFbId : threadKey.otherUserFbId || "").toString(),
82
+ messageID: msgMetadata.messageId || "",
83
+ senderID: (msgMetadata.actorFbId || "").toString(),
84
+ attachments: (msg.attachments || []).map(att => {
85
+ try {
86
+ const mercury = JSON.parse(att.mercuryJSON);
87
+ Object.assign(att, mercury);
88
+ } catch (ex) {
89
+ // Ignore parsing errors
90
+ }
91
+ return att;
92
+ }).map(att => {
93
+ let x;
94
+ try {
95
+ x = _formatAttachment(att);
96
+ } catch (ex) {
97
+ x = att;
98
+ x.error = ex;
99
+ x.type = "unknown";
100
+ }
101
+ return x;
102
+ }),
103
+ args: (msg.body || "").trim().split(/\s+/),
104
+ body: msg.body || "",
105
+ isGroup: !!threadKey.threadFbId,
106
+ mentions,
107
+ timestamp: parseInt(msgMetadata.timestamp || 0),
108
+ participantIDs: (msg.participants || []).map(e => e.toString())
109
+ };
110
+ if (d.deltaMessageReply.repliedToMessage) {
111
+ try {
112
+ const repliedTo = d.deltaMessageReply.repliedToMessage;
113
+ const rmentions = getMentionsFromDeltaMessage(repliedTo);
114
+ const msgMetadata = repliedTo.messageMetadata;
115
+ if (msgMetadata && msgMetadata.threadKey) {
116
+ callbackToReturn.messageReply = {
117
+ threadID: (msgMetadata.threadKey.threadFbId ? msgMetadata.threadKey.threadFbId : msgMetadata.threadKey.otherUserFbId || "").toString(),
118
+ messageID: msgMetadata.messageId || "",
119
+ senderID: (msgMetadata.actorFbId || "").toString(),
120
+ attachments: (repliedTo.attachments || []).map(att => {
121
+ let mercury;
122
+ try {
123
+ mercury = JSON.parse(att.mercuryJSON);
124
+ Object.assign(att, mercury);
125
+ } catch (ex) {
126
+ mercury = {};
127
+ }
128
+ return att;
129
+ }).map(att => {
130
+ let x;
131
+ try {
132
+ x = _formatAttachment(att);
133
+ } catch (ex) {
134
+ x = att;
135
+ x.error = ex;
136
+ x.type = "unknown";
137
+ }
138
+ return x;
139
+ }),
140
+ args: (repliedTo.body || "").trim().split(/\s+/),
141
+ body: repliedTo.body || "",
142
+ isGroup: !!msgMetadata.threadKey.threadFbId,
143
+ mentions: rmentions,
144
+ timestamp: parseInt(msgMetadata.timestamp || 0),
145
+ participantIDs: (repliedTo.participants || []).map(e => e.toString())
146
+ };
147
+ }
148
+ } catch (err) {
149
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
150
+ logger(`parseDelta message_reply repliedToMessage error: ${errMsg}`, "warn");
151
+ }
152
+ } else if (d.deltaMessageReply.replyToMessageId) {
153
+ return defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, {
154
+ av: ctx.globalOptions.pageID,
155
+ queries: JSON.stringify({
156
+ o0: {
157
+ doc_id: "2848441488556444",
158
+ query_params: {
159
+ thread_and_message_id: {
160
+ thread_id: callbackToReturn.threadID,
161
+ message_id: d.deltaMessageReply.replyToMessageId.id
162
+ }
163
+ }
164
+ }
165
+ })
166
+ }).then(parseAndCheckLogin(ctx, defaultFuncs)).then(resData => {
167
+ if (resData[resData.length - 1].error_results > 0) throw resData[0].o0.errors;
168
+ if (resData[resData.length - 1].successful_results === 0) throw { error: "forcedFetch: there was no successful_results", res: resData };
169
+ const fetchData = resData[0].o0.data.message;
170
+ const mobj = {};
171
+ for (const n in fetchData.message.ranges) {
172
+ mobj[fetchData.message.ranges[n].entity.id] = (fetchData.message.text || "").substr(fetchData.message.ranges[n].offset, fetchData.message.ranges[n].length);
173
+ }
174
+ callbackToReturn.messageReply = {
175
+ type: "Message",
176
+ threadID: callbackToReturn.threadID,
177
+ messageID: fetchData.message_id,
178
+ senderID: fetchData.message_sender.id.toString(),
179
+ attachments: fetchData.message.blob_attachment.map(att => _formatAttachment({ blob_attachment: att })),
180
+ args: (fetchData.message.text || "").trim().split(/\s+/) || [],
181
+ body: fetchData.message.text || "",
182
+ isGroup: callbackToReturn.isGroup,
183
+ mentions: mobj,
184
+ timestamp: parseInt(fetchData.timestamp_precise)
185
+ };
186
+ }).catch(err => {
187
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
188
+ logger(`parseDelta message_reply fetch error: ${errMsg}`, "warn");
189
+ }).finally(() => {
190
+ if (callbackToReturn) {
191
+ if (!ctx.globalOptions.selfListen && callbackToReturn.senderID === ctx.userID) return;
192
+ globalCallback(null, callbackToReturn);
193
+ }
194
+ });
195
+ } else {
196
+ if (callbackToReturn) callbackToReturn.delta = d;
197
+ }
198
+ } catch (err) {
199
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
200
+ logger(`parseDelta message_reply error: ${errMsg}`, "warn");
201
+ return;
202
+ }
203
+ if (callbackToReturn) {
204
+ if (!ctx.globalOptions.selfListen && callbackToReturn.senderID === ctx.userID) return;
205
+ globalCallback(null, callbackToReturn);
206
+ }
207
+ }
208
+ }
209
+ return;
210
+ }
211
+ }
212
+ switch (delta.class) {
213
+ case "ReadReceipt": {
214
+ let fmtMsg;
215
+ try {
216
+ fmtMsg = formatDeltaReadReceipt(delta);
217
+ } catch (err) {
218
+ return;
219
+ }
220
+ globalCallback(null, fmtMsg);
221
+ break;
222
+ }
223
+ case "AdminTextMessage": {
224
+ switch (delta.type) {
225
+ case "instant_game_dynamic_custom_update":
226
+ case "accept_pending_thread":
227
+ case "confirm_friend_request":
228
+ case "shared_album_delete":
229
+ case "shared_album_addition":
230
+ case "pin_messages_v2":
231
+ case "unpin_messages_v2":
232
+ case "change_thread_theme":
233
+ case "change_thread_nickname":
234
+ case "change_thread_icon":
235
+ case "change_thread_quick_reaction":
236
+ case "change_thread_admins":
237
+ case "group_poll":
238
+ case "joinable_group_link_mode_change":
239
+ case "magic_words":
240
+ case "change_thread_approval_mode":
241
+ case "messenger_call_log":
242
+ case "participant_joined_group_call":
243
+ case "rtc_call_log":
244
+ case "update_vote": {
245
+ let fmtMsg;
246
+ try {
247
+ fmtMsg = formatDeltaEvent(delta);
248
+ } catch (err) {
249
+ return;
250
+ }
251
+ globalCallback(null, fmtMsg);
252
+ break;
253
+ }
254
+ }
255
+ break;
256
+ }
257
+ case "ForcedFetch": {
258
+ if (!delta.threadKey) return;
259
+ const mid = delta.messageId;
260
+ const tid = delta.threadKey.threadFbId;
261
+ if (mid && tid) {
262
+ const form = {
263
+ av: ctx.globalOptions.pageID,
264
+ queries: JSON.stringify({
265
+ o0: {
266
+ doc_id: "2848441488556444",
267
+ query_params: {
268
+ thread_and_message_id: {
269
+ thread_id: tid.toString(),
270
+ message_id: mid
271
+ }
272
+ }
273
+ }
274
+ })
275
+ };
276
+ defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form).then(parseAndCheckLogin(ctx, defaultFuncs)).then(resData => {
277
+ if (resData[resData.length - 1].error_results > 0) throw resData[0].o0.errors;
278
+ if (resData[resData.length - 1].successful_results === 0) throw { error: "forcedFetch: there was no successful_results", res: resData };
279
+ const fetchData = resData[0].o0.data.message;
280
+ if (getType(fetchData) === "Object") {
281
+ switch (fetchData.__typename) {
282
+ case "ThreadImageMessage":
283
+ if ((!ctx.globalOptions.selfListen && fetchData.message_sender.id.toString() === ctx.userID) || !ctx.loggedIn) {} else {
284
+ globalCallback(null, {
285
+ type: "event",
286
+ threadID: formatID(tid.toString()),
287
+ logMessageType: "log:thread-image",
288
+ logMessageData: {
289
+ image: {
290
+ attachmentID: fetchData.image_with_metadata && fetchData.image_with_metadata.legacy_attachment_id,
291
+ width: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.x,
292
+ height: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.y,
293
+ url: fetchData.image_with_metadata && fetchData.image_with_metadata.preview.uri
294
+ }
295
+ },
296
+ logMessageBody: fetchData.snippet,
297
+ timestamp: fetchData.timestamp_precise,
298
+ author: fetchData.message_sender.id
299
+ });
300
+ }
301
+ break;
302
+ case "UserMessage": {
303
+ const event = {
304
+ type: "message",
305
+ senderID: formatID(fetchData.message_sender.id),
306
+ body: fetchData.message.text || "",
307
+ threadID: formatID(tid.toString()),
308
+ messageID: fetchData.message_id,
309
+ attachments: [
310
+ {
311
+ type: "share",
312
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
313
+ url: fetchData.extensible_attachment.story_attachment.url,
314
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
315
+ description: fetchData.extensible_attachment.story_attachment.description.text,
316
+ source: fetchData.extensible_attachment.story_attachment.source,
317
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
318
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
319
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
320
+ playable: ((fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false),
321
+ duration: ((fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0),
322
+ subattachments: fetchData.extensible_attachment.subattachments,
323
+ properties: fetchData.extensible_attachment.story_attachment.properties
324
+ }
325
+ ],
326
+ mentions: {},
327
+ timestamp: parseInt(fetchData.timestamp_precise),
328
+ isGroup: fetchData.message_sender.id !== tid.toString()
329
+ };
330
+ globalCallback(null, event);
331
+ break;
332
+ }
333
+ default:
334
+ break;
335
+ }
336
+ } else {
337
+ return;
338
+ }
339
+ }).catch(err => {
340
+ const errMsg = err && err.message ? err.message : String(err || "Unknown error");
341
+ logger(`parseDelta ForcedFetch error: ${errMsg}`, "warn");
342
+ });
343
+ }
344
+ break;
345
+ }
346
+ case "ThreadName":
347
+ case "ParticipantsAddedToGroupThread":
348
+ case "ParticipantLeftGroupThread": {
349
+ let formattedEvent;
350
+ try {
351
+ formattedEvent = formatDeltaEvent(delta);
352
+ } catch (err) {
353
+ return;
354
+ }
355
+ if (!ctx.globalOptions.selfListen && formattedEvent.author.toString() === ctx.userID) return;
356
+ if (!ctx.loggedIn) return;
357
+ globalCallback(null, formattedEvent);
358
+ break;
359
+ }
360
+ case "NewMessage": {
361
+ const hasLiveLocation = d => {
362
+ const attachment = d.attachments && d.attachments[0] && d.attachments[0].mercury && d.attachments[0].mercury.extensible_attachment;
363
+ const storyAttachment = attachment && attachment.story_attachment;
364
+ return storyAttachment && storyAttachment.style_list && storyAttachment.style_list.includes("message_live_location");
365
+ };
366
+ if (delta.attachments && delta.attachments.length === 1 && hasLiveLocation(delta)) {
367
+ delta.class = "UserLocation";
368
+ try {
369
+ const fmtMsg = formatDeltaEvent(delta);
370
+ globalCallback(null, fmtMsg);
371
+ } catch (err) {}
372
+ }
373
+ break;
374
+ }
375
+ }
376
+ };
377
+ };
@@ -0,0 +1,215 @@
1
+ "use strict";
2
+ /**
3
+ * Builds a duplex stream over WebSocket for MQTT: proxy for writes, PassThrough for reads.
4
+ * Handles ping/pong, liveness timeout, and clean shutdown.
5
+ */
6
+ const { Writable, PassThrough } = require("stream");
7
+ const Duplexify = require("duplexify");
8
+
9
+ const PING_INTERVAL_MS = 30000;
10
+ const LIVENESS_CHECK_MS = 10000;
11
+ const LIVENESS_MAX_IDLE_MS = 65000;
12
+
13
+ function buildProxy() {
14
+ let target = null;
15
+ let ended = false;
16
+ const Proxy = new Writable({
17
+ autoDestroy: true,
18
+ write(chunk, enc, cb) {
19
+ if (ended || this.destroyed) return cb();
20
+ const ws = target;
21
+ if (ws && ws.readyState === 1) {
22
+ try {
23
+ ws.send(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), cb);
24
+ } catch (e) {
25
+ cb(e);
26
+ }
27
+ } else cb();
28
+ },
29
+ writev(chunks, cb) {
30
+ if (ended || this.destroyed) return cb();
31
+ const ws = target;
32
+ if (!ws || ws.readyState !== 1) return cb();
33
+ try {
34
+ for (const { chunk } of chunks) {
35
+ ws.send(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
36
+ }
37
+ cb();
38
+ } catch (e) {
39
+ cb(e);
40
+ }
41
+ },
42
+ final(cb) {
43
+ ended = true;
44
+ const ws = target;
45
+ target = null;
46
+ if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
47
+ try {
48
+ typeof ws.terminate === "function" ? ws.terminate() : ws.close();
49
+ } catch { }
50
+ }
51
+ cb();
52
+ }
53
+ });
54
+ Proxy.setTarget = ws => {
55
+ if (ended) return;
56
+ target = ws;
57
+ };
58
+ Proxy.hardEnd = () => {
59
+ ended = true;
60
+ target = null;
61
+ };
62
+ return Proxy;
63
+ }
64
+
65
+ function buildStream(options, WebSocket, Proxy) {
66
+ const readable = new PassThrough();
67
+ const Stream = Duplexify(undefined, undefined, Object.assign({ end: false, autoDestroy: true }, options));
68
+ const NoopWritable = new Writable({ write(_c, _e, cb) { cb(); } });
69
+ let ws = WebSocket;
70
+ let pingTimer = null;
71
+ let livenessTimer = null;
72
+ let lastActivity = Date.now();
73
+ let attached = false;
74
+ let style = "prop";
75
+ let closed = false;
76
+
77
+ const toBuffer = d => {
78
+ if (Buffer.isBuffer(d)) return d;
79
+ if (d instanceof ArrayBuffer) return Buffer.from(d);
80
+ if (ArrayBuffer.isView(d)) return Buffer.from(d.buffer, d.byteOffset, d.byteLength);
81
+ return Buffer.from(String(d));
82
+ };
83
+
84
+ const swapToNoopWritable = () => {
85
+ try { Stream.setWritable(NoopWritable); } catch { }
86
+ };
87
+
88
+ const onOpen = () => {
89
+ if (closed) return;
90
+ Proxy.setTarget(ws);
91
+ Stream.setWritable(Proxy);
92
+ Stream.setReadable(readable);
93
+ Stream.emit("connect");
94
+ lastActivity = Date.now();
95
+ clearInterval(pingTimer);
96
+ clearInterval(livenessTimer);
97
+ pingTimer = setInterval(() => {
98
+ if (!ws || ws.readyState !== 1) return;
99
+ if (typeof ws.ping === "function") {
100
+ try { ws.ping(); } catch { }
101
+ } else {
102
+ try { ws.send("ping"); } catch { }
103
+ }
104
+ }, PING_INTERVAL_MS);
105
+ livenessTimer = setInterval(() => {
106
+ if (!ws || ws.readyState !== 1) return;
107
+ if (Date.now() - lastActivity > LIVENESS_MAX_IDLE_MS) {
108
+ try { typeof ws.terminate === "function" ? ws.terminate() : ws.close(); } catch { }
109
+ }
110
+ }, LIVENESS_CHECK_MS);
111
+ };
112
+
113
+ const onMessage = data => {
114
+ lastActivity = Date.now();
115
+ readable.write(toBuffer(style === "dom" && data && data.data !== undefined ? data.data : data));
116
+ };
117
+
118
+ const onPong = () => {
119
+ lastActivity = Date.now();
120
+ };
121
+
122
+ const cleanup = () => {
123
+ if (closed) return;
124
+ closed = true;
125
+ clearInterval(pingTimer);
126
+ clearInterval(livenessTimer);
127
+ pingTimer = null;
128
+ livenessTimer = null;
129
+ Proxy.hardEnd();
130
+ swapToNoopWritable();
131
+ if (ws) {
132
+ detach(ws);
133
+ try {
134
+ if (ws.readyState === 1) {
135
+ typeof ws.terminate === "function" ? ws.terminate() : ws.close();
136
+ }
137
+ } catch { }
138
+ ws = null;
139
+ }
140
+ readable.end();
141
+ };
142
+
143
+ const onError = err => {
144
+ cleanup();
145
+ Stream.destroy(err);
146
+ };
147
+
148
+ const onClose = () => {
149
+ cleanup();
150
+ Stream.end();
151
+ if (!Stream.destroyed) Stream.destroy();
152
+ };
153
+
154
+ const attach = w => {
155
+ if (attached || !w) return;
156
+ attached = true;
157
+ if (typeof w.on === "function" && typeof w.off === "function") {
158
+ style = "node";
159
+ w.on("open", onOpen);
160
+ w.on("message", onMessage);
161
+ w.on("error", onError);
162
+ w.on("close", onClose);
163
+ if (typeof w.on === "function") w.on("pong", onPong);
164
+ } else if (typeof w.addEventListener === "function" && typeof w.removeEventListener === "function") {
165
+ style = "dom";
166
+ w.addEventListener("open", onOpen);
167
+ w.addEventListener("message", onMessage);
168
+ w.addEventListener("error", onError);
169
+ w.addEventListener("close", onClose);
170
+ } else {
171
+ style = "prop";
172
+ w.onopen = onOpen;
173
+ w.onmessage = onMessage;
174
+ w.onerror = onError;
175
+ w.onclose = onClose;
176
+ }
177
+ };
178
+
179
+ const detach = w => {
180
+ if (!attached || !w) return;
181
+ attached = false;
182
+ if (style === "node" && typeof w.off === "function") {
183
+ w.off("open", onOpen);
184
+ w.off("message", onMessage);
185
+ w.off("error", onError);
186
+ w.off("close", onClose);
187
+ if (typeof w.off === "function") w.off("pong", onPong);
188
+ } else if (style === "dom" && typeof w.removeEventListener === "function") {
189
+ w.removeEventListener("open", onOpen);
190
+ w.removeEventListener("message", onMessage);
191
+ w.removeEventListener("error", onError);
192
+ w.removeEventListener("close", onClose);
193
+ } else {
194
+ w.onopen = null;
195
+ w.onmessage = null;
196
+ w.onerror = null;
197
+ w.onclose = null;
198
+ }
199
+ };
200
+
201
+ attach(ws);
202
+ if (ws && ws.readyState === 1) onOpen();
203
+
204
+ Stream.on("prefinish", swapToNoopWritable);
205
+ Stream.on("finish", cleanup);
206
+ Stream.on("close", cleanup);
207
+ Proxy.on("close", swapToNoopWritable);
208
+
209
+ return Stream;
210
+ }
211
+
212
+ module.exports = {
213
+ buildProxy,
214
+ buildStream
215
+ };
@@ -0,0 +1,29 @@
1
+ // ============================================================
2
+ // AYMAN-FCA v2.0 — MQTT Topics
3
+ // © 2025 Ayman. All Rights Reserved.
4
+ // ============================================================
5
+ "use strict";
6
+
7
+ module.exports = {
8
+ topics: [
9
+ "/ls_req",
10
+ "/ls_resp",
11
+ "/legacy_web",
12
+ "/webrtc",
13
+ "/rtc_multi",
14
+ "/onevc",
15
+ "/br_sr",
16
+ "/sr_res",
17
+ "/t_ms",
18
+ "/thread_typing",
19
+ "/orca_typing_notifications",
20
+ "/notify_disconnect",
21
+ "/orca_presence",
22
+ "/inbox",
23
+ "/mercury",
24
+ "/messaging_events",
25
+ "/orca_message_notifications",
26
+ "/pp",
27
+ "/webrtc_response"
28
+ ]
29
+ };