fca-orion-api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ };