fca-orion-api 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 (57) hide show
  1. package/config.js +5 -0
  2. package/data/fcaVersion.json +0 -0
  3. package/index.js +417 -0
  4. package/instantUpdate.js +40 -0
  5. package/package.json +30 -0
  6. package/src/addExternalModule.js +19 -0
  7. package/src/addUserToGroup.js +113 -0
  8. package/src/changeAdminStatus.js +79 -0
  9. package/src/changeArchivedStatus.js +55 -0
  10. package/src/changeAvatar.js +126 -0
  11. package/src/changeBio.js +77 -0
  12. package/src/changeBlockedStatus.js +47 -0
  13. package/src/changeGroupImage.js +132 -0
  14. package/src/changeNickname.js +59 -0
  15. package/src/changeThreadColor.js +65 -0
  16. package/src/changeThreadEmoji.js +55 -0
  17. package/src/createNewGroup.js +86 -0
  18. package/src/createPoll.js +71 -0
  19. package/src/deleteMessage.js +56 -0
  20. package/src/deleteThread.js +56 -0
  21. package/src/forwardAttachment.js +60 -0
  22. package/src/getCurrentUserID.js +7 -0
  23. package/src/getEmojiUrl.js +29 -0
  24. package/src/getFriendsList.js +83 -0
  25. package/src/getMessage.js +796 -0
  26. package/src/getThreadHistory.js +666 -0
  27. package/src/getThreadInfo.js +232 -0
  28. package/src/getThreadList.js +241 -0
  29. package/src/getThreadPictures.js +79 -0
  30. package/src/getUserID.js +66 -0
  31. package/src/getUserInfo.js +74 -0
  32. package/src/handleFriendRequest.js +61 -0
  33. package/src/handleMessageRequest.js +65 -0
  34. package/src/httpGet.js +57 -0
  35. package/src/httpPost.js +57 -0
  36. package/src/httpPostFormData.js +63 -0
  37. package/src/listenMqtt.js +853 -0
  38. package/src/logout.js +75 -0
  39. package/src/markAsDelivered.js +58 -0
  40. package/src/markAsRead.js +80 -0
  41. package/src/markAsReadAll.js +50 -0
  42. package/src/markAsSeen.js +59 -0
  43. package/src/muteThread.js +52 -0
  44. package/src/refreshFb_dtsg.js +81 -0
  45. package/src/removeUserFromGroup.js +79 -0
  46. package/src/resolvePhotoUrl.js +45 -0
  47. package/src/searchForThread.js +53 -0
  48. package/src/sendMessage.js +477 -0
  49. package/src/sendTypingIndicator.js +103 -0
  50. package/src/setMessageReaction.js +121 -0
  51. package/src/setPostReaction.js +109 -0
  52. package/src/setTitle.js +86 -0
  53. package/src/threadColors.js +131 -0
  54. package/src/unfriend.js +52 -0
  55. package/src/unsendMessage.js +49 -0
  56. package/src/uploadAttachment.js +95 -0
  57. package/utils.js +1545 -0
@@ -0,0 +1,853 @@
1
+ /* eslint-disable no-redeclare */
2
+ "use strict";
3
+ const utils = require("../utils");
4
+ const log = require("npmlog");
5
+ const mqtt = require('mqtt');
6
+ const websocket = require('websocket-stream');
7
+ const HttpsProxyAgent = require('https-proxy-agent');
8
+ const EventEmitter = require('events');
9
+
10
+ const identity = function () { };
11
+
12
+ const topics = [
13
+ "/legacy_web",
14
+ "/webrtc",
15
+ "/rtc_multi",
16
+ "/onevc",
17
+ "/br_sr", //Notification
18
+ //Need to publish /br_sr right after this
19
+ "/sr_res",
20
+ "/t_ms",
21
+ "/thread_typing",
22
+ "/orca_typing_notifications",
23
+ "/notify_disconnect",
24
+ //Need to publish /messenger_sync_create_queue right after this
25
+ "/orca_presence",
26
+ //Will receive /sr_res right here.
27
+
28
+ "/legacy_web_mtouch"
29
+ // "/inbox",
30
+ // "/mercury",
31
+ // "/messaging_events",
32
+ // "/orca_message_notifications",
33
+ // "/pp",
34
+ // "/webrtc_response",
35
+ ];
36
+
37
+ function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
38
+ //Don't really know what this does but I think it's for the active state?
39
+ //TODO: Move to ctx when implemented
40
+ const chatOn = ctx.globalOptions.online;
41
+ const foreground = false;
42
+
43
+ const sessionID = Math.floor(Math.random() * 9007199254740991) + 1;
44
+ const username = {
45
+ u: ctx.i_userID || ctx.userID,
46
+ s: sessionID,
47
+ chat_on: chatOn,
48
+ fg: foreground,
49
+ d: utils.getGUID(),
50
+ ct: "websocket",
51
+ //App id from facebook
52
+ aid: "219994525426954",
53
+ mqtt_sid: "",
54
+ cp: 3,
55
+ ecp: 10,
56
+ st: [],
57
+ pm: [],
58
+ dc: "",
59
+ no_auto_fg: true,
60
+ gas: null,
61
+ pack: [],
62
+ a: ctx.globalOptions.userAgent,
63
+ aids: null
64
+ };
65
+ const cookies = ctx.jar.getCookies("https://www.facebook.com").join("; ");
66
+
67
+ let host;
68
+ if (ctx.mqttEndpoint) {
69
+ host = `${ctx.mqttEndpoint}&sid=${sessionID}`;
70
+ } else if (ctx.region) {
71
+ host = `wss://edge-chat.facebook.com/chat?region=${ctx.region.toLocaleLowerCase()}&sid=${sessionID}`;
72
+ } else {
73
+ host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}`;
74
+ }
75
+
76
+ const options = {
77
+ clientId: "mqttwsclient",
78
+ protocolId: 'MQIsdp',
79
+ protocolVersion: 3,
80
+ username: JSON.stringify(username),
81
+ clean: true,
82
+ wsOptions: {
83
+ headers: {
84
+ 'Cookie': cookies,
85
+ 'Origin': 'https://www.facebook.com',
86
+ 'User-Agent': ctx.globalOptions.userAgent,
87
+ 'Referer': 'https://www.facebook.com/',
88
+ 'Host': new URL(host).hostname //'edge-chat.facebook.com'
89
+ },
90
+ origin: 'https://www.facebook.com',
91
+ protocolVersion: 13
92
+ },
93
+ keepalive: 10,
94
+ reschedulePings: false
95
+ };
96
+
97
+ if (typeof ctx.globalOptions.proxy != "undefined") {
98
+ const agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
99
+ options.wsOptions.agent = agent;
100
+ }
101
+
102
+ ctx.mqttClient = new mqtt.Client(_ => websocket(host, options.wsOptions), options);
103
+
104
+ const mqttClient = ctx.mqttClient;
105
+
106
+ mqttClient.on('error', function (err) {
107
+ log.error("listenMqtt", err);
108
+ mqttClient.end();
109
+ if (ctx.globalOptions.autoReconnect) {
110
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
111
+ } else {
112
+ utils.checkLiveCookie(ctx, defaultFuncs)
113
+ .then(res => {
114
+ globalCallback({
115
+ type: "stop_listen",
116
+ error: "Connection refused: Server unavailable"
117
+ }, null);
118
+ })
119
+ .catch(err => {
120
+ globalCallback({
121
+ type: "account_inactive",
122
+ error: "Maybe your account is blocked by facebook, please login and check at https://facebook.com"
123
+ }, null);
124
+ });
125
+ }
126
+ });
127
+
128
+ mqttClient.on('close', function () {
129
+
130
+ });
131
+
132
+ mqttClient.on('connect', function () {
133
+ topics.forEach(function (topicsub) {
134
+ mqttClient.subscribe(topicsub);
135
+ });
136
+
137
+ let topic;
138
+ const queue = {
139
+ sync_api_version: 10,
140
+ max_deltas_able_to_process: 1000,
141
+ delta_batch_size: 500,
142
+ encoding: "JSON",
143
+ entity_fbid: ctx.i_userID || ctx.userID
144
+ };
145
+
146
+ if (ctx.syncToken) {
147
+ topic = "/messenger_sync_get_diffs";
148
+ queue.last_seq_id = ctx.lastSeqId;
149
+ queue.sync_token = ctx.syncToken;
150
+ } else {
151
+ topic = "/messenger_sync_create_queue";
152
+ queue.initial_titan_sequence_id = ctx.lastSeqId;
153
+ queue.device_params = null;
154
+ }
155
+
156
+ mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
157
+ // set status online
158
+ // fix by NTKhang
159
+ mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
160
+ mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
161
+
162
+ const rTimeout = setTimeout(function () {
163
+ mqttClient.end();
164
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
165
+ }, 5000);
166
+
167
+ ctx.tmsWait = function () {
168
+ clearTimeout(rTimeout);
169
+ ctx.globalOptions.emitReady ? globalCallback({
170
+ type: "ready",
171
+ error: null
172
+ }) : "";
173
+ delete ctx.tmsWait;
174
+ };
175
+
176
+ });
177
+
178
+ mqttClient.on('message', function (topic, message, _packet) {
179
+ let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message;
180
+ try {
181
+ jsonMessage = JSON.parse(jsonMessage);
182
+ }
183
+ catch (e) {
184
+ jsonMessage = {};
185
+ }
186
+
187
+ if (jsonMessage.type === "jewel_requests_add") {
188
+ globalCallback(null, {
189
+ type: "friend_request_received",
190
+ actorFbId: jsonMessage.from.toString(),
191
+ timestamp: Date.now().toString()
192
+ });
193
+ }
194
+ else if (jsonMessage.type === "jewel_requests_remove_old") {
195
+ globalCallback(null, {
196
+ type: "friend_request_cancel",
197
+ actorFbId: jsonMessage.from.toString(),
198
+ timestamp: Date.now().toString()
199
+ });
200
+ }
201
+ else if (topic === "/t_ms") {
202
+ if (ctx.tmsWait && typeof ctx.tmsWait == "function") {
203
+ ctx.tmsWait();
204
+ }
205
+
206
+ if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
207
+ ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
208
+ ctx.syncToken = jsonMessage.syncToken;
209
+ }
210
+
211
+ if (jsonMessage.lastIssuedSeqId) {
212
+ ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
213
+ }
214
+
215
+ //If it contains more than 1 delta
216
+ for (const i in jsonMessage.deltas) {
217
+ const delta = jsonMessage.deltas[i];
218
+ parseDelta(defaultFuncs, api, ctx, globalCallback, { "delta": delta });
219
+ }
220
+ } else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
221
+ const typ = {
222
+ type: "typ",
223
+ isTyping: !!jsonMessage.state,
224
+ from: jsonMessage.sender_fbid.toString(),
225
+ threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
226
+ };
227
+ (function () { globalCallback(null, typ); })();
228
+ } else if (topic === "/orca_presence") {
229
+ if (!ctx.globalOptions.updatePresence) {
230
+ for (const i in jsonMessage.list) {
231
+ const data = jsonMessage.list[i];
232
+ const userID = data["u"];
233
+
234
+ const presence = {
235
+ type: "presence",
236
+ userID: userID.toString(),
237
+ //Convert to ms
238
+ timestamp: data["l"] * 1000,
239
+ statuses: data["p"]
240
+ };
241
+ (function () { globalCallback(null, presence); })();
242
+ }
243
+ }
244
+ }
245
+
246
+ });
247
+
248
+ }
249
+
250
+ function parseDelta(defaultFuncs, api, ctx, globalCallback, v) {
251
+ if (v.delta.class == "NewMessage") {
252
+ //Not tested for pages
253
+ if (ctx.globalOptions.pageID &&
254
+ ctx.globalOptions.pageID != v.queue
255
+ )
256
+ return;
257
+
258
+ (function resolveAttachmentUrl(i) {
259
+ if (i == (v.delta.attachments || []).length) {
260
+ let fmtMsg;
261
+ try {
262
+ fmtMsg = utils.formatDeltaMessage(v);
263
+ } catch (err) {
264
+ return globalCallback({
265
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
266
+ detail: err,
267
+ res: v,
268
+ type: "parse_error"
269
+ });
270
+ }
271
+ if (fmtMsg) {
272
+ if (ctx.globalOptions.autoMarkDelivery) {
273
+ markDelivery(ctx, api, fmtMsg.threadID, fmtMsg.messageID);
274
+ }
275
+ }
276
+ return !ctx.globalOptions.selfListen &&
277
+ (fmtMsg.senderID === ctx.i_userID || fmtMsg.senderID === ctx.userID) ?
278
+ undefined :
279
+ (function () { globalCallback(null, fmtMsg); })();
280
+ } else {
281
+ if (v.delta.attachments[i].mercury.attach_type == "photo") {
282
+ api.resolvePhotoUrl(
283
+ v.delta.attachments[i].fbid,
284
+ (err, url) => {
285
+ if (!err)
286
+ v.delta.attachments[
287
+ i
288
+ ].mercury.metadata.url = url;
289
+ return resolveAttachmentUrl(i + 1);
290
+ }
291
+ );
292
+ } else {
293
+ return resolveAttachmentUrl(i + 1);
294
+ }
295
+ }
296
+ })(0);
297
+ }
298
+
299
+ if (v.delta.class == "ClientPayload") {
300
+ const clientPayload = utils.decodeClientPayload(
301
+ v.delta.payload
302
+ );
303
+
304
+ if (clientPayload && clientPayload.deltas) {
305
+ for (const i in clientPayload.deltas) {
306
+ const delta = clientPayload.deltas[i];
307
+ if (delta.deltaMessageReaction && !!ctx.globalOptions.listenEvents) {
308
+ (function () {
309
+ globalCallback(null, {
310
+ type: "message_reaction",
311
+ threadID: (delta.deltaMessageReaction.threadKey
312
+ .threadFbId ?
313
+ delta.deltaMessageReaction.threadKey.threadFbId : delta.deltaMessageReaction.threadKey
314
+ .otherUserFbId).toString(),
315
+ messageID: delta.deltaMessageReaction.messageId,
316
+ reaction: delta.deltaMessageReaction.reaction,
317
+ senderID: delta.deltaMessageReaction.senderId == 0 ? delta.deltaMessageReaction.userId.toString() : delta.deltaMessageReaction.senderId.toString(),
318
+ userID: (delta.deltaMessageReaction.userId || delta.deltaMessageReaction.senderId).toString()
319
+ });
320
+ })();
321
+ } else if (delta.deltaRecallMessageData && !!ctx.globalOptions.listenEvents) {
322
+ (function () {
323
+ globalCallback(null, {
324
+ type: "message_unsend",
325
+ threadID: (delta.deltaRecallMessageData.threadKey.threadFbId ?
326
+ delta.deltaRecallMessageData.threadKey.threadFbId : delta.deltaRecallMessageData.threadKey
327
+ .otherUserFbId).toString(),
328
+ messageID: delta.deltaRecallMessageData.messageID,
329
+ senderID: delta.deltaRecallMessageData.senderID.toString(),
330
+ deletionTimestamp: delta.deltaRecallMessageData.deletionTimestamp,
331
+ timestamp: delta.deltaRecallMessageData.timestamp
332
+ });
333
+ })();
334
+ } else if (delta.deltaRemoveMessage && !!ctx.globalOptions.listenEvents) {
335
+ (function () {
336
+ globalCallback(null, {
337
+ type: "message_self_delete",
338
+ threadID: (delta.deltaRemoveMessage.threadKey.threadFbId ?
339
+ delta.deltaRemoveMessage.threadKey.threadFbId : delta.deltaRemoveMessage.threadKey
340
+ .otherUserFbId).toString(),
341
+ messageID: delta.deltaRemoveMessage.messageIds.length == 1 ? delta.deltaRemoveMessage.messageIds[0] : delta.deltaRemoveMessage.messageIds,
342
+ senderID: api.getCurrentUserID(),
343
+ deletionTimestamp: delta.deltaRemoveMessage.deletionTimestamp,
344
+ timestamp: delta.deltaRemoveMessage.timestamp
345
+ });
346
+ })();
347
+ }
348
+ else if (delta.deltaMessageReply) {
349
+ //Mention block - #1
350
+ let mdata =
351
+ delta.deltaMessageReply.message === undefined ? [] :
352
+ delta.deltaMessageReply.message.data === undefined ? [] :
353
+ delta.deltaMessageReply.message.data.prng === undefined ? [] :
354
+ JSON.parse(delta.deltaMessageReply.message.data.prng);
355
+ let m_id = mdata.map(u => u.i);
356
+ let m_offset = mdata.map(u => u.o);
357
+ let m_length = mdata.map(u => u.l);
358
+
359
+ const mentions = {};
360
+
361
+ for (let i = 0; i < m_id.length; i++) {
362
+ mentions[m_id[i]] = (delta.deltaMessageReply.message.body || "").substring(
363
+ m_offset[i],
364
+ m_offset[i] + m_length[i]
365
+ );
366
+ }
367
+ //Mention block - 1#
368
+ const callbackToReturn = {
369
+ type: "message_reply",
370
+ threadID: (delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId ?
371
+ delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.message.messageMetadata.threadKey
372
+ .otherUserFbId).toString(),
373
+ messageID: delta.deltaMessageReply.message.messageMetadata.messageId,
374
+ senderID: delta.deltaMessageReply.message.messageMetadata.actorFbId.toString(),
375
+ attachments: (delta.deltaMessageReply.message.attachments || []).map(function (att) {
376
+ const mercury = JSON.parse(att.mercuryJSON);
377
+ Object.assign(att, mercury);
378
+ return att;
379
+ }).map(att => {
380
+ let x;
381
+ try {
382
+ x = utils._formatAttachment(att);
383
+ } catch (ex) {
384
+ x = att;
385
+ x.error = ex;
386
+ x.type = "unknown";
387
+ }
388
+ return x;
389
+ }),
390
+ body: delta.deltaMessageReply.message.body || "",
391
+ isGroup: !!delta.deltaMessageReply.message.messageMetadata.threadKey.threadFbId,
392
+ mentions: mentions,
393
+ timestamp: delta.deltaMessageReply.message.messageMetadata.timestamp,
394
+ participantIDs: (delta.deltaMessageReply.message.messageMetadata.cid.canonicalParticipantFbids || delta.deltaMessageReply.message.participants || []).map(e => e.toString())
395
+ };
396
+
397
+ if (delta.deltaMessageReply.repliedToMessage) {
398
+ //Mention block - #2
399
+ mdata =
400
+ delta.deltaMessageReply.repliedToMessage === undefined ? [] :
401
+ delta.deltaMessageReply.repliedToMessage.data === undefined ? [] :
402
+ delta.deltaMessageReply.repliedToMessage.data.prng === undefined ? [] :
403
+ JSON.parse(delta.deltaMessageReply.repliedToMessage.data.prng);
404
+ m_id = mdata.map(u => u.i);
405
+ m_offset = mdata.map(u => u.o);
406
+ m_length = mdata.map(u => u.l);
407
+
408
+ const rmentions = {};
409
+
410
+ for (let i = 0; i < m_id.length; i++) {
411
+ rmentions[m_id[i]] = (delta.deltaMessageReply.repliedToMessage.body || "").substring(
412
+ m_offset[i],
413
+ m_offset[i] + m_length[i]
414
+ );
415
+ }
416
+ //Mention block - 2#
417
+ callbackToReturn.messageReply = {
418
+ threadID: (delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId ?
419
+ delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId : delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey
420
+ .otherUserFbId).toString(),
421
+ messageID: delta.deltaMessageReply.repliedToMessage.messageMetadata.messageId,
422
+ senderID: delta.deltaMessageReply.repliedToMessage.messageMetadata.actorFbId.toString(),
423
+ attachments: delta.deltaMessageReply.repliedToMessage.attachments.map(function (att) {
424
+ const mercury = JSON.parse(att.mercuryJSON);
425
+ Object.assign(att, mercury);
426
+ return att;
427
+ }).map(att => {
428
+ let x;
429
+ try {
430
+ x = utils._formatAttachment(att);
431
+ } catch (ex) {
432
+ x = att;
433
+ x.error = ex;
434
+ x.type = "unknown";
435
+ }
436
+ return x;
437
+ }),
438
+ body: delta.deltaMessageReply.repliedToMessage.body || "",
439
+ isGroup: !!delta.deltaMessageReply.repliedToMessage.messageMetadata.threadKey.threadFbId,
440
+ mentions: rmentions,
441
+ timestamp: delta.deltaMessageReply.repliedToMessage.messageMetadata.timestamp
442
+ };
443
+ } else if (delta.deltaMessageReply.replyToMessageId) {
444
+ return defaultFuncs
445
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, {
446
+ "av": ctx.globalOptions.pageID,
447
+ "queries": JSON.stringify({
448
+ "o0": {
449
+ //Using the same doc_id as forcedFetch
450
+ "doc_id": "2848441488556444",
451
+ "query_params": {
452
+ "thread_and_message_id": {
453
+ "thread_id": callbackToReturn.threadID,
454
+ "message_id": delta.deltaMessageReply.replyToMessageId.id
455
+ }
456
+ }
457
+ }
458
+ })
459
+ })
460
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
461
+ .then((resData) => {
462
+ if (resData[resData.length - 1].error_results > 0) {
463
+ throw resData[0].o0.errors;
464
+ }
465
+
466
+ if (resData[resData.length - 1].successful_results === 0) {
467
+ throw { error: "forcedFetch: there was no successful_results", res: resData };
468
+ }
469
+
470
+ const fetchData = resData[0].o0.data.message;
471
+
472
+ const mobj = {};
473
+ for (const n in fetchData.message.ranges) {
474
+ mobj[fetchData.message.ranges[n].entity.id] = (fetchData.message.text || "").substr(fetchData.message.ranges[n].offset, fetchData.message.ranges[n].length);
475
+ }
476
+
477
+ callbackToReturn.messageReply = {
478
+ threadID: callbackToReturn.threadID,
479
+ messageID: fetchData.message_id,
480
+ senderID: fetchData.message_sender.id.toString(),
481
+ attachments: fetchData.message.blob_attachment.map(att => {
482
+ let x;
483
+ try {
484
+ x = utils._formatAttachment({
485
+ blob_attachment: att
486
+ });
487
+ } catch (ex) {
488
+ x = att;
489
+ x.error = ex;
490
+ x.type = "unknown";
491
+ }
492
+ return x;
493
+ }),
494
+ body: fetchData.message.text || "",
495
+ isGroup: callbackToReturn.isGroup,
496
+ mentions: mobj,
497
+ timestamp: parseInt(fetchData.timestamp_precise)
498
+ };
499
+ })
500
+ .catch((err) => {
501
+ log.error("forcedFetch", err);
502
+ })
503
+ .finally(function () {
504
+ if (ctx.globalOptions.autoMarkDelivery) {
505
+ markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
506
+ }
507
+ !ctx.globalOptions.selfListen &&
508
+ (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ?
509
+ undefined :
510
+ (function () { globalCallback(null, callbackToReturn); })();
511
+ });
512
+ } else {
513
+ callbackToReturn.delta = delta;
514
+ }
515
+
516
+ if (ctx.globalOptions.autoMarkDelivery) {
517
+ markDelivery(ctx, api, callbackToReturn.threadID, callbackToReturn.messageID);
518
+ }
519
+
520
+ return !ctx.globalOptions.selfListen &&
521
+ (callbackToReturn.senderID === ctx.i_userID || callbackToReturn.senderID === ctx.userID) ?
522
+ undefined :
523
+ (function () { globalCallback(null, callbackToReturn); })();
524
+ }
525
+ }
526
+ return;
527
+ }
528
+ }
529
+
530
+ if (v.delta.class !== "NewMessage" &&
531
+ !ctx.globalOptions.listenEvents
532
+ )
533
+ return;
534
+
535
+ switch (v.delta.class) {
536
+ case "ReadReceipt":
537
+ var fmtMsg;
538
+ try {
539
+ fmtMsg = utils.formatDeltaReadReceipt(v.delta);
540
+ }
541
+ catch (err) {
542
+ return globalCallback({
543
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
544
+ detail: err,
545
+ res: v.delta,
546
+ type: "parse_error"
547
+ });
548
+ }
549
+ return (function () { globalCallback(null, fmtMsg); })();
550
+ case "AdminTextMessage":
551
+ switch (v.delta.type) {
552
+ case "change_thread_theme":
553
+ case "change_thread_nickname":
554
+ case "change_thread_icon":
555
+ case "change_thread_quick_reaction":
556
+ case "change_thread_admins":
557
+ case "group_poll":
558
+ case "joinable_group_link_mode_change":
559
+ case "magic_words":
560
+ case "change_thread_approval_mode":
561
+ case "messenger_call_log":
562
+ case "participant_joined_group_call":
563
+ var fmtMsg;
564
+ try {
565
+ fmtMsg = utils.formatDeltaEvent(v.delta);
566
+ }
567
+ catch (err) {
568
+ return globalCallback({
569
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
570
+ detail: err,
571
+ res: v.delta,
572
+ type: "parse_error"
573
+ });
574
+ }
575
+ return (function () { globalCallback(null, fmtMsg); })();
576
+ default:
577
+ return;
578
+ }
579
+ //For group images
580
+ case "ForcedFetch":
581
+ if (!v.delta.threadKey) return;
582
+ var mid = v.delta.messageId;
583
+ var tid = v.delta.threadKey.threadFbId;
584
+ if (mid && tid) {
585
+ const form = {
586
+ "av": ctx.globalOptions.pageID,
587
+ "queries": JSON.stringify({
588
+ "o0": {
589
+ //This doc_id is valid as of March 25, 2020
590
+ "doc_id": "2848441488556444",
591
+ "query_params": {
592
+ "thread_and_message_id": {
593
+ "thread_id": tid.toString(),
594
+ "message_id": mid
595
+ }
596
+ }
597
+ }
598
+ })
599
+ };
600
+
601
+ defaultFuncs
602
+ .post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
603
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
604
+ .then((resData) => {
605
+ if (resData[resData.length - 1].error_results > 0) {
606
+ throw resData[0].o0.errors;
607
+ }
608
+
609
+ if (resData[resData.length - 1].successful_results === 0) {
610
+ throw { error: "forcedFetch: there was no successful_results", res: resData };
611
+ }
612
+
613
+ const fetchData = resData[0].o0.data.message;
614
+
615
+ if (utils.getType(fetchData) == "Object") {
616
+ log.info("forcedFetch", fetchData);
617
+ switch (fetchData.__typename) {
618
+ case "ThreadImageMessage":
619
+ (!ctx.globalOptions.selfListenEvent && (fetchData.message_sender.id.toString() === ctx.i_userID || fetchData.message_sender.id.toString() === ctx.userID)) || !ctx.loggedIn ?
620
+ undefined :
621
+ (function () {
622
+ globalCallback(null, {
623
+ type: "event",
624
+ threadID: utils.formatID(tid.toString()),
625
+ messageID: fetchData.message_id,
626
+ logMessageType: "log:thread-image",
627
+ logMessageData: {
628
+ attachmentID: fetchData.image_with_metadata && fetchData.image_with_metadata.legacy_attachment_id,
629
+ width: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.x,
630
+ height: fetchData.image_with_metadata && fetchData.image_with_metadata.original_dimensions.y,
631
+ url: fetchData.image_with_metadata && fetchData.image_with_metadata.preview.uri
632
+ },
633
+ logMessageBody: fetchData.snippet,
634
+ timestamp: fetchData.timestamp_precise,
635
+ author: fetchData.message_sender.id
636
+ });
637
+ })();
638
+ break;
639
+ case "UserMessage":
640
+ log.info("ff-Return", {
641
+ type: "message",
642
+ senderID: utils.formatID(fetchData.message_sender.id),
643
+ body: fetchData.message.text || "",
644
+ threadID: utils.formatID(tid.toString()),
645
+ messageID: fetchData.message_id,
646
+ attachments: [{
647
+ type: "share",
648
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
649
+ url: fetchData.extensible_attachment.story_attachment.url,
650
+
651
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
652
+ description: fetchData.extensible_attachment.story_attachment.description.text,
653
+ source: fetchData.extensible_attachment.story_attachment.source,
654
+
655
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
656
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
657
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
658
+ playable: (fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false,
659
+ duration: (fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
660
+
661
+ subattachments: fetchData.extensible_attachment.subattachments,
662
+ properties: fetchData.extensible_attachment.story_attachment.properties
663
+ }],
664
+ mentions: {},
665
+ timestamp: parseInt(fetchData.timestamp_precise),
666
+ participantIDs: (fetchData.participants || (fetchData.messageMetadata ? fetchData.messageMetadata.cid ? fetchData.messageMetadata.cid.canonicalParticipantFbids : fetchData.messageMetadata.participantIds : []) || []),
667
+ isGroup: (fetchData.message_sender.id != tid.toString())
668
+ });
669
+ globalCallback(null, {
670
+ type: "message",
671
+ senderID: utils.formatID(fetchData.message_sender.id),
672
+ body: fetchData.message.text || "",
673
+ threadID: utils.formatID(tid.toString()),
674
+ messageID: fetchData.message_id,
675
+ attachments: [{
676
+ type: "share",
677
+ ID: fetchData.extensible_attachment.legacy_attachment_id,
678
+ url: fetchData.extensible_attachment.story_attachment.url,
679
+
680
+ title: fetchData.extensible_attachment.story_attachment.title_with_entities.text,
681
+ description: fetchData.extensible_attachment.story_attachment.description.text,
682
+ source: fetchData.extensible_attachment.story_attachment.source,
683
+
684
+ image: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).uri,
685
+ width: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).width,
686
+ height: ((fetchData.extensible_attachment.story_attachment.media || {}).image || {}).height,
687
+ playable: (fetchData.extensible_attachment.story_attachment.media || {}).is_playable || false,
688
+ duration: (fetchData.extensible_attachment.story_attachment.media || {}).playable_duration_in_ms || 0,
689
+
690
+ subattachments: fetchData.extensible_attachment.subattachments,
691
+ properties: fetchData.extensible_attachment.story_attachment.properties
692
+ }],
693
+ mentions: {},
694
+ timestamp: parseInt(fetchData.timestamp_precise),
695
+ participantIDs: (fetchData.participants || (fetchData.messageMetadata ? fetchData.messageMetadata.cid ? fetchData.messageMetadata.cid.canonicalParticipantFbids : fetchData.messageMetadata.participantIds : []) || []),
696
+ isGroup: (fetchData.message_sender.id != tid.toString())
697
+ });
698
+ }
699
+ } else {
700
+ log.error("forcedFetch", fetchData);
701
+ }
702
+ })
703
+ .catch((err) => {
704
+ log.error("forcedFetch", err);
705
+ });
706
+ }
707
+ break;
708
+ case "ThreadName":
709
+ case "ParticipantsAddedToGroupThread":
710
+ case "ParticipantLeftGroupThread":
711
+ case "ApprovalQueue":
712
+ var formattedEvent;
713
+ try {
714
+ formattedEvent = utils.formatDeltaEvent(v.delta);
715
+ } catch (err) {
716
+ return globalCallback({
717
+ error: "Problem parsing message object. Please open an issue at https://github.com/ntkhang03/fb-chat-api/issues.",
718
+ detail: err,
719
+ res: v.delta,
720
+ type: "parse_error"
721
+ });
722
+ }
723
+ return (!ctx.globalOptions.selfListenEvent && (formattedEvent.author.toString() === ctx.i_userID || formattedEvent.author.toString() === ctx.userID)) || !ctx.loggedIn ?
724
+ undefined :
725
+ (function () { globalCallback(null, formattedEvent); })();
726
+ }
727
+ }
728
+
729
+ function markDelivery(ctx, api, threadID, messageID) {
730
+ if (threadID && messageID) {
731
+ api.markAsDelivered(threadID, messageID, (err) => {
732
+ if (err) {
733
+ log.error("markAsDelivered", err);
734
+ } else {
735
+ if (ctx.globalOptions.autoMarkRead) {
736
+ api.markAsRead(threadID, (err) => {
737
+ if (err) {
738
+ log.error("markAsDelivered", err);
739
+ }
740
+ });
741
+ }
742
+ }
743
+ });
744
+ }
745
+ }
746
+
747
+ function getSeqId(defaultFuncs, api, ctx, globalCallback) {
748
+ const jar = ctx.jar;
749
+ utils
750
+ .get('https://www.facebook.com/', jar, null, ctx.globalOptions, { noRef: true })
751
+ .then(utils.saveCookies(jar))
752
+ .then(function (resData) {
753
+ const html = resData.body;
754
+ const oldFBMQTTMatch = html.match(/irisSeqID:"(.+?)",appID:219994525426954,endpoint:"(.+?)"/);
755
+ let mqttEndpoint = null;
756
+ let region = null;
757
+ let irisSeqID = null;
758
+ let noMqttData = null;
759
+
760
+ if (oldFBMQTTMatch) {
761
+ irisSeqID = oldFBMQTTMatch[1];
762
+ mqttEndpoint = oldFBMQTTMatch[2];
763
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
764
+ log.info("login", `Got this account's message region: ${region}`);
765
+ } else {
766
+ const newFBMQTTMatch = html.match(/{"app_id":"219994525426954","endpoint":"(.+?)","iris_seq_id":"(.+?)"}/);
767
+ if (newFBMQTTMatch) {
768
+ irisSeqID = newFBMQTTMatch[2];
769
+ mqttEndpoint = newFBMQTTMatch[1].replace(/\\\//g, "/");
770
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
771
+ log.info("login", `Got this account's message region: ${region}`);
772
+ } else {
773
+ const legacyFBMQTTMatch = html.match(/(\["MqttWebConfig",\[\],{fbid:")(.+?)(",appID:219994525426954,endpoint:")(.+?)(",pollingEndpoint:")(.+?)(3790])/);
774
+ if (legacyFBMQTTMatch) {
775
+ mqttEndpoint = legacyFBMQTTMatch[4];
776
+ region = new URL(mqttEndpoint).searchParams.get("region").toUpperCase();
777
+ log.warn("login", `Cannot get sequence ID with new RegExp. Fallback to old RegExp (without seqID)...`);
778
+ log.info("login", `Got this account's message region: ${region}`);
779
+ log.info("login", `[Unused] Polling endpoint: ${legacyFBMQTTMatch[6]}`);
780
+ } else {
781
+ log.warn("login", "Cannot get MQTT region & sequence ID.");
782
+ noMqttData = html;
783
+ }
784
+ }
785
+ }
786
+
787
+ ctx.lastSeqId = irisSeqID;
788
+ ctx.mqttEndpoint = mqttEndpoint;
789
+ ctx.region = region;
790
+ if (noMqttData) {
791
+ api["htmlData"] = noMqttData;
792
+ }
793
+
794
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
795
+ })
796
+ .catch(function (err) {
797
+ log.error("getSeqId", err);
798
+ });
799
+ }
800
+
801
+ module.exports = function (defaultFuncs, api, ctx) {
802
+ let globalCallback = identity;
803
+
804
+ return function (callback) {
805
+ class MessageEmitter extends EventEmitter {
806
+ stopListening(callback) {
807
+
808
+ callback = callback || (() => { });
809
+ globalCallback = identity;
810
+ if (ctx.mqttClient) {
811
+ ctx.mqttClient.unsubscribe("/webrtc");
812
+ ctx.mqttClient.unsubscribe("/rtc_multi");
813
+ ctx.mqttClient.unsubscribe("/onevc");
814
+ ctx.mqttClient.publish("/browser_close", "{}");
815
+ ctx.mqttClient.end(false, function (...data) {
816
+ callback(data);
817
+ ctx.mqttClient = undefined;
818
+ });
819
+ }
820
+ }
821
+
822
+ async stopListeningAsync() {
823
+ return new Promise((resolve) => {
824
+ this.stopListening(resolve);
825
+ });
826
+ }
827
+ }
828
+
829
+ const msgEmitter = new MessageEmitter();
830
+ globalCallback = (callback || function (error, message) {
831
+ if (error) {
832
+ return msgEmitter.emit("error", error);
833
+ }
834
+ msgEmitter.emit("message", message);
835
+ });
836
+
837
+ // Reset some stuff
838
+ if (!ctx.firstListen)
839
+ ctx.lastSeqId = null;
840
+ ctx.syncToken = undefined;
841
+ ctx.t_mqttCalled = false;
842
+
843
+ if (!ctx.firstListen || !ctx.lastSeqId) {
844
+ getSeqId(defaultFuncs, api, ctx, globalCallback);
845
+ } else {
846
+ listenMqtt(defaultFuncs, api, ctx, globalCallback);
847
+ }
848
+
849
+ api.stopListening = msgEmitter.stopListening;
850
+ api.stopListeningAsync = msgEmitter.stopListeningAsync;
851
+ return msgEmitter;
852
+ };
853
+ };