@dongdev/fca-unofficial 0.0.3 → 0.0.5

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 (76) hide show
  1. package/.travis.yml +6 -6
  2. package/CHANGELOG.md +1 -1
  3. package/DOCS.md +1738 -1738
  4. package/LICENSE-MIT +21 -21
  5. package/README.md +219 -219
  6. package/index.js +267 -569
  7. package/lib/login.js +0 -0
  8. package/package.json +3 -3
  9. package/src/addExternalModule.js +19 -15
  10. package/src/addUserToGroup.js +113 -77
  11. package/src/changeAdminStatus.js +79 -47
  12. package/src/changeArchivedStatus.js +55 -41
  13. package/src/changeAvatar.js +126 -0
  14. package/src/changeBio.js +66 -54
  15. package/src/changeBlockedStatus.js +40 -29
  16. package/src/changeGroupImage.js +127 -101
  17. package/src/changeNickname.js +50 -36
  18. package/src/changeThreadColor.js +65 -61
  19. package/src/changeThreadEmoji.js +55 -41
  20. package/src/createNewGroup.js +86 -70
  21. package/src/createPoll.js +71 -59
  22. package/src/deleteMessage.js +56 -44
  23. package/src/deleteThread.js +56 -42
  24. package/src/forwardAttachment.js +60 -47
  25. package/src/getCurrentUserID.js +7 -7
  26. package/src/getEmojiUrl.js +29 -27
  27. package/src/getFriendsList.js +83 -73
  28. package/src/getMessage.js +796 -0
  29. package/src/getThreadHistory.js +666 -537
  30. package/src/getThreadInfo.js +232 -171
  31. package/src/getThreadList.js +241 -213
  32. package/src/getThreadPictures.js +79 -59
  33. package/src/getUserID.js +66 -61
  34. package/src/getUserInfo.js +74 -66
  35. package/src/handleFriendRequest.js +61 -46
  36. package/src/handleMessageRequest.js +65 -47
  37. package/src/httpGet.js +52 -44
  38. package/src/httpPost.js +52 -43
  39. package/src/httpPostFormData.js +63 -0
  40. package/src/listenMqtt.js +877 -709
  41. package/src/logout.js +62 -55
  42. package/src/markAsDelivered.js +58 -47
  43. package/src/markAsRead.js +80 -70
  44. package/src/markAsReadAll.js +49 -39
  45. package/src/markAsSeen.js +59 -48
  46. package/src/muteThread.js +52 -45
  47. package/src/postFormData.js +46 -0
  48. package/src/refreshFb_dtsg.js +81 -0
  49. package/src/removeUserFromGroup.js +79 -45
  50. package/src/resolvePhotoUrl.js +45 -36
  51. package/src/searchForThread.js +53 -42
  52. package/src/sendMessage.js +328 -328
  53. package/src/sendMessageMqtt.js +316 -0
  54. package/src/sendTypingIndicator.js +103 -70
  55. package/src/setMessageReaction.js +106 -98
  56. package/src/setPostReaction.js +102 -95
  57. package/src/setTitle.js +86 -70
  58. package/src/threadColors.js +131 -41
  59. package/src/unfriend.js +52 -42
  60. package/src/unsendMessage.js +49 -39
  61. package/src/uploadAttachment.js +95 -0
  62. package/utils.js +1470 -1196
  63. package/.gitattributes +0 -2
  64. package/src/Screenshot.js +0 -83
  65. package/src/changeAvt.js +0 -85
  66. package/src/getThreadHistoryDeprecated.js +0 -71
  67. package/src/getThreadInfoDeprecated.js +0 -56
  68. package/src/getThreadListDeprecated.js +0 -46
  69. package/src/shareContact.js +0 -46
  70. package/test/data/shareAttach.js +0 -146
  71. package/test/data/something.mov +0 -0
  72. package/test/data/test.png +0 -0
  73. package/test/data/test.txt +0 -7
  74. package/test/example-config.json +0 -18
  75. package/test/test-page.js +0 -140
  76. package/test/test.js +0 -385
package/utils.js CHANGED
@@ -1,1196 +1,1470 @@
1
- /* eslint-disable no-prototype-builtins */
2
- "use strict";
3
-
4
- var bluebird = require("bluebird");
5
- var request = bluebird.promisify(require("request").defaults({ jar: true }));
6
- var stream = require("stream");
7
- var log = require("npmlog");
8
- var querystring = require("querystring");
9
- var url = require("url");
10
-
11
- function setProxy(url) {
12
- if (typeof url == undefined) return request = bluebird.promisify(require("request").defaults({ jar: true }));
13
- return request = bluebird.promisify(require("request").defaults({ jar: true, proxy: url }));
14
- }
15
-
16
- function getHeaders(url, options, ctx, customHeader) {
17
- var headers = {
18
- "Content-Type": "application/x-www-form-urlencoded",
19
- Referer: "https://www.facebook.com/",
20
- Host: url.replace("https://", "").split("/")[0],
21
- Origin: "https://www.facebook.com",
22
- "User-Agent": options.userAgent,
23
- Connection: "keep-alive"
24
- };
25
- if (customHeader) Object.assign(headers, customHeader);
26
-
27
- if (ctx && ctx.region) headers["X-MSGR-Region"] = ctx.region;
28
-
29
- return headers;
30
- }
31
-
32
- function isReadableStream(obj) {
33
- return (
34
- obj instanceof stream.Stream &&
35
- (getType(obj._read) === "Function" ||
36
- getType(obj._read) === "AsyncFunction") &&
37
- getType(obj._readableState) === "Object"
38
- );
39
- }
40
-
41
- function get(url, jar, qs, options, ctx) {
42
- // I'm still confused about this
43
- if (getType(qs) === "Object")
44
- for (var prop in qs)
45
- if (qs.hasOwnProperty(prop) && getType(qs[prop]) === "Object") qs[prop] = JSON.stringify(qs[prop]);
46
- var op = {
47
- headers: getHeaders(url, options, ctx),
48
- timeout: 60000,
49
- qs: qs,
50
- url: url,
51
- method: "GET",
52
- jar: jar,
53
- gzip: true
54
- };
55
-
56
- return request(op).then(function (res) {
57
- return res[0];
58
- });
59
- }
60
-
61
- function post(url, jar, form, options, ctx, customHeader) {
62
- var op = {
63
- headers: getHeaders(url, options, ctx, customHeader),
64
- timeout: 60000,
65
- url: url,
66
- method: "POST",
67
- form: form,
68
- jar: jar,
69
- gzip: true
70
- };
71
-
72
- return request(op).then(function (res) {
73
- return res[0];
74
- });
75
- }
76
-
77
- function postFormData(url, jar, form, qs, options, ctx) {
78
- var headers = getHeaders(url, options, ctx);
79
- headers["Content-Type"] = "multipart/form-data";
80
- var op = {
81
- headers: headers,
82
- timeout: 60000,
83
- url: url,
84
- method: "POST",
85
- formData: form,
86
- qs: qs,
87
- jar: jar,
88
- gzip: true
89
- };
90
-
91
- return request(op).then(function (res) {
92
- return res[0];
93
- });
94
- }
95
-
96
- function padZeros(val, len) {
97
- val = String(val);
98
- len = len || 2;
99
- while (val.length < len) val = "0" + val;
100
- return val;
101
- }
102
-
103
- function generateThreadingID(clientID) {
104
- var k = Date.now();
105
- var l = Math.floor(Math.random() * 4294967295);
106
- var m = clientID;
107
- return "<" + k + ":" + l + "-" + m + "@mail.projektitan.com>";
108
- }
109
-
110
- function binaryToDecimal(data) {
111
- var ret = "";
112
- while (data !== "0") {
113
- var end = 0;
114
- var fullName = "";
115
- var i = 0;
116
- for (; i < data.length; i++) {
117
- end = 2 * end + parseInt(data[i], 10);
118
- if (end >= 10) {
119
- fullName += "1";
120
- end -= 10;
121
- }
122
- else fullName += "0";
123
- }
124
- ret = end.toString() + ret;
125
- data = fullName.slice(fullName.indexOf("1"));
126
- }
127
- return ret;
128
- }
129
-
130
- function generateOfflineThreadingID() {
131
- var ret = Date.now();
132
- var value = Math.floor(Math.random() * 4294967295);
133
- var str = ("0000000000000000000000" + value.toString(2)).slice(-22);
134
- var msgs = ret.toString(2) + str;
135
- return binaryToDecimal(msgs);
136
- }
137
-
138
- var h;
139
- var i = {};
140
- var j = {
141
- _: "%",
142
- A: "%2",
143
- B: "000",
144
- C: "%7d",
145
- D: "%7b%22",
146
- E: "%2c%22",
147
- F: "%22%3a",
148
- G: "%2c%22ut%22%3a1",
149
- H: "%2c%22bls%22%3a",
150
- I: "%2c%22n%22%3a%22%",
151
- J: "%22%3a%7b%22i%22%3a0%7d",
152
- K: "%2c%22pt%22%3a0%2c%22vis%22%3a",
153
- L: "%2c%22ch%22%3a%7b%22h%22%3a%22",
154
- M: "%7b%22v%22%3a2%2c%22time%22%3a1",
155
- N: ".channel%22%2c%22sub%22%3a%5b",
156
- O: "%2c%22sb%22%3a1%2c%22t%22%3a%5b",
157
- P: "%2c%22ud%22%3a100%2c%22lc%22%3a0",
158
- Q: "%5d%2c%22f%22%3anull%2c%22uct%22%3a",
159
- R: ".channel%22%2c%22sub%22%3a%5b1%5d",
160
- S: "%22%2c%22m%22%3a0%7d%2c%7b%22i%22%3a",
161
- T: "%2c%22blc%22%3a1%2c%22snd%22%3a1%2c%22ct%22%3a",
162
- U: "%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
163
- V: "%2c%22blc%22%3a0%2c%22snd%22%3a0%2c%22ct%22%3a",
164
- W: "%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a",
165
- X: "%2c%22ri%22%3a0%7d%2c%22state%22%3a%7b%22p%22%3a0%2c%22ut%22%3a1",
166
- Y: "%2c%22pt%22%3a0%2c%22vis%22%3a1%2c%22bls%22%3a0%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
167
- Z: "%2c%22sb%22%3a1%2c%22t%22%3a%5b%5d%2c%22f%22%3anull%2c%22uct%22%3a0%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a"
168
- };
169
- (function () {
170
- var l = [];
171
- for (var m in j) {
172
- i[j[m]] = m;
173
- l.push(j[m]);
174
- }
175
- l.reverse();
176
- h = new RegExp(l.join("|"), "g");
177
- })();
178
-
179
- function presenceEncode(str) {
180
- return encodeURIComponent(str)
181
- .replace(/([_A-Z])|%../g, function (m, n) {
182
- return n ? "%" + n.charCodeAt(0).toString(16) : m;
183
- })
184
- .toLowerCase()
185
- .replace(h, function (m) {
186
- return i[m];
187
- });
188
- }
189
-
190
- // eslint-disable-next-line no-unused-vars
191
- function presenceDecode(str) {
192
- return decodeURIComponent(
193
- str.replace(/[_A-Z]/g, function (m) {
194
- return j[m];
195
- })
196
- );
197
- }
198
-
199
- function generatePresence(userID) {
200
- var time = Date.now();
201
- return (
202
- "E" +
203
- presenceEncode(
204
- JSON.stringify({
205
- v: 3,
206
- time: parseInt(time / 1000, 10),
207
- user: userID,
208
- state: {
209
- ut: 0,
210
- t2: [],
211
- lm2: null,
212
- uct2: time,
213
- tr: null,
214
- tw: Math.floor(Math.random() * 4294967295) + 1,
215
- at: time
216
- },
217
- ch: { ["p_" + userID]: 0 }
218
- })
219
- )
220
- );
221
- }
222
-
223
- function generateAccessiblityCookie() {
224
- var time = Date.now();
225
- return encodeURIComponent(
226
- JSON.stringify({
227
- sr: 0,
228
- "sr-ts": time,
229
- jk: 0,
230
- "jk-ts": time,
231
- kb: 0,
232
- "kb-ts": time,
233
- hcm: 0,
234
- "hcm-ts": time
235
- })
236
- );
237
- }
238
-
239
- function getGUID() {
240
- /** @type {number} */
241
- var sectionLength = Date.now();
242
- /** @type {string} */
243
- var id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
244
- /** @type {number} */
245
- var r = Math.floor((sectionLength + Math.random() * 16) % 16);
246
- /** @type {number} */
247
- sectionLength = Math.floor(sectionLength / 16);
248
- /** @type {string} */
249
- var _guid = (c == "x" ? r : (r & 7) | 8).toString(16);
250
- return _guid;
251
- });
252
- return id;
253
- }
254
-
255
- function _formatAttachment(attachment1, attachment2) {
256
- // TODO: THIS IS REALLY BAD
257
- // This is an attempt at fixing Facebook's inconsistencies. Sometimes they give us
258
- // two attachment objects, but sometimes only one. They each contain part of the
259
- // data that you'd want so we merge them for convenience.
260
- // Instead of having a bunch of if statements guarding every access to image_data,
261
- // we set it to empty object and use the fact that it'll return undefined.
262
- attachment2 = attachment2 || { id: "", image_data: {} };
263
- attachment1 = attachment1.mercury ? attachment1.mercury : attachment1;
264
- var blob = attachment1.blob_attachment;
265
- var type =
266
- blob && blob.__typename ? blob.__typename : attachment1.attach_type;
267
- if (!type && attachment1.sticker_attachment) {
268
- type = "StickerAttachment";
269
- blob = attachment1.sticker_attachment;
270
- }
271
- else if (!type && attachment1.extensible_attachment) {
272
- if (attachment1.extensible_attachment.story_attachment && attachment1.extensible_attachment.story_attachment.target && attachment1.extensible_attachment.story_attachment.target.__typename && attachment1.extensible_attachment.story_attachment.target.__typename === "MessageLocation") type = "MessageLocation";
273
- else type = "ExtensibleAttachment";
274
-
275
- blob = attachment1.extensible_attachment;
276
- }
277
- // TODO: Determine whether "sticker", "photo", "file" etc are still used
278
- // KEEP IN SYNC WITH getThreadHistory
279
- switch (type) {
280
- case "sticker":
281
- return {
282
- type: "sticker",
283
- ID: attachment1.metadata.stickerID.toString(),
284
- url: attachment1.url,
285
-
286
- packID: attachment1.metadata.packID.toString(),
287
- spriteUrl: attachment1.metadata.spriteURI,
288
- spriteUrl2x: attachment1.metadata.spriteURI2x,
289
- width: attachment1.metadata.width,
290
- height: attachment1.metadata.height,
291
-
292
- caption: attachment2.caption,
293
- description: attachment2.description,
294
-
295
- frameCount: attachment1.metadata.frameCount,
296
- frameRate: attachment1.metadata.frameRate,
297
- framesPerRow: attachment1.metadata.framesPerRow,
298
- framesPerCol: attachment1.metadata.framesPerCol,
299
-
300
- stickerID: attachment1.metadata.stickerID.toString(), // @Legacy
301
- spriteURI: attachment1.metadata.spriteURI, // @Legacy
302
- spriteURI2x: attachment1.metadata.spriteURI2x // @Legacy
303
- };
304
- case "file":
305
- return {
306
- type: "file",
307
- filename: attachment1.name,
308
- ID: attachment2.id.toString(),
309
- url: attachment1.url,
310
-
311
- isMalicious: attachment2.is_malicious,
312
- contentType: attachment2.mime_type,
313
-
314
- name: attachment1.name, // @Legacy
315
- mimeType: attachment2.mime_type, // @Legacy
316
- fileSize: attachment2.file_size // @Legacy
317
- };
318
- case "photo":
319
- return {
320
- type: "photo",
321
- ID: attachment1.metadata.fbid.toString(),
322
- filename: attachment1.fileName,
323
- thumbnailUrl: attachment1.thumbnail_url,
324
-
325
- previewUrl: attachment1.preview_url,
326
- previewWidth: attachment1.preview_width,
327
- previewHeight: attachment1.preview_height,
328
-
329
- largePreviewUrl: attachment1.large_preview_url,
330
- largePreviewWidth: attachment1.large_preview_width,
331
- largePreviewHeight: attachment1.large_preview_height,
332
-
333
- url: attachment1.metadata.url, // @Legacy
334
- width: attachment1.metadata.dimensions.split(",")[0], // @Legacy
335
- height: attachment1.metadata.dimensions.split(",")[1], // @Legacy
336
- name: attachment1.fileName // @Legacy
337
- };
338
- case "animated_image":
339
- return {
340
- type: "animated_image",
341
- ID: attachment2.id.toString(),
342
- filename: attachment2.filename,
343
-
344
- previewUrl: attachment1.preview_url,
345
- previewWidth: attachment1.preview_width,
346
- previewHeight: attachment1.preview_height,
347
-
348
- url: attachment2.image_data.url,
349
- width: attachment2.image_data.width,
350
- height: attachment2.image_data.height,
351
-
352
- name: attachment1.name, // @Legacy
353
- facebookUrl: attachment1.url, // @Legacy
354
- thumbnailUrl: attachment1.thumbnail_url, // @Legacy
355
- mimeType: attachment2.mime_type, // @Legacy
356
- rawGifImage: attachment2.image_data.raw_gif_image, // @Legacy
357
- rawWebpImage: attachment2.image_data.raw_webp_image, // @Legacy
358
- animatedGifUrl: attachment2.image_data.animated_gif_url, // @Legacy
359
- animatedGifPreviewUrl: attachment2.image_data.animated_gif_preview_url, // @Legacy
360
- animatedWebpUrl: attachment2.image_data.animated_webp_url, // @Legacy
361
- animatedWebpPreviewUrl: attachment2.image_data.animated_webp_preview_url // @Legacy
362
- };
363
- case "share":
364
- return {
365
- type: "share",
366
- ID: attachment1.share.share_id.toString(),
367
- url: attachment2.href,
368
-
369
- title: attachment1.share.title,
370
- description: attachment1.share.description,
371
- source: attachment1.share.source,
372
-
373
- image: attachment1.share.media.image,
374
- width: attachment1.share.media.image_size.width,
375
- height: attachment1.share.media.image_size.height,
376
- playable: attachment1.share.media.playable,
377
- duration: attachment1.share.media.duration,
378
-
379
- subattachments: attachment1.share.subattachments,
380
- properties: {},
381
-
382
- animatedImageSize: attachment1.share.media.animated_image_size, // @Legacy
383
- facebookUrl: attachment1.share.uri, // @Legacy
384
- target: attachment1.share.target, // @Legacy
385
- styleList: attachment1.share.style_list // @Legacy
386
- };
387
- case "video":
388
- return {
389
- type: "video",
390
- ID: attachment1.metadata.fbid.toString(),
391
- filename: attachment1.name,
392
-
393
- previewUrl: attachment1.preview_url,
394
- previewWidth: attachment1.preview_width,
395
- previewHeight: attachment1.preview_height,
396
-
397
- url: attachment1.url,
398
- width: attachment1.metadata.dimensions.width,
399
- height: attachment1.metadata.dimensions.height,
400
-
401
- duration: attachment1.metadata.duration,
402
- videoType: "unknown",
403
-
404
- thumbnailUrl: attachment1.thumbnail_url // @Legacy
405
- };
406
- case "error":
407
- return {
408
- type: "error",
409
-
410
- // Save error attachments because we're unsure of their format,
411
- // and whether there are cases they contain something useful for debugging.
412
- attachment1: attachment1,
413
- attachment2: attachment2
414
- };
415
- case "MessageImage":
416
- return {
417
- type: "photo",
418
- ID: blob.legacy_attachment_id,
419
- filename: blob.filename,
420
- thumbnailUrl: blob.thumbnail.uri,
421
-
422
- previewUrl: blob.preview.uri,
423
- previewWidth: blob.preview.width,
424
- previewHeight: blob.preview.height,
425
-
426
- largePreviewUrl: blob.large_preview.uri,
427
- largePreviewWidth: blob.large_preview.width,
428
- largePreviewHeight: blob.large_preview.height,
429
-
430
- url: blob.large_preview.uri, // @Legacy
431
- width: blob.original_dimensions.x, // @Legacy
432
- height: blob.original_dimensions.y, // @Legacy
433
- name: blob.filename // @Legacy
434
- };
435
- case "MessageAnimatedImage":
436
- return {
437
- type: "animated_image",
438
- ID: blob.legacy_attachment_id,
439
- filename: blob.filename,
440
-
441
- previewUrl: blob.preview_image.uri,
442
- previewWidth: blob.preview_image.width,
443
- previewHeight: blob.preview_image.height,
444
-
445
- url: blob.animated_image.uri,
446
- width: blob.animated_image.width,
447
- height: blob.animated_image.height,
448
-
449
- thumbnailUrl: blob.preview_image.uri, // @Legacy
450
- name: blob.filename, // @Legacy
451
- facebookUrl: blob.animated_image.uri, // @Legacy
452
- rawGifImage: blob.animated_image.uri, // @Legacy
453
- animatedGifUrl: blob.animated_image.uri, // @Legacy
454
- animatedGifPreviewUrl: blob.preview_image.uri, // @Legacy
455
- animatedWebpUrl: blob.animated_image.uri, // @Legacy
456
- animatedWebpPreviewUrl: blob.preview_image.uri // @Legacy
457
- };
458
- case "MessageVideo":
459
- return {
460
- type: "video",
461
- filename: blob.filename,
462
- ID: blob.legacy_attachment_id,
463
-
464
- previewUrl: blob.large_image.uri,
465
- previewWidth: blob.large_image.width,
466
- previewHeight: blob.large_image.height,
467
-
468
- url: blob.playable_url,
469
- width: blob.original_dimensions.x,
470
- height: blob.original_dimensions.y,
471
-
472
- duration: blob.playable_duration_in_ms,
473
- videoType: blob.video_type.toLowerCase(),
474
-
475
- thumbnailUrl: blob.large_image.uri // @Legacy
476
- };
477
- case "MessageAudio":
478
- return {
479
- type: "audio",
480
- filename: blob.filename,
481
- ID: blob.url_shimhash,
482
-
483
- audioType: blob.audio_type,
484
- duration: blob.playable_duration_in_ms,
485
- url: blob.playable_url,
486
-
487
- isVoiceMail: blob.is_voicemail
488
- };
489
- case "StickerAttachment":
490
- return {
491
- type: "sticker",
492
- ID: blob.id,
493
- url: blob.url,
494
-
495
- packID: blob.pack ? blob.pack.id : null,
496
- spriteUrl: blob.sprite_image,
497
- spriteUrl2x: blob.sprite_image_2x,
498
- width: blob.width,
499
- height: blob.height,
500
-
501
- caption: blob.label,
502
- description: blob.label,
503
-
504
- frameCount: blob.frame_count,
505
- frameRate: blob.frame_rate,
506
- framesPerRow: blob.frames_per_row,
507
- framesPerCol: blob.frames_per_column,
508
-
509
- stickerID: blob.id, // @Legacy
510
- spriteURI: blob.sprite_image, // @Legacy
511
- spriteURI2x: blob.sprite_image_2x // @Legacy
512
- };
513
- case "MessageLocation":
514
- var urlAttach = blob.story_attachment.url;
515
- var mediaAttach = blob.story_attachment.media;
516
-
517
- var u = querystring.parse(url.parse(urlAttach).query).u;
518
- var where1 = querystring.parse(url.parse(u).query).where1;
519
- var address = where1.split(", ");
520
-
521
- var latitude;
522
- var longitude;
523
-
524
- try {
525
- latitude = Number.parseFloat(address[0]);
526
- longitude = Number.parseFloat(address[1]);
527
- }
528
- catch (err) {
529
- /* empty */
530
- }
531
-
532
- var imageUrl;
533
- var width;
534
- var height;
535
-
536
- if (mediaAttach && mediaAttach.image) {
537
- imageUrl = mediaAttach.image.uri;
538
- width = mediaAttach.image.width;
539
- height = mediaAttach.image.height;
540
- }
541
-
542
- return {
543
- type: "location",
544
- ID: blob.legacy_attachment_id,
545
- latitude: latitude,
546
- longitude: longitude,
547
- image: imageUrl,
548
- width: width,
549
- height: height,
550
- url: u || urlAttach,
551
- address: where1,
552
-
553
- facebookUrl: blob.story_attachment.url, // @Legacy
554
- target: blob.story_attachment.target, // @Legacy
555
- styleList: blob.story_attachment.style_list // @Legacy
556
- };
557
- case "ExtensibleAttachment":
558
- return {
559
- type: "share",
560
- ID: blob.legacy_attachment_id,
561
- url: blob.story_attachment.url,
562
-
563
- title: blob.story_attachment.title_with_entities.text,
564
- description: blob.story_attachment.description && blob.story_attachment.description.text,
565
- source: blob.story_attachment.source ? blob.story_attachment.source.text : null,
566
-
567
- image: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.uri,
568
- width: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.width,
569
- height: blob.story_attachment.media && blob.story_attachment.media.image && blob.story_attachment.media.image.height,
570
- playable: blob.story_attachment.media && blob.story_attachment.media.is_playable,
571
- duration: blob.story_attachment.media && blob.story_attachment.media.playable_duration_in_ms,
572
- playableUrl: blob.story_attachment.media == null ? null : blob.story_attachment.media.playable_url,
573
-
574
- subattachments: blob.story_attachment.subattachments,
575
- properties: blob.story_attachment.properties.reduce(function (obj, cur) {
576
- obj[cur.key] = cur.value.text;
577
- return obj;
578
- }, {}),
579
-
580
- facebookUrl: blob.story_attachment.url, // @Legacy
581
- target: blob.story_attachment.target, // @Legacy
582
- styleList: blob.story_attachment.style_list // @Legacy
583
- };
584
- case "MessageFile":
585
- return {
586
- type: "file",
587
- filename: blob.filename,
588
- ID: blob.message_file_fbid,
589
-
590
- url: blob.url,
591
- isMalicious: blob.is_malicious,
592
- contentType: blob.content_type,
593
-
594
- name: blob.filename,
595
- mimeType: "",
596
- fileSize: -1
597
- };
598
- default:
599
- throw new Error(`Unrecognized attach_file of type type\`JSON.stringify(attachment1, null, 4) attachment2: JSON.stringify(attachment2, null, 4)\``);
600
- }
601
- }
602
-
603
- function formatAttachment(attachments, attachmentIds, attachmentMap, shareMap) {
604
- attachmentMap = shareMap || attachmentMap;
605
- return attachments ?
606
- attachments.map(function (val, i) {
607
- if (!attachmentMap || !attachmentIds || !attachmentMap[attachmentIds[i]]) return _formatAttachment(val);
608
- return _formatAttachment(val, attachmentMap[attachmentIds[i]]);
609
- }) : [];
610
- }
611
-
612
- function formatDeltaMessage(m) {
613
- var md = m.delta.messageMetadata;
614
- var mdata = m.delta.data === undefined ? [] : m.delta.data.prng === undefined ? [] : JSON.parse(m.delta.data.prng);
615
- var m_id = mdata.map(u => u.i);
616
- var m_offset = mdata.map(u => u.o);
617
- var m_length = mdata.map(u => u.l);
618
- var mentions = {};
619
- var body = m.delta.body || "";
620
- var args = body == "" ? [] : body.trim().split(/\s+/);
621
- for (var i = 0; i < m_id.length; i++) mentions[m_id[i]] = m.delta.body.substring(m_offset[i], m_offset[i] + m_length[i]);
622
-
623
- return {
624
- type: "message",
625
- senderID: formatID(md.actorFbId.toString()),
626
- threadID: formatID((md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()),
627
- messageID: md.messageId,
628
- args: args,
629
- body: body,
630
- attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
631
- mentions: mentions,
632
- timestamp: md.timestamp,
633
- isGroup: !!md.threadKey.threadFbId,
634
- participantIDs: m.delta.participants || (md.cid ? md.cid.canonicalParticipantFbids : []) || []
635
- };
636
- }
637
-
638
- function formatID(id) {
639
- if (id != undefined && id != null) return id.replace(/(fb)?id[:.]/, "");
640
- else return id;
641
- }
642
-
643
- function formatMessage(m) {
644
- var originalMessage = m.message ? m.message : m;
645
- var obj = {
646
- type: "message",
647
- senderName: originalMessage.sender_name,
648
- senderID: formatID(originalMessage.sender_fbid.toString()),
649
- participantNames: originalMessage.group_thread_info ? originalMessage.group_thread_info.participant_names : [originalMessage.sender_name.split(" ")[0]],
650
- participantIDs: originalMessage.group_thread_info
651
- ? originalMessage.group_thread_info.participant_ids.map(function (v) {
652
- return formatID(v.toString());
653
- })
654
- : [formatID(originalMessage.sender_fbid)],
655
- body: originalMessage.body || "",
656
- threadID: formatID((originalMessage.thread_fbid || originalMessage.other_user_fbid).toString()),
657
- threadName: originalMessage.group_thread_info ? originalMessage.group_thread_info.name : originalMessage.sender_name,
658
- location: originalMessage.coordinates ? originalMessage.coordinates : null,
659
- messageID: originalMessage.mid ? originalMessage.mid.toString() : originalMessage.message_id,
660
- attachments: formatAttachment(originalMessage.attachments, originalMessage.attachmentIds, originalMessage.attachment_map, originalMessage.share_map),
661
- timestamp: originalMessage.timestamp,
662
- timestampAbsolute: originalMessage.timestamp_absolute,
663
- timestampRelative: originalMessage.timestamp_relative,
664
- timestampDatetime: originalMessage.timestamp_datetime,
665
- tags: originalMessage.tags,
666
- reactions: originalMessage.reactions ? originalMessage.reactions : [],
667
- isUnread: originalMessage.is_unread
668
- };
669
-
670
- if (m.type === "pages_messaging") obj.pageID = m.realtime_viewer_fbid.toString();
671
- obj.isGroup = obj.participantIDs.length > 2;
672
-
673
- return obj;
674
- }
675
-
676
- function formatEvent(m) {
677
- var originalMessage = m.message ? m.message : m;
678
- var logMessageType = originalMessage.log_message_type;
679
- var logMessageData;
680
- if (logMessageType === "log:generic-admin-text") {
681
- logMessageData = originalMessage.log_message_data.untypedData;
682
- logMessageType = getAdminTextMessageType(originalMessage.log_message_data.message_type);
683
- }
684
- else logMessageData = originalMessage.log_message_data;
685
-
686
- return Object.assign(formatMessage(originalMessage), {
687
- type: "event",
688
- logMessageType: logMessageType,
689
- logMessageData: logMessageData,
690
- logMessageBody: originalMessage.log_message_body
691
- });
692
- }
693
-
694
- function formatHistoryMessage(m) {
695
- switch (m.action_type) {
696
- case "ma-type:log-message":
697
- return formatEvent(m);
698
- default:
699
- return formatMessage(m);
700
- }
701
- }
702
-
703
- // Get a more readable message type for AdminTextMessages
704
- function getAdminTextMessageType(m) {
705
- switch (m.type) {
706
- case "change_thread_theme":
707
- return "log:thread-color";
708
- case "change_thread_icon":
709
- case "change_thread_quick_reaction":
710
- return "log:thread-icon";
711
- case "change_thread_nickname":
712
- return "log:user-nickname";
713
- case "change_thread_admins":
714
- return "log:thread-admins";
715
- case "group_poll":
716
- return "log:thread-poll";
717
- case "change_thread_approval_mode":
718
- return "log:thread-approval-mode";
719
- case "messenger_call_log":
720
- case "participant_joined_group_call":
721
- return "log:thread-call";
722
- }
723
- }
724
-
725
- function formatDeltaEvent(m) {
726
- var logMessageType;
727
- var logMessageData;
728
-
729
- // log:thread-color => {theme_color}
730
- // log:user-nickname => {participant_id, nickname}
731
- // log:thread-icon => {thread_icon}
732
- // log:thread-name => {name}
733
- // log:subscribe => {addedParticipants - [Array]}
734
- // log:unsubscribe => {leftParticipantFbId}
735
-
736
- switch (m.class) {
737
- case "AdminTextMessage":
738
- logMessageType = getAdminTextMessageType(m);
739
- logMessageData = m.untypedData;
740
- break;
741
- case "ThreadName":
742
- logMessageType = "log:thread-name";
743
- logMessageData = { name: m.name };
744
- break;
745
- case "ParticipantsAddedToGroupThread":
746
- logMessageType = "log:subscribe";
747
- logMessageData = { addedParticipants: m.addedParticipants };
748
- break;
749
- case "ParticipantLeftGroupThread":
750
- logMessageType = "log:unsubscribe";
751
- logMessageData = { leftParticipantFbId: m.leftParticipantFbId };
752
- break;
753
- }
754
-
755
- return {
756
- type: "event",
757
- threadID: formatID((m.messageMetadata.threadKey.threadFbId || m.messageMetadata.threadKey.otherUserFbId).toString()),
758
- logMessageType: logMessageType,
759
- logMessageData: logMessageData,
760
- logMessageBody: m.messageMetadata.adminText,
761
- author: m.messageMetadata.actorFbId,
762
- participantIDs: (m.participants || []).map(p => p.toString())
763
- };
764
- }
765
-
766
- function formatTyp(event) {
767
- return {
768
- isTyping: !!event.st,
769
- from: event.from.toString(),
770
- threadID: formatID((event.to || event.thread_fbid || event.from).toString()),
771
- // When receiving typ indication from mobile, `from_mobile` isn't set.
772
- // If it is, we just use that value.
773
- fromMobile: event.hasOwnProperty("from_mobile") ? event.from_mobile : true,
774
- userID: (event.realtime_viewer_fbid || event.from).toString(),
775
- type: "typ"
776
- };
777
- }
778
-
779
- function formatDeltaReadReceipt(delta) {
780
- // otherUserFbId seems to be used as both the readerID and the threadID in a 1-1 chat.
781
- // In a group chat actorFbId is used for the reader and threadFbId for the thread.
782
- return {
783
- reader: (delta.threadKey.otherUserFbId || delta.actorFbId).toString(),
784
- time: delta.actionTimestampMs,
785
- threadID: formatID((delta.threadKey.otherUserFbId || delta.threadKey.threadFbId).toString()),
786
- type: "read_receipt"
787
- };
788
- }
789
-
790
- function formatReadReceipt(event) {
791
- return {
792
- reader: event.reader.toString(),
793
- time: event.time,
794
- threadID: formatID((event.thread_fbid || event.reader).toString()),
795
- type: "read_receipt"
796
- };
797
- }
798
-
799
- function formatRead(event) {
800
- return {
801
- threadID: formatID(((event.chat_ids && event.chat_ids[0]) || (event.thread_fbids && event.thread_fbids[0])).toString()),
802
- time: event.timestamp,
803
- type: "read"
804
- };
805
- }
806
-
807
- function getFrom(str, startToken, endToken) {
808
- var start = str.indexOf(startToken) + startToken.length;
809
- if (start < startToken.length) return "";
810
-
811
- var lastHalf = str.substring(start);
812
- var end = lastHalf.indexOf(endToken);
813
- if (end === -1) throw Error("Could not find endTime `" + endToken + "` in the given string.");
814
- return lastHalf.substring(0, end);
815
- }
816
-
817
- function makeParsable(html) {
818
- let withoutForLoop = html.replace(/for\s*\(\s*;\s*;\s*\)\s*;\s*/, "");
819
-
820
- // (What the fuck FB, why windows style newlines?)
821
- // So sometimes FB will send us base multiple objects in the same response.
822
- // They're all valid JSON, one after the other, at the top level. We detect
823
- // that and make it parse-able by JSON.parse.
824
- // Ben - July 15th 2017
825
- //
826
- // It turns out that Facebook may insert random number of spaces before
827
- // next object begins (issue #616)
828
- // rav_kr - 2018-03-19
829
- let maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/);
830
- if (maybeMultipleObjects.length === 1) return maybeMultipleObjects;
831
-
832
- return "[" + maybeMultipleObjects.join("},{") + "]";
833
- }
834
-
835
- function arrToForm(form) {
836
- return arrayToObject(form,
837
- function (v) {
838
- return v.name;
839
- },
840
- function (v) {
841
- return v.val;
842
- }
843
- );
844
- }
845
-
846
- function arrayToObject(arr, getKey, getValue) {
847
- return arr.reduce(function (acc, val) {
848
- acc[getKey(val)] = getValue(val);
849
- return acc;
850
- }, {});
851
- }
852
-
853
- function getSignatureID() {
854
- return Math.floor(Math.random() * 2147483648).toString(16);
855
- }
856
-
857
- function generateTimestampRelative() {
858
- var d = new Date();
859
- return d.getHours() + ":" + padZeros(d.getMinutes());
860
- }
861
-
862
- function makeDefaults(html, userID, ctx) {
863
- var reqCounter = 1;
864
- var fb_dtsg = getFrom(html, 'name="fb_dtsg" value="', '"');
865
-
866
- // @Hack Ok we've done hacky things, this is definitely on top 5.
867
- // We totally assume the object is flat and try parsing until a }.
868
- // If it works though it's cool because we get a bunch of extra data things.
869
- //
870
- // Update: we don't need this. Leaving it in in case we ever do.
871
- // Ben - July 15th 2017
872
-
873
- // var siteData = getFrom(html, "[\"SiteData\",[],", "},");
874
- // try {
875
- // siteData = JSON.parse(siteData + "}");
876
- // } catch(e) {
877
- // log.warn("makeDefaults", "Couldn't parse SiteData. Won't have access to some variables.");
878
- // siteData = {};
879
- // }
880
-
881
- var ttstamp = "2";
882
- for (var i = 0; i < fb_dtsg.length; i++) ttstamp += fb_dtsg.charCodeAt(i);
883
- var revision = getFrom(html, 'revision":', ",");
884
-
885
- function mergeWithDefaults(obj) {
886
- // @TODO This is missing a key called __dyn.
887
- // After some investigation it seems like __dyn is some sort of set that FB
888
- // calls BitMap. It seems like certain responses have a "define" key in the
889
- // res.jsmods arrays. I think the code iterates over those and calls `set`
890
- // on the bitmap for each of those keys. Then it calls
891
- // bitmap.toCompressedString() which returns what __dyn is.
892
- //
893
- // So far the API has been working without this.
894
- //
895
- // Ben - July 15th 2017
896
- var newObj = {
897
- __user: userID,
898
- __req: (reqCounter++).toString(36),
899
- __rev: revision,
900
- __a: 1,
901
- // __af: siteData.features,
902
- fb_dtsg: ctx.fb_dtsg ? ctx.fb_dtsg : fb_dtsg,
903
- jazoest: ctx.ttstamp ? ctx.ttstamp : ttstamp
904
- // __spin_r: siteData.__spin_r,
905
- // __spin_b: siteData.__spin_b,
906
- // __spin_t: siteData.__spin_t,
907
- };
908
-
909
- // @TODO this is probably not needed.
910
- // Ben - July 15th 2017
911
- // if (siteData.be_key) {
912
- // newObj[siteData.be_key] = siteData.be_mode;
913
- // }
914
- // if (siteData.pkg_cohort_key) {
915
- // newObj[siteData.pkg_cohort_key] = siteData.pkg_cohort;
916
- // }
917
-
918
- if (!obj) return newObj;
919
- for (var prop in obj)
920
- if (obj.hasOwnProperty(prop))
921
- if (!newObj[prop]) newObj[prop] = obj[prop];
922
- return newObj;
923
- }
924
-
925
- function postWithDefaults(url, jar, form, ctxx) {
926
- return post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx);
927
- }
928
-
929
- function getWithDefaults(url, jar, qs, ctxx) {
930
- return get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx);
931
- }
932
-
933
- function postFormDataWithDefault(url, jar, form, qs, ctxx) {
934
- return postFormData(url, jar, mergeWithDefaults(form), mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx);
935
- }
936
-
937
- return {
938
- get: getWithDefaults,
939
- post: postWithDefaults,
940
- postFormData: postFormDataWithDefault
941
- };
942
- }
943
-
944
- function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
945
- if (retryCount == undefined) retryCount = 0;
946
- return function (data) {
947
- return bluebird.try(function () {
948
- log.verbose("parseAndCheckLogin", data.body);
949
- if (data.statusCode >= 500 && data.statusCode < 600) {
950
- if (retryCount >= 5) {
951
- throw {
952
- error: "Request retry failed. Check the `res` and `statusCode` property on this error.",
953
- statusCode: data.statusCode,
954
- res: data.body
955
- };
956
- }
957
- retryCount++;
958
- var retryTime = Math.floor(Math.random() * 5000);
959
- log.warn("parseAndCheckLogin", "Got status code " + data.statusCode + " - " + retryCount + ". attempt to retry in " + retryTime + " milliseconds...");
960
- var url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.request.uri.pathname;
961
- if (data.request.headers["Content-Type"].split(";")[0] === "multipart/form-data") return bluebird.delay(retryTime).then(() => defaultFuncs.postFormData(url, ctx.jar, data.request.formData, {})).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
962
- else return bluebird.delay(retryTime).then(() => defaultFuncs.post(url, ctx.jar, data.request.formData)).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
963
- }
964
- if (data.statusCode !== 200) throw new Error("parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.");
965
-
966
- var res = null;
967
- try {
968
- res = JSON.parse(makeParsable(data.body));
969
- }
970
- catch (e) {
971
- throw {
972
- error: "JSON.parse error. Check the `detail` property on this error.",
973
- detail: e,
974
- res: data.body
975
- };
976
- }
977
-
978
- // In some cases the response contains only a redirect URL which should be followed
979
- if (res.redirect && data.request.method === "GET") return defaultFuncs.get(res.redirect, ctx.jar).then(parseAndCheckLogin(ctx, defaultFuncs));
980
-
981
- // TODO: handle multiple cookies?
982
- if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") {
983
- res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", "");
984
- var cookie = formatCookie(res.jsmods.require[0][3], "facebook");
985
- var cookie2 = formatCookie(res.jsmods.require[0][3], "messenger");
986
- ctx.jar.setCookie(cookie, "https://www.facebook.com");
987
- ctx.jar.setCookie(cookie2, "https://www.messenger.com");
988
- }
989
-
990
- // On every request we check if we got a DTSG and we mutate the context so that we use the latest
991
- // one for the next requests.
992
- if (res.jsmods && Array.isArray(res.jsmods.require)) {
993
- var arr = res.jsmods.require;
994
- for (var i in arr) {
995
- if (arr[i][0] === "DTSG" && arr[i][1] === "setToken") {
996
- ctx.fb_dtsg = arr[i][3][0];
997
-
998
- // Update ttstamp since that depends on fb_dtsg
999
- ctx.ttstamp = "2";
1000
- for (var j = 0; j < ctx.fb_dtsg.length; j++) ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
1001
- }
1002
- }
1003
- }
1004
-
1005
- if (res.error === 1357001) throw { error: "Not logged in." };
1006
- return res;
1007
- });
1008
- };
1009
- }
1010
-
1011
- function saveCookies(jar) {
1012
- return function (res) {
1013
- var cookies = res.headers["set-cookie"] || [];
1014
- cookies.forEach(function (c) {
1015
- if (c.indexOf(".facebook.com") > -1) jar.setCookie(c, "https://www.facebook.com");
1016
- var c2 = c.replace(/domain=\.facebook\.com/, "domain=.messenger.com");
1017
- jar.setCookie(c2, "https://www.messenger.com");
1018
- });
1019
- return res;
1020
- };
1021
- }
1022
-
1023
- var NUM_TO_MONTH = [
1024
- "Jan",
1025
- "Feb",
1026
- "Mar",
1027
- "Apr",
1028
- "May",
1029
- "Jun",
1030
- "Jul",
1031
- "Aug",
1032
- "Sep",
1033
- "Oct",
1034
- "Nov",
1035
- "Dec"
1036
- ];
1037
- var NUM_TO_DAY = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1038
-
1039
- function formatDate(date) {
1040
- var d = date.getUTCDate();
1041
- d = d >= 10 ? d : `0${d}`;
1042
- var h = date.getUTCHours();
1043
- h = h >= 10 ? h : `0${h}`;
1044
- var m = date.getUTCMinutes();
1045
- m = m >= 10 ? m : `0${m}`;
1046
- var s = date.getUTCSeconds();
1047
- s = s >= 10 ? s : `0${s}`;
1048
- return `${NUM_TO_DAY[date.getUTCDay()]}, ${d} ${NUM_TO_MONTH[date.getUTCMonth()]} ${date.getUTCFullYear()} ${h}:${m}:${s} GMT`;
1049
- }
1050
-
1051
- function formatCookie(arr, url) {
1052
- return arr[0] + "=" + arr[1] + "; Path=" + arr[3] + "; Domain=" + url + ".com";
1053
- }
1054
-
1055
- function formatThread(data) {
1056
- return {
1057
- threadID: formatID(data.thread_fbid.toString()),
1058
- participants: data.participants.map(formatID),
1059
- participantIDs: data.participants.map(formatID),
1060
- name: data.name,
1061
- nicknames: data.custom_nickname,
1062
- snippet: data.snippet,
1063
- snippetAttachments: data.snippet_attachments,
1064
- snippetSender: formatID((data.snippet_sender || "").toString()),
1065
- unreadCount: data.unread_count,
1066
- messageCount: data.message_count,
1067
- imageSrc: data.image_src,
1068
- timestamp: data.timestamp,
1069
- serverTimestamp: data.server_timestamp, // what is this?
1070
- muteUntil: data.mute_until,
1071
- isCanonicalUser: data.is_canonical_user,
1072
- isCanonical: data.is_canonical,
1073
- isSubscribed: data.is_subscribed,
1074
- folder: data.folder,
1075
- isArchived: data.is_archived,
1076
- recipientsLoadable: data.recipients_loadable,
1077
- hasEmailParticipant: data.has_email_participant,
1078
- readOnly: data.read_only,
1079
- canReply: data.can_reply,
1080
- cannotReplyReason: data.cannot_reply_reason,
1081
- lastMessageTimestamp: data.last_message_timestamp,
1082
- lastReadTimestamp: data.last_read_timestamp,
1083
- lastMessageType: data.last_message_type,
1084
- emoji: data.custom_like_icon,
1085
- color: data.custom_color,
1086
- adminIDs: data.admin_ids,
1087
- threadType: data.thread_type
1088
- };
1089
- }
1090
-
1091
- function getType(obj) {
1092
- return Object.prototype.toString.call(obj).slice(8, -1);
1093
- }
1094
-
1095
- function formatProxyPresence(presence, userID) {
1096
- if (presence.lat === undefined || presence.p === undefined) return null;
1097
- return {
1098
- type: "presence",
1099
- timestamp: presence.lat * 1000,
1100
- userID: userID || '',
1101
- statuses: presence.p
1102
- };
1103
- }
1104
-
1105
- function formatPresence(presence, userID) {
1106
- return {
1107
- type: "presence",
1108
- timestamp: presence.la * 1000,
1109
- userID: userID || '',
1110
- statuses: presence.a
1111
- };
1112
- }
1113
-
1114
- function decodeClientPayload(payload) {
1115
- /*
1116
- Special function which Client using to "encode" clients JSON payload
1117
- */
1118
- function Utf8ArrayToStr(array) {
1119
- var out, i, len, c;
1120
- var char2, char3;
1121
- out = "";
1122
- len = array.length;
1123
- i = 0;
1124
- while (i < len) {
1125
- c = array[i++];
1126
- switch (c >> 4) {
1127
- case 0:
1128
- case 1:
1129
- case 2:
1130
- case 3:
1131
- case 4:
1132
- case 5:
1133
- case 6:
1134
- case 7:
1135
- out += String.fromCharCode(c);
1136
- break;
1137
- case 12:
1138
- case 13:
1139
- char2 = array[i++];
1140
- out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F));
1141
- break;
1142
- case 14:
1143
- char2 = array[i++];
1144
- char3 = array[i++];
1145
- out += String.fromCharCode(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
1146
- break;
1147
- }
1148
- }
1149
- return out;
1150
- }
1151
- return JSON.parse(Utf8ArrayToStr(payload));
1152
- }
1153
-
1154
- function getAppState(jar) {
1155
- return jar.getCookies("https://www.facebook.com").concat(jar.getCookies("https://facebook.com")).concat(jar.getCookies("https://www.messenger.com"));
1156
- }
1157
- module.exports = {
1158
- isReadableStream,
1159
- get,
1160
- post,
1161
- postFormData,
1162
- generateThreadingID,
1163
- generateOfflineThreadingID,
1164
- getGUID,
1165
- getFrom,
1166
- makeParsable,
1167
- arrToForm,
1168
- getSignatureID,
1169
- getJar: request.jar,
1170
- generateTimestampRelative,
1171
- makeDefaults,
1172
- parseAndCheckLogin,
1173
- saveCookies,
1174
- getType,
1175
- _formatAttachment,
1176
- formatHistoryMessage,
1177
- formatID,
1178
- formatMessage,
1179
- formatDeltaEvent,
1180
- formatDeltaMessage,
1181
- formatProxyPresence,
1182
- formatPresence,
1183
- formatTyp,
1184
- formatDeltaReadReceipt,
1185
- formatCookie,
1186
- formatThread,
1187
- formatReadReceipt,
1188
- formatRead,
1189
- generatePresence,
1190
- generateAccessiblityCookie,
1191
- formatDate,
1192
- decodeClientPayload,
1193
- getAppState,
1194
- getAdminTextMessageType,
1195
- setProxy
1196
- };
1
+ /* eslint-disable no-prototype-builtins */
2
+ "use strict";
3
+
4
+ let request = promisifyPromise(require("request").defaults({ jar: true, proxy: process.env.FB_PROXY }));
5
+ const stream = require("stream");
6
+ const log = require("npmlog");
7
+ const querystring = require("querystring");
8
+ const url = require("url");
9
+ var bluebird = require("bluebird");
10
+
11
+ class CustomError extends Error {
12
+ constructor(obj) {
13
+ if (typeof obj === 'string')
14
+ obj = { message: obj };
15
+ if (typeof obj !== 'object' || obj === null)
16
+ throw new TypeError('Object required');
17
+ obj.message ? super(obj.message) : super();
18
+ Object.assign(this, obj);
19
+ }
20
+ }
21
+
22
+ function callbackToPromise(func) {
23
+ return function (...args) {
24
+ return new Promise((resolve, reject) => {
25
+ func(...args, (err, data) => {
26
+ if (err)
27
+ reject(err);
28
+ else
29
+ resolve(data);
30
+ });
31
+ });
32
+ };
33
+ }
34
+
35
+ function isHasCallback(func) {
36
+ if (typeof func !== "function")
37
+ return false;
38
+ return func.toString().split("\n")[0].match(/(callback|cb)\s*\)/) !== null;
39
+ }
40
+
41
+ // replace for bluebird.promisify (but this only applies best to the `request` package)
42
+ function promisifyPromise(promise) {
43
+ const keys = Object.keys(promise);
44
+ let promise_;
45
+ if (
46
+ typeof promise === "function"
47
+ && isHasCallback(promise)
48
+ )
49
+ promise_ = callbackToPromise(promise);
50
+ else
51
+ promise_ = promise;
52
+
53
+ for (const key of keys) {
54
+ if (!promise[key]?.toString)
55
+ continue;
56
+
57
+ if (
58
+ typeof promise[key] === "function"
59
+ && isHasCallback(promise[key])
60
+ ) {
61
+ promise_[key] = callbackToPromise(promise[key]);
62
+ }
63
+ else {
64
+ promise_[key] = promise[key];
65
+ }
66
+ }
67
+
68
+ return promise_;
69
+ }
70
+
71
+ // replace for bluebird.delay
72
+ function delay(ms) {
73
+ return new Promise(resolve => setTimeout(resolve, ms));
74
+ }
75
+
76
+ // replace for bluebird.try
77
+ function tryPromise(tryFunc) {
78
+ return new Promise((resolve, reject) => {
79
+ try {
80
+ resolve(tryFunc());
81
+ } catch (error) {
82
+ reject(error);
83
+ }
84
+ });
85
+ }
86
+
87
+ function setProxy(url) {
88
+ if (typeof url == "undefined")
89
+ return request = promisifyPromise(require("request").defaults({
90
+ jar: true
91
+ }));
92
+ return request = promisifyPromise(require("request").defaults({
93
+ jar: true,
94
+ proxy: url
95
+ }));
96
+ }
97
+
98
+ function getHeaders(url, options, ctx, customHeader) {
99
+ const headers = {
100
+ "Content-Type": "application/x-www-form-urlencoded",
101
+ Referer: "https://www.facebook.com/",
102
+ Host: url.replace("https://", "").split("/")[0],
103
+ Origin: "https://www.facebook.com",
104
+ "User-Agent": options.userAgent,
105
+ Connection: "keep-alive",
106
+ "sec-fetch-site": "same-origin"
107
+ };
108
+ if (customHeader) {
109
+ Object.assign(headers, customHeader);
110
+ }
111
+ if (ctx && ctx.region) {
112
+ headers["X-MSGR-Region"] = ctx.region;
113
+ }
114
+
115
+ return headers;
116
+ }
117
+
118
+ function isReadableStream(obj) {
119
+ return (
120
+ obj instanceof stream.Stream &&
121
+ (getType(obj._read) === "Function" ||
122
+ getType(obj._read) === "AsyncFunction") &&
123
+ getType(obj._readableState) === "Object"
124
+ );
125
+ }
126
+
127
+ function get(url, jar, qs, options, ctx) {
128
+ // I'm still confused about this
129
+ if (getType(qs) === "Object") {
130
+ for (const prop in qs) {
131
+ if (qs.hasOwnProperty(prop) && getType(qs[prop]) === "Object") {
132
+ qs[prop] = JSON.stringify(qs[prop]);
133
+ }
134
+ }
135
+ }
136
+ const op = {
137
+ headers: getHeaders(url, options, ctx),
138
+ timeout: 60000,
139
+ qs: qs,
140
+ url: url,
141
+ method: "GET",
142
+ jar: jar,
143
+ gzip: true
144
+ };
145
+
146
+ return request(op).then(function (res) {
147
+ return Array.isArray(res) ? res[0] : res;
148
+ });
149
+ }
150
+
151
+ function post(url, jar, form, options, ctx, customHeader) {
152
+ const op = {
153
+ headers: getHeaders(url, options, ctx, customHeader),
154
+ timeout: 60000,
155
+ url: url,
156
+ method: "POST",
157
+ form: form,
158
+ jar: jar,
159
+ gzip: true
160
+ };
161
+
162
+ return request(op).then(function (res) {
163
+ return Array.isArray(res) ? res[0] : res;
164
+ });
165
+ }
166
+
167
+ function postFormData(url, jar, form, qs, options, ctx) {
168
+ const headers = getHeaders(url, options, ctx);
169
+ headers["Content-Type"] = "multipart/form-data";
170
+ const op = {
171
+ headers: headers,
172
+ timeout: 60000,
173
+ url: url,
174
+ method: "POST",
175
+ formData: form,
176
+ qs: qs,
177
+ jar: jar,
178
+ gzip: true
179
+ };
180
+
181
+ return request(op).then(function (res) {
182
+ return Array.isArray(res) ? res[0] : res;
183
+ });
184
+ }
185
+
186
+ function padZeros(val, len) {
187
+ val = String(val);
188
+ len = len || 2;
189
+ while (val.length < len) val = "0" + val;
190
+ return val;
191
+ }
192
+
193
+ function generateThreadingID(clientID) {
194
+ const k = Date.now();
195
+ const l = Math.floor(Math.random() * 4294967295);
196
+ const m = clientID;
197
+ return "<" + k + ":" + l + "-" + m + "@mail.projektitan.com>";
198
+ }
199
+
200
+ function binaryToDecimal(data) {
201
+ let ret = "";
202
+ while (data !== "0") {
203
+ let end = 0;
204
+ let fullName = "";
205
+ let i = 0;
206
+ for (; i < data.length; i++) {
207
+ end = 2 * end + parseInt(data[i], 10);
208
+ if (end >= 10) {
209
+ fullName += "1";
210
+ end -= 10;
211
+ }
212
+ else {
213
+ fullName += "0";
214
+ }
215
+ }
216
+ ret = end.toString() + ret;
217
+ data = fullName.slice(fullName.indexOf("1"));
218
+ }
219
+ return ret;
220
+ }
221
+
222
+ function generateOfflineThreadingID() {
223
+ const ret = Date.now();
224
+ const value = Math.floor(Math.random() * 4294967295);
225
+ const str = ("0000000000000000000000" + value.toString(2)).slice(-22);
226
+ const msgs = ret.toString(2) + str;
227
+ return binaryToDecimal(msgs);
228
+ }
229
+
230
+ let h;
231
+ const i = {};
232
+ const j = {
233
+ _: "%",
234
+ A: "%2",
235
+ B: "000",
236
+ C: "%7d",
237
+ D: "%7b%22",
238
+ E: "%2c%22",
239
+ F: "%22%3a",
240
+ G: "%2c%22ut%22%3a1",
241
+ H: "%2c%22bls%22%3a",
242
+ I: "%2c%22n%22%3a%22%",
243
+ J: "%22%3a%7b%22i%22%3a0%7d",
244
+ K: "%2c%22pt%22%3a0%2c%22vis%22%3a",
245
+ L: "%2c%22ch%22%3a%7b%22h%22%3a%22",
246
+ M: "%7b%22v%22%3a2%2c%22time%22%3a1",
247
+ N: ".channel%22%2c%22sub%22%3a%5b",
248
+ O: "%2c%22sb%22%3a1%2c%22t%22%3a%5b",
249
+ P: "%2c%22ud%22%3a100%2c%22lc%22%3a0",
250
+ Q: "%5d%2c%22f%22%3anull%2c%22uct%22%3a",
251
+ R: ".channel%22%2c%22sub%22%3a%5b1%5d",
252
+ S: "%22%2c%22m%22%3a0%7d%2c%7b%22i%22%3a",
253
+ T: "%2c%22blc%22%3a1%2c%22snd%22%3a1%2c%22ct%22%3a",
254
+ U: "%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
255
+ V: "%2c%22blc%22%3a0%2c%22snd%22%3a0%2c%22ct%22%3a",
256
+ W: "%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a",
257
+ X: "%2c%22ri%22%3a0%7d%2c%22state%22%3a%7b%22p%22%3a0%2c%22ut%22%3a1",
258
+ Y:
259
+ "%2c%22pt%22%3a0%2c%22vis%22%3a1%2c%22bls%22%3a0%2c%22blc%22%3a0%2c%22snd%22%3a1%2c%22ct%22%3a",
260
+ Z:
261
+ "%2c%22sb%22%3a1%2c%22t%22%3a%5b%5d%2c%22f%22%3anull%2c%22uct%22%3a0%2c%22s%22%3a0%2c%22blo%22%3a0%7d%2c%22bl%22%3a%7b%22ac%22%3a"
262
+ };
263
+ (function () {
264
+ const l = [];
265
+ for (const m in j) {
266
+ i[j[m]] = m;
267
+ l.push(j[m]);
268
+ }
269
+ l.reverse();
270
+ h = new RegExp(l.join("|"), "g");
271
+ })();
272
+
273
+ function presenceEncode(str) {
274
+ return encodeURIComponent(str)
275
+ .replace(/([_A-Z])|%../g, function (m, n) {
276
+ return n ? "%" + n.charCodeAt(0).toString(16) : m;
277
+ })
278
+ .toLowerCase()
279
+ .replace(h, function (m) {
280
+ return i[m];
281
+ });
282
+ }
283
+
284
+ // eslint-disable-next-line no-unused-vars
285
+ function presenceDecode(str) {
286
+ return decodeURIComponent(
287
+ str.replace(/[_A-Z]/g, function (m) {
288
+ return j[m];
289
+ })
290
+ );
291
+ }
292
+
293
+ function generatePresence(userID) {
294
+ const time = Date.now();
295
+ return (
296
+ "E" +
297
+ presenceEncode(
298
+ JSON.stringify({
299
+ v: 3,
300
+ time: parseInt(time / 1000, 10),
301
+ user: userID,
302
+ state: {
303
+ ut: 0,
304
+ t2: [],
305
+ lm2: null,
306
+ uct2: time,
307
+ tr: null,
308
+ tw: Math.floor(Math.random() * 4294967295) + 1,
309
+ at: time
310
+ },
311
+ ch: {
312
+ ["p_" + userID]: 0
313
+ }
314
+ })
315
+ )
316
+ );
317
+ }
318
+
319
+ function generateAccessiblityCookie() {
320
+ const time = Date.now();
321
+ return encodeURIComponent(
322
+ JSON.stringify({
323
+ sr: 0,
324
+ "sr-ts": time,
325
+ jk: 0,
326
+ "jk-ts": time,
327
+ kb: 0,
328
+ "kb-ts": time,
329
+ hcm: 0,
330
+ "hcm-ts": time
331
+ })
332
+ );
333
+ }
334
+
335
+ function getGUID() {
336
+ /** @type {number} */
337
+ let sectionLength = Date.now();
338
+ /** @type {string} */
339
+ const id = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
340
+ /** @type {number} */
341
+ const r = Math.floor((sectionLength + Math.random() * 16) % 16);
342
+ /** @type {number} */
343
+ sectionLength = Math.floor(sectionLength / 16);
344
+ /** @type {string} */
345
+ const _guid = (c == "x" ? r : (r & 7) | 8).toString(16);
346
+ return _guid;
347
+ });
348
+ return id;
349
+ }
350
+
351
+ function getExtension(original_extension, fullFileName = "") {
352
+ if (original_extension) {
353
+ return original_extension;
354
+ }
355
+ else {
356
+ const extension = fullFileName.split(".").pop();
357
+ if (extension === fullFileName) {
358
+ return "";
359
+ }
360
+ else {
361
+ return extension;
362
+ }
363
+ }
364
+ }
365
+
366
+ function _formatAttachment(attachment1, attachment2) {
367
+ // TODO: THIS IS REALLY BAD
368
+ // This is an attempt at fixing Facebook's inconsistencies. Sometimes they give us
369
+ // two attachment objects, but sometimes only one. They each contain part of the
370
+ // data that you'd want so we merge them for convenience.
371
+ // Instead of having a bunch of if statements guarding every access to image_data,
372
+ // we set it to empty object and use the fact that it'll return undefined.
373
+ const fullFileName = attachment1.filename;
374
+ const fileSize = Number(attachment1.fileSize || 0);
375
+ const durationVideo = attachment1.genericMetadata ? Number(attachment1.genericMetadata.videoLength) : undefined;
376
+ const durationAudio = attachment1.genericMetadata ? Number(attachment1.genericMetadata.duration) : undefined;
377
+ const mimeType = attachment1.mimeType;
378
+
379
+ attachment2 = attachment2 || { id: "", image_data: {} };
380
+ attachment1 = attachment1.mercury || attachment1;
381
+ let blob = attachment1.blob_attachment || attachment1.sticker_attachment;
382
+ let type =
383
+ blob && blob.__typename ? blob.__typename : attachment1.attach_type;
384
+ if (!type && attachment1.sticker_attachment) {
385
+ type = "StickerAttachment";
386
+ blob = attachment1.sticker_attachment;
387
+ }
388
+ else if (!type && attachment1.extensible_attachment) {
389
+ if (
390
+ attachment1.extensible_attachment.story_attachment &&
391
+ attachment1.extensible_attachment.story_attachment.target &&
392
+ attachment1.extensible_attachment.story_attachment.target.__typename &&
393
+ attachment1.extensible_attachment.story_attachment.target.__typename === "MessageLocation"
394
+ ) {
395
+ type = "MessageLocation";
396
+ }
397
+ else {
398
+ type = "ExtensibleAttachment";
399
+ }
400
+
401
+ blob = attachment1.extensible_attachment;
402
+ }
403
+ // TODO: Determine whether "sticker", "photo", "file" etc are still used
404
+ // KEEP IN SYNC WITH getThreadHistory
405
+ switch (type) {
406
+ case "sticker":
407
+ return {
408
+ type: "sticker",
409
+ ID: attachment1.metadata.stickerID.toString(),
410
+ url: attachment1.url,
411
+
412
+ packID: attachment1.metadata.packID.toString(),
413
+ spriteUrl: attachment1.metadata.spriteURI,
414
+ spriteUrl2x: attachment1.metadata.spriteURI2x,
415
+ width: attachment1.metadata.width,
416
+ height: attachment1.metadata.height,
417
+
418
+ caption: attachment2.caption,
419
+ description: attachment2.description,
420
+
421
+ frameCount: attachment1.metadata.frameCount,
422
+ frameRate: attachment1.metadata.frameRate,
423
+ framesPerRow: attachment1.metadata.framesPerRow,
424
+ framesPerCol: attachment1.metadata.framesPerCol,
425
+
426
+ stickerID: attachment1.metadata.stickerID.toString(), // @Legacy
427
+ spriteURI: attachment1.metadata.spriteURI, // @Legacy
428
+ spriteURI2x: attachment1.metadata.spriteURI2x // @Legacy
429
+ };
430
+ case "file":
431
+ return {
432
+ type: "file",
433
+ ID: attachment2.id.toString(),
434
+ fullFileName: fullFileName,
435
+ filename: attachment1.name,
436
+ fileSize: fileSize,
437
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
438
+ mimeType: mimeType,
439
+ url: attachment1.url,
440
+
441
+ isMalicious: attachment2.is_malicious,
442
+ contentType: attachment2.mime_type,
443
+
444
+ name: attachment1.name // @Legacy
445
+ };
446
+ case "photo":
447
+ return {
448
+ type: "photo",
449
+ ID: attachment1.metadata.fbid.toString(),
450
+ filename: attachment1.fileName,
451
+ fullFileName: fullFileName,
452
+ fileSize: fileSize,
453
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
454
+ mimeType: mimeType,
455
+ thumbnailUrl: attachment1.thumbnail_url,
456
+
457
+ previewUrl: attachment1.preview_url,
458
+ previewWidth: attachment1.preview_width,
459
+ previewHeight: attachment1.preview_height,
460
+
461
+ largePreviewUrl: attachment1.large_preview_url,
462
+ largePreviewWidth: attachment1.large_preview_width,
463
+ largePreviewHeight: attachment1.large_preview_height,
464
+
465
+ url: attachment1.metadata.url, // @Legacy
466
+ width: attachment1.metadata.dimensions.split(",")[0], // @Legacy
467
+ height: attachment1.metadata.dimensions.split(",")[1], // @Legacy
468
+ name: fullFileName // @Legacy
469
+ };
470
+ case "animated_image":
471
+ return {
472
+ type: "animated_image",
473
+ ID: attachment2.id.toString(),
474
+ filename: attachment2.filename,
475
+ fullFileName: fullFileName,
476
+ original_extension: getExtension(attachment2.original_extension, fullFileName),
477
+ mimeType: mimeType,
478
+
479
+ previewUrl: attachment1.preview_url,
480
+ previewWidth: attachment1.preview_width,
481
+ previewHeight: attachment1.preview_height,
482
+
483
+ url: attachment2.image_data.url,
484
+ width: attachment2.image_data.width,
485
+ height: attachment2.image_data.height,
486
+
487
+ name: attachment1.name, // @Legacy
488
+ facebookUrl: attachment1.url, // @Legacy
489
+ thumbnailUrl: attachment1.thumbnail_url, // @Legacy
490
+ rawGifImage: attachment2.image_data.raw_gif_image, // @Legacy
491
+ rawWebpImage: attachment2.image_data.raw_webp_image, // @Legacy
492
+ animatedGifUrl: attachment2.image_data.animated_gif_url, // @Legacy
493
+ animatedGifPreviewUrl: attachment2.image_data.animated_gif_preview_url, // @Legacy
494
+ animatedWebpUrl: attachment2.image_data.animated_webp_url, // @Legacy
495
+ animatedWebpPreviewUrl: attachment2.image_data.animated_webp_preview_url // @Legacy
496
+ };
497
+ case "share":
498
+ return {
499
+ type: "share",
500
+ ID: attachment1.share.share_id.toString(),
501
+ url: attachment2.href,
502
+
503
+ title: attachment1.share.title,
504
+ description: attachment1.share.description,
505
+ source: attachment1.share.source,
506
+
507
+ image: attachment1.share.media.image,
508
+ width: attachment1.share.media.image_size.width,
509
+ height: attachment1.share.media.image_size.height,
510
+ playable: attachment1.share.media.playable,
511
+ duration: attachment1.share.media.duration,
512
+
513
+ subattachments: attachment1.share.subattachments,
514
+ properties: {},
515
+
516
+ animatedImageSize: attachment1.share.media.animated_image_size, // @Legacy
517
+ facebookUrl: attachment1.share.uri, // @Legacy
518
+ target: attachment1.share.target, // @Legacy
519
+ styleList: attachment1.share.style_list // @Legacy
520
+ };
521
+ case "video":
522
+ return {
523
+ type: "video",
524
+ ID: attachment1.metadata.fbid.toString(),
525
+ filename: attachment1.name,
526
+ fullFileName: fullFileName,
527
+ original_extension: getExtension(attachment1.original_extension, fullFileName),
528
+ mimeType: mimeType,
529
+ duration: durationVideo,
530
+
531
+ previewUrl: attachment1.preview_url,
532
+ previewWidth: attachment1.preview_width,
533
+ previewHeight: attachment1.preview_height,
534
+
535
+ url: attachment1.url,
536
+ width: attachment1.metadata.dimensions.width,
537
+ height: attachment1.metadata.dimensions.height,
538
+
539
+ videoType: "unknown",
540
+
541
+ thumbnailUrl: attachment1.thumbnail_url // @Legacy
542
+ };
543
+ case "error":
544
+ return {
545
+ type: "error",
546
+
547
+ // Save error attachments because we're unsure of their format,
548
+ // and whether there are cases they contain something useful for debugging.
549
+ attachment1: attachment1,
550
+ attachment2: attachment2
551
+ };
552
+ case "MessageImage":
553
+ return {
554
+ type: "photo",
555
+ ID: blob.legacy_attachment_id,
556
+ filename: blob.filename,
557
+ fullFileName: fullFileName,
558
+ fileSize: fileSize,
559
+ original_extension: getExtension(blob.original_extension, fullFileName),
560
+ mimeType: mimeType,
561
+ thumbnailUrl: blob.thumbnail.uri,
562
+
563
+ previewUrl: blob.preview.uri,
564
+ previewWidth: blob.preview.width,
565
+ previewHeight: blob.preview.height,
566
+
567
+ largePreviewUrl: blob.large_preview.uri,
568
+ largePreviewWidth: blob.large_preview.width,
569
+ largePreviewHeight: blob.large_preview.height,
570
+
571
+ url: blob.large_preview.uri, // @Legacy
572
+ width: blob.original_dimensions.x, // @Legacy
573
+ height: blob.original_dimensions.y, // @Legacy
574
+ name: blob.filename // @Legacy
575
+ };
576
+ case "MessageAnimatedImage":
577
+ return {
578
+ type: "animated_image",
579
+ ID: blob.legacy_attachment_id,
580
+ filename: blob.filename,
581
+ fullFileName: fullFileName,
582
+ original_extension: getExtension(blob.original_extension, fullFileName),
583
+ mimeType: mimeType,
584
+
585
+ previewUrl: blob.preview_image.uri,
586
+ previewWidth: blob.preview_image.width,
587
+ previewHeight: blob.preview_image.height,
588
+
589
+ url: blob.animated_image.uri,
590
+ width: blob.animated_image.width,
591
+ height: blob.animated_image.height,
592
+
593
+ thumbnailUrl: blob.preview_image.uri, // @Legacy
594
+ name: blob.filename, // @Legacy
595
+ facebookUrl: blob.animated_image.uri, // @Legacy
596
+ rawGifImage: blob.animated_image.uri, // @Legacy
597
+ animatedGifUrl: blob.animated_image.uri, // @Legacy
598
+ animatedGifPreviewUrl: blob.preview_image.uri, // @Legacy
599
+ animatedWebpUrl: blob.animated_image.uri, // @Legacy
600
+ animatedWebpPreviewUrl: blob.preview_image.uri // @Legacy
601
+ };
602
+ case "MessageVideo":
603
+ return {
604
+ type: "video",
605
+ ID: blob.legacy_attachment_id,
606
+ filename: blob.filename,
607
+ fullFileName: fullFileName,
608
+ original_extension: getExtension(blob.original_extension, fullFileName),
609
+ fileSize: fileSize,
610
+ duration: durationVideo,
611
+ mimeType: mimeType,
612
+
613
+ previewUrl: blob.large_image.uri,
614
+ previewWidth: blob.large_image.width,
615
+ previewHeight: blob.large_image.height,
616
+
617
+ url: blob.playable_url,
618
+ width: blob.original_dimensions.x,
619
+ height: blob.original_dimensions.y,
620
+
621
+ videoType: blob.video_type.toLowerCase(),
622
+
623
+ thumbnailUrl: blob.large_image.uri // @Legacy
624
+ };
625
+ case "MessageAudio":
626
+ return {
627
+ type: "audio",
628
+ ID: blob.url_shimhash,
629
+ filename: blob.filename,
630
+ fullFileName: fullFileName,
631
+ fileSize: fileSize,
632
+ duration: durationAudio,
633
+ original_extension: getExtension(blob.original_extension, fullFileName),
634
+ mimeType: mimeType,
635
+
636
+ audioType: blob.audio_type,
637
+ url: blob.playable_url,
638
+
639
+ isVoiceMail: blob.is_voicemail
640
+ };
641
+ case "StickerAttachment":
642
+ case "Sticker":
643
+ return {
644
+ type: "sticker",
645
+ ID: blob.id,
646
+ url: blob.url,
647
+
648
+ packID: blob.pack ? blob.pack.id : null,
649
+ spriteUrl: blob.sprite_image,
650
+ spriteUrl2x: blob.sprite_image_2x,
651
+ width: blob.width,
652
+ height: blob.height,
653
+
654
+ caption: blob.label,
655
+ description: blob.label,
656
+
657
+ frameCount: blob.frame_count,
658
+ frameRate: blob.frame_rate,
659
+ framesPerRow: blob.frames_per_row,
660
+ framesPerCol: blob.frames_per_column,
661
+
662
+ stickerID: blob.id, // @Legacy
663
+ spriteURI: blob.sprite_image, // @Legacy
664
+ spriteURI2x: blob.sprite_image_2x // @Legacy
665
+ };
666
+ case "MessageLocation":
667
+ var urlAttach = blob.story_attachment.url;
668
+ var mediaAttach = blob.story_attachment.media;
669
+
670
+ var u = querystring.parse(url.parse(urlAttach).query).u;
671
+ var where1 = querystring.parse(url.parse(u).query).where1;
672
+ var address = where1.split(", ");
673
+
674
+ var latitude;
675
+ var longitude;
676
+
677
+ try {
678
+ latitude = Number.parseFloat(address[0]);
679
+ longitude = Number.parseFloat(address[1]);
680
+ } catch (err) {
681
+ /* empty */
682
+ }
683
+
684
+ var imageUrl;
685
+ var width;
686
+ var height;
687
+
688
+ if (mediaAttach && mediaAttach.image) {
689
+ imageUrl = mediaAttach.image.uri;
690
+ width = mediaAttach.image.width;
691
+ height = mediaAttach.image.height;
692
+ }
693
+
694
+ return {
695
+ type: "location",
696
+ ID: blob.legacy_attachment_id,
697
+ latitude: latitude,
698
+ longitude: longitude,
699
+ image: imageUrl,
700
+ width: width,
701
+ height: height,
702
+ url: u || urlAttach,
703
+ address: where1,
704
+
705
+ facebookUrl: blob.story_attachment.url, // @Legacy
706
+ target: blob.story_attachment.target, // @Legacy
707
+ styleList: blob.story_attachment.style_list // @Legacy
708
+ };
709
+ case "ExtensibleAttachment":
710
+ return {
711
+ type: "share",
712
+ ID: blob.legacy_attachment_id,
713
+ url: blob.story_attachment.url,
714
+
715
+ title: blob.story_attachment.title_with_entities.text,
716
+ description:
717
+ blob.story_attachment.description &&
718
+ blob.story_attachment.description.text,
719
+ source: blob.story_attachment.source
720
+ ? blob.story_attachment.source.text
721
+ : null,
722
+
723
+ image:
724
+ blob.story_attachment.media &&
725
+ blob.story_attachment.media.image &&
726
+ blob.story_attachment.media.image.uri,
727
+ width:
728
+ blob.story_attachment.media &&
729
+ blob.story_attachment.media.image &&
730
+ blob.story_attachment.media.image.width,
731
+ height:
732
+ blob.story_attachment.media &&
733
+ blob.story_attachment.media.image &&
734
+ blob.story_attachment.media.image.height,
735
+ playable:
736
+ blob.story_attachment.media &&
737
+ blob.story_attachment.media.is_playable,
738
+ duration:
739
+ blob.story_attachment.media &&
740
+ blob.story_attachment.media.playable_duration_in_ms,
741
+ playableUrl:
742
+ blob.story_attachment.media == null
743
+ ? null
744
+ : blob.story_attachment.media.playable_url,
745
+
746
+ subattachments: blob.story_attachment.subattachments,
747
+ properties: blob.story_attachment.properties.reduce(function (obj, cur) {
748
+ obj[cur.key] = cur.value.text;
749
+ return obj;
750
+ }, {}),
751
+
752
+ facebookUrl: blob.story_attachment.url, // @Legacy
753
+ target: blob.story_attachment.target, // @Legacy
754
+ styleList: blob.story_attachment.style_list // @Legacy
755
+ };
756
+ case "MessageFile":
757
+ return {
758
+ type: "file",
759
+ ID: blob.message_file_fbid,
760
+ fullFileName: fullFileName,
761
+ filename: blob.filename,
762
+ fileSize: fileSize,
763
+ mimeType: blob.mimetype,
764
+ original_extension: blob.original_extension || fullFileName.split(".").pop(),
765
+
766
+ url: blob.url,
767
+ isMalicious: blob.is_malicious,
768
+ contentType: blob.content_type,
769
+
770
+ name: blob.filename
771
+ };
772
+ default:
773
+ throw new Error(
774
+ "unrecognized attach_file of type " +
775
+ type +
776
+ "`" +
777
+ JSON.stringify(attachment1, null, 4) +
778
+ " attachment2: " +
779
+ JSON.stringify(attachment2, null, 4) +
780
+ "`"
781
+ );
782
+ }
783
+ }
784
+
785
+ function formatAttachment(attachments, attachmentIds, attachmentMap, shareMap) {
786
+ attachmentMap = shareMap || attachmentMap;
787
+ return attachments
788
+ ? attachments.map(function (val, i) {
789
+ if (
790
+ !attachmentMap ||
791
+ !attachmentIds ||
792
+ !attachmentMap[attachmentIds[i]]
793
+ ) {
794
+ return _formatAttachment(val);
795
+ }
796
+ return _formatAttachment(val, attachmentMap[attachmentIds[i]]);
797
+ })
798
+ : [];
799
+ }
800
+
801
+ function formatDeltaMessage(m) {
802
+ const md = m.delta.messageMetadata;
803
+
804
+ const mdata =
805
+ m.delta.data === undefined
806
+ ? []
807
+ : m.delta.data.prng === undefined
808
+ ? []
809
+ : JSON.parse(m.delta.data.prng);
810
+ const m_id = mdata.map(u => u.i);
811
+ const m_offset = mdata.map(u => u.o);
812
+ const m_length = mdata.map(u => u.l);
813
+ const mentions = {};
814
+ for (let i = 0; i < m_id.length; i++) {
815
+ mentions[m_id[i]] = m.delta.body.substring(
816
+ m_offset[i],
817
+ m_offset[i] + m_length[i]
818
+ );
819
+ }
820
+ return {
821
+ type: "message",
822
+ senderID: formatID(md.actorFbId.toString()),
823
+ body: m.delta.body || "",
824
+ threadID: formatID(
825
+ (md.threadKey.threadFbId || md.threadKey.otherUserFbId).toString()
826
+ ),
827
+ messageID: md.messageId,
828
+ attachments: (m.delta.attachments || []).map(v => _formatAttachment(v)),
829
+ mentions: mentions,
830
+ timestamp: md.timestamp,
831
+ isGroup: !!md.threadKey.threadFbId,
832
+ participantIDs: m.delta.participants || (md.cid ? md.cid.canonicalParticipantFbids : []) || []
833
+ };
834
+ }
835
+
836
+ function formatID(id) {
837
+ if (id != undefined && id != null) {
838
+ return id.replace(/(fb)?id[:.]/, "");
839
+ }
840
+ else {
841
+ return id;
842
+ }
843
+ }
844
+
845
+ function formatMessage(m) {
846
+ const originalMessage = m.message ? m.message : m;
847
+ const obj = {
848
+ type: "message",
849
+ senderName: originalMessage.sender_name,
850
+ senderID: formatID(originalMessage.sender_fbid.toString()),
851
+ participantNames: originalMessage.group_thread_info
852
+ ? originalMessage.group_thread_info.participant_names
853
+ : [originalMessage.sender_name.split(" ")[0]],
854
+ participantIDs: originalMessage.group_thread_info
855
+ ? originalMessage.group_thread_info.participant_ids.map(function (v) {
856
+ return formatID(v.toString());
857
+ })
858
+ : [formatID(originalMessage.sender_fbid)],
859
+ body: originalMessage.body || "",
860
+ threadID: formatID(
861
+ (
862
+ originalMessage.thread_fbid || originalMessage.other_user_fbid
863
+ ).toString()
864
+ ),
865
+ threadName: originalMessage.group_thread_info
866
+ ? originalMessage.group_thread_info.name
867
+ : originalMessage.sender_name,
868
+ location: originalMessage.coordinates ? originalMessage.coordinates : null,
869
+ messageID: originalMessage.mid
870
+ ? originalMessage.mid.toString()
871
+ : originalMessage.message_id,
872
+ attachments: formatAttachment(
873
+ originalMessage.attachments,
874
+ originalMessage.attachmentIds,
875
+ originalMessage.attachment_map,
876
+ originalMessage.share_map
877
+ ),
878
+ timestamp: originalMessage.timestamp,
879
+ timestampAbsolute: originalMessage.timestamp_absolute,
880
+ timestampRelative: originalMessage.timestamp_relative,
881
+ timestampDatetime: originalMessage.timestamp_datetime,
882
+ tags: originalMessage.tags,
883
+ reactions: originalMessage.reactions ? originalMessage.reactions : [],
884
+ isUnread: originalMessage.is_unread
885
+ };
886
+
887
+ if (m.type === "pages_messaging")
888
+ obj.pageID = m.realtime_viewer_fbid.toString();
889
+ obj.isGroup = obj.participantIDs.length > 2;
890
+
891
+ return obj;
892
+ }
893
+
894
+ function formatEvent(m) {
895
+ const originalMessage = m.message ? m.message : m;
896
+ let logMessageType = originalMessage.log_message_type;
897
+ let logMessageData;
898
+ if (logMessageType === "log:generic-admin-text") {
899
+ logMessageData = originalMessage.log_message_data.untypedData;
900
+ logMessageType = getAdminTextMessageType(
901
+ originalMessage.log_message_data.message_type
902
+ );
903
+ }
904
+ else {
905
+ logMessageData = originalMessage.log_message_data;
906
+ }
907
+
908
+ return Object.assign(formatMessage(originalMessage), {
909
+ type: "event",
910
+ logMessageType: logMessageType,
911
+ logMessageData: logMessageData,
912
+ logMessageBody: originalMessage.log_message_body
913
+ });
914
+ }
915
+
916
+ function formatHistoryMessage(m) {
917
+ switch (m.action_type) {
918
+ case "ma-type:log-message":
919
+ return formatEvent(m);
920
+ default:
921
+ return formatMessage(m);
922
+ }
923
+ }
924
+
925
+ // Get a more readable message type for AdminTextMessages
926
+ function getAdminTextMessageType(type) {
927
+ switch (type) {
928
+ case "change_thread_theme":
929
+ return "log:thread-color";
930
+ case "change_thread_icon":
931
+ return "log:thread-icon";
932
+ case "change_thread_nickname":
933
+ return "log:user-nickname";
934
+ case "change_thread_admins":
935
+ return "log:thread-admins";
936
+ case "group_poll":
937
+ return "log:thread-poll";
938
+ case "change_thread_approval_mode":
939
+ return "log:thread-approval-mode";
940
+ case "messenger_call_log":
941
+ case "participant_joined_group_call":
942
+ return "log:thread-call";
943
+ default:
944
+ return type;
945
+ }
946
+ }
947
+
948
+ function formatDeltaEvent(m) {
949
+ let logMessageType;
950
+ let logMessageData;
951
+
952
+ // log:thread-color => {theme_color}
953
+ // log:user-nickname => {participant_id, nickname}
954
+ // log:thread-icon => {thread_icon}
955
+ // log:thread-name => {name}
956
+ // log:subscribe => {addedParticipants - [Array]}
957
+ // log:unsubscribe => {leftParticipantFbId}
958
+
959
+ switch (m.class) {
960
+ case "AdminTextMessage":
961
+ logMessageData = m.untypedData;
962
+ logMessageType = getAdminTextMessageType(m.type);
963
+ break;
964
+ case "ThreadName":
965
+ logMessageType = "log:thread-name";
966
+ logMessageData = { name: m.name };
967
+ break;
968
+ case "ParticipantsAddedToGroupThread":
969
+ logMessageType = "log:subscribe";
970
+ logMessageData = { addedParticipants: m.addedParticipants };
971
+ break;
972
+ case "ParticipantLeftGroupThread":
973
+ logMessageType = "log:unsubscribe";
974
+ logMessageData = { leftParticipantFbId: m.leftParticipantFbId };
975
+ break;
976
+ case "ApprovalQueue":
977
+ logMessageType = "log:approval-queue";
978
+ logMessageData = {
979
+ approvalQueue: {
980
+ action: m.action,
981
+ recipientFbId: m.recipientFbId,
982
+ requestSource: m.requestSource,
983
+ ...m.messageMetadata
984
+ }
985
+ };
986
+ }
987
+
988
+ return {
989
+ type: "event",
990
+ threadID: formatID(
991
+ (
992
+ m.messageMetadata.threadKey.threadFbId ||
993
+ m.messageMetadata.threadKey.otherUserFbId
994
+ ).toString()
995
+ ),
996
+ messageID: m.messageMetadata.messageId.toString(),
997
+ logMessageType: logMessageType,
998
+ logMessageData: logMessageData,
999
+ logMessageBody: m.messageMetadata.adminText,
1000
+ timestamp: m.messageMetadata.timestamp,
1001
+ author: m.messageMetadata.actorFbId,
1002
+ participantIDs: (m.participants || []).map(p => p.toString())
1003
+ };
1004
+ }
1005
+
1006
+ function formatTyp(event) {
1007
+ return {
1008
+ isTyping: !!event.st,
1009
+ from: event.from.toString(),
1010
+ threadID: formatID(
1011
+ (event.to || event.thread_fbid || event.from).toString()
1012
+ ),
1013
+ // When receiving typ indication from mobile, `from_mobile` isn't set.
1014
+ // If it is, we just use that value.
1015
+ fromMobile: event.hasOwnProperty("from_mobile") ? event.from_mobile : true,
1016
+ userID: (event.realtime_viewer_fbid || event.from).toString(),
1017
+ type: "typ"
1018
+ };
1019
+ }
1020
+
1021
+ function formatDeltaReadReceipt(delta) {
1022
+ // otherUserFbId seems to be used as both the readerID and the threadID in a 1-1 chat.
1023
+ // In a group chat actorFbId is used for the reader and threadFbId for the thread.
1024
+ return {
1025
+ reader: (delta.threadKey.otherUserFbId || delta.actorFbId).toString(),
1026
+ time: delta.actionTimestampMs,
1027
+ threadID: formatID(
1028
+ (delta.threadKey.otherUserFbId || delta.threadKey.threadFbId).toString()
1029
+ ),
1030
+ type: "read_receipt"
1031
+ };
1032
+ }
1033
+
1034
+ function formatReadReceipt(event) {
1035
+ return {
1036
+ reader: event.reader.toString(),
1037
+ time: event.time,
1038
+ threadID: formatID((event.thread_fbid || event.reader).toString()),
1039
+ type: "read_receipt"
1040
+ };
1041
+ }
1042
+
1043
+ function formatRead(event) {
1044
+ return {
1045
+ threadID: formatID(
1046
+ (
1047
+ (event.chat_ids && event.chat_ids[0]) ||
1048
+ (event.thread_fbids && event.thread_fbids[0])
1049
+ ).toString()
1050
+ ),
1051
+ time: event.timestamp,
1052
+ type: "read"
1053
+ };
1054
+ }
1055
+
1056
+ function getFrom(str, startToken, endToken) {
1057
+ const start = str.indexOf(startToken) + startToken.length;
1058
+ if (start < startToken.length) return "";
1059
+
1060
+ const lastHalf = str.substring(start);
1061
+ const end = lastHalf.indexOf(endToken);
1062
+ if (end === -1) {
1063
+ throw new Error(
1064
+ "Could not find endTime `" + endToken + "` in the given string."
1065
+ );
1066
+ }
1067
+ return lastHalf.substring(0, end);
1068
+ }
1069
+
1070
+ function makeParsable(html) {
1071
+ const withoutForLoop = html.replace(/for\s*\(\s*;\s*;\s*\)\s*;\s*/, "");
1072
+
1073
+ // (What the fuck FB, why windows style newlines?)
1074
+ // So sometimes FB will send us base multiple objects in the same response.
1075
+ // They're all valid JSON, one after the other, at the top level. We detect
1076
+ // that and make it parse-able by JSON.parse.
1077
+ // Ben - July 15th 2017
1078
+ //
1079
+ // It turns out that Facebook may insert random number of spaces before
1080
+ // next object begins (issue #616)
1081
+ // rav_kr - 2018-03-19
1082
+ const maybeMultipleObjects = withoutForLoop.split(/\}\r\n *\{/);
1083
+ if (maybeMultipleObjects.length === 1) return maybeMultipleObjects;
1084
+
1085
+ return "[" + maybeMultipleObjects.join("},{") + "]";
1086
+ }
1087
+
1088
+ function arrToForm(form) {
1089
+ return arrayToObject(
1090
+ form,
1091
+ function (v) {
1092
+ return v.name;
1093
+ },
1094
+ function (v) {
1095
+ return v.val;
1096
+ }
1097
+ );
1098
+ }
1099
+
1100
+ function arrayToObject(arr, getKey, getValue) {
1101
+ return arr.reduce(function (acc, val) {
1102
+ acc[getKey(val)] = getValue(val);
1103
+ return acc;
1104
+ }, {});
1105
+ }
1106
+
1107
+ function getSignatureID() {
1108
+ return Math.floor(Math.random() * 2147483648).toString(16);
1109
+ }
1110
+
1111
+ function generateTimestampRelative() {
1112
+ const d = new Date();
1113
+ return d.getHours() + ":" + padZeros(d.getMinutes());
1114
+ }
1115
+
1116
+ function makeDefaults(html, userID, ctx) {
1117
+ let reqCounter = 1;
1118
+ const fb_dtsg = getFrom(html, 'name="fb_dtsg" value="', '"');
1119
+
1120
+ // @Hack Ok we've done hacky things, this is definitely on top 5.
1121
+ // We totally assume the object is flat and try parsing until a }.
1122
+ // If it works though it's cool because we get a bunch of extra data things.
1123
+ //
1124
+ // Update: we don't need this. Leaving it in in case we ever do.
1125
+ // Ben - July 15th 2017
1126
+
1127
+ // var siteData = getFrom(html, "[\"SiteData\",[],", "},");
1128
+ // try {
1129
+ // siteData = JSON.parse(siteData + "}");
1130
+ // } catch(e) {
1131
+ // log.warn("makeDefaults", "Couldn't parse SiteData. Won't have access to some variables.");
1132
+ // siteData = {};
1133
+ // }
1134
+
1135
+ let ttstamp = "2";
1136
+ for (let i = 0; i < fb_dtsg.length; i++) {
1137
+ ttstamp += fb_dtsg.charCodeAt(i);
1138
+ }
1139
+ const revision = getFrom(html, 'revision":', ",");
1140
+
1141
+ function mergeWithDefaults(obj) {
1142
+ // @TODO This is missing a key called __dyn.
1143
+ // After some investigation it seems like __dyn is some sort of set that FB
1144
+ // calls BitMap. It seems like certain responses have a "define" key in the
1145
+ // res.jsmods arrays. I think the code iterates over those and calls `set`
1146
+ // on the bitmap for each of those keys. Then it calls
1147
+ // bitmap.toCompressedString() which returns what __dyn is.
1148
+ //
1149
+ // So far the API has been working without this.
1150
+ //
1151
+ // Ben - July 15th 2017
1152
+ const newObj = {
1153
+ __user: userID,
1154
+ __req: (reqCounter++).toString(36),
1155
+ __rev: revision,
1156
+ __a: 1,
1157
+ // __af: siteData.features,
1158
+ fb_dtsg: ctx.fb_dtsg ? ctx.fb_dtsg : fb_dtsg,
1159
+ jazoest: ctx.ttstamp ? ctx.ttstamp : ttstamp
1160
+ // __spin_r: siteData.__spin_r,
1161
+ // __spin_b: siteData.__spin_b,
1162
+ // __spin_t: siteData.__spin_t,
1163
+ };
1164
+
1165
+ // @TODO this is probably not needed.
1166
+ // Ben - July 15th 2017
1167
+ // if (siteData.be_key) {
1168
+ // newObj[siteData.be_key] = siteData.be_mode;
1169
+ // }
1170
+ // if (siteData.pkg_cohort_key) {
1171
+ // newObj[siteData.pkg_cohort_key] = siteData.pkg_cohort;
1172
+ // }
1173
+
1174
+ if (!obj) return newObj;
1175
+
1176
+ for (const prop in obj) {
1177
+ if (obj.hasOwnProperty(prop)) {
1178
+ if (!newObj[prop]) {
1179
+ newObj[prop] = obj[prop];
1180
+ }
1181
+ }
1182
+ }
1183
+
1184
+ return newObj;
1185
+ }
1186
+
1187
+ function postWithDefaults(url, jar, form, ctxx, customHeader = {}) {
1188
+ return post(url, jar, mergeWithDefaults(form), ctx.globalOptions, ctxx || ctx, customHeader);
1189
+ }
1190
+
1191
+ function getWithDefaults(url, jar, qs, ctxx, customHeader = {}) {
1192
+ return get(url, jar, mergeWithDefaults(qs), ctx.globalOptions, ctxx || ctx, customHeader);
1193
+ }
1194
+
1195
+ function postFormDataWithDefault(url, jar, form, qs, ctxx) {
1196
+ return postFormData(
1197
+ url,
1198
+ jar,
1199
+ mergeWithDefaults(form),
1200
+ mergeWithDefaults(qs),
1201
+ ctx.globalOptions,
1202
+ ctxx || ctx
1203
+ );
1204
+ }
1205
+
1206
+ return {
1207
+ get: getWithDefaults,
1208
+ post: postWithDefaults,
1209
+ postFormData: postFormDataWithDefault
1210
+ };
1211
+ }
1212
+
1213
+ function parseAndCheckLogin(ctx, defaultFuncs, retryCount) {
1214
+ if (retryCount == undefined) retryCount = 0;
1215
+ return function (data) {
1216
+ return bluebird.try(function () {
1217
+ log.verbose("parseAndCheckLogin", data.body);
1218
+ if (data.statusCode >= 500 && data.statusCode < 600) {
1219
+ if (retryCount >= 5) {
1220
+ throw {
1221
+ error: "Request retry failed. Check the `res` and `statusCode` property on this error.",
1222
+ statusCode: data.statusCode,
1223
+ res: data.body
1224
+ };
1225
+ }
1226
+ retryCount++;
1227
+ var retryTime = Math.floor(Math.random() * 5000);
1228
+ log.warn("parseAndCheckLogin", "Got status code " + data.statusCode + " - " + retryCount + ". attempt to retry in " + retryTime + " milliseconds...");
1229
+ var url = data.request.uri.protocol + "//" + data.request.uri.hostname + data.request.uri.pathname;
1230
+ if (data.request.headers["Content-Type"].split(";")[0] === "multipart/form-data") return bluebird.delay(retryTime).then(() => defaultFuncs.postFormData(url, ctx.jar, data.request.formData, {})).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
1231
+ else return bluebird.delay(retryTime).then(() => defaultFuncs.post(url, ctx.jar, data.request.formData)).then(parseAndCheckLogin(ctx, defaultFuncs, retryCount));
1232
+ }
1233
+ if (data.statusCode !== 200) throw new Error("parseAndCheckLogin got status code: " + data.statusCode + ". Bailing out of trying to parse response.");
1234
+
1235
+ var res = null;
1236
+ try {
1237
+ res = JSON.parse(makeParsable(data.body));
1238
+ }
1239
+ catch (e) {
1240
+ throw {
1241
+ error: "JSON.parse error. Check the `detail` property on this error.",
1242
+ detail: e,
1243
+ res: data.body
1244
+ };
1245
+ }
1246
+
1247
+ // In some cases the response contains only a redirect URL which should be followed
1248
+ if (res.redirect && data.request.method === "GET") return defaultFuncs.get(res.redirect, ctx.jar).then(parseAndCheckLogin(ctx, defaultFuncs));
1249
+
1250
+ // TODO: handle multiple cookies?
1251
+ if (res.jsmods && res.jsmods.require && Array.isArray(res.jsmods.require[0]) && res.jsmods.require[0][0] === "Cookie") {
1252
+ res.jsmods.require[0][3][0] = res.jsmods.require[0][3][0].replace("_js_", "");
1253
+ var cookie = formatCookie(res.jsmods.require[0][3], "facebook");
1254
+ var cookie2 = formatCookie(res.jsmods.require[0][3], "messenger");
1255
+ ctx.jar.setCookie(cookie, "https://www.facebook.com");
1256
+ ctx.jar.setCookie(cookie2, "https://www.messenger.com");
1257
+ }
1258
+
1259
+ // On every request we check if we got a DTSG and we mutate the context so that we use the latest
1260
+ // one for the next requests.
1261
+ if (res.jsmods && Array.isArray(res.jsmods.require)) {
1262
+ var arr = res.jsmods.require;
1263
+ for (var i in arr) {
1264
+ if (arr[i][0] === "DTSG" && arr[i][1] === "setToken") {
1265
+ ctx.fb_dtsg = arr[i][3][0];
1266
+
1267
+ // Update ttstamp since that depends on fb_dtsg
1268
+ ctx.ttstamp = "2";
1269
+ for (var j = 0; j < ctx.fb_dtsg.length; j++) ctx.ttstamp += ctx.fb_dtsg.charCodeAt(j);
1270
+ }
1271
+ }
1272
+ }
1273
+
1274
+ if (res.error === 1357001) throw { error: "Not logged in." };
1275
+ return res;
1276
+ });
1277
+ };
1278
+ }
1279
+
1280
+ function checkLiveCookie(ctx, defaultFuncs) {
1281
+ return defaultFuncs
1282
+ .get("https://m.facebook.com/me", ctx.jar)
1283
+ .then(function (res) {
1284
+ if (res.body.indexOf(ctx.i_userID || ctx.userID) === -1) {
1285
+ throw new CustomError({
1286
+ message: "Not logged in.",
1287
+ error: "Not logged in."
1288
+ });
1289
+ }
1290
+ return true;
1291
+ });
1292
+ }
1293
+
1294
+ function saveCookies(jar) {
1295
+ return function (res) {
1296
+ const cookies = res.headers["set-cookie"] || [];
1297
+ cookies.forEach(function (c) {
1298
+ if (c.indexOf(".facebook.com") > -1) {
1299
+ jar.setCookie(c, "https://www.facebook.com");
1300
+ }
1301
+ const c2 = c.replace(/domain=\.facebook\.com/, "domain=.messenger.com");
1302
+ jar.setCookie(c2, "https://www.messenger.com");
1303
+ });
1304
+ return res;
1305
+ };
1306
+ }
1307
+
1308
+ const NUM_TO_MONTH = [
1309
+ "Jan",
1310
+ "Feb",
1311
+ "Mar",
1312
+ "Apr",
1313
+ "May",
1314
+ "Jun",
1315
+ "Jul",
1316
+ "Aug",
1317
+ "Sep",
1318
+ "Oct",
1319
+ "Nov",
1320
+ "Dec"
1321
+ ];
1322
+ const NUM_TO_DAY = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
1323
+ function formatDate(date) {
1324
+ let d = date.getUTCDate();
1325
+ d = d >= 10 ? d : "0" + d;
1326
+ let h = date.getUTCHours();
1327
+ h = h >= 10 ? h : "0" + h;
1328
+ let m = date.getUTCMinutes();
1329
+ m = m >= 10 ? m : "0" + m;
1330
+ let s = date.getUTCSeconds();
1331
+ s = s >= 10 ? s : "0" + s;
1332
+ return (
1333
+ NUM_TO_DAY[date.getUTCDay()] +
1334
+ ", " +
1335
+ d +
1336
+ " " +
1337
+ NUM_TO_MONTH[date.getUTCMonth()] +
1338
+ " " +
1339
+ date.getUTCFullYear() +
1340
+ " " +
1341
+ h +
1342
+ ":" +
1343
+ m +
1344
+ ":" +
1345
+ s +
1346
+ " GMT"
1347
+ );
1348
+ }
1349
+
1350
+ function formatCookie(arr, url) {
1351
+ return (
1352
+ arr[0] + "=" + arr[1] + "; Path=" + arr[3] + "; Domain=" + url + ".com"
1353
+ );
1354
+ }
1355
+
1356
+ function formatThread(data) {
1357
+ return {
1358
+ threadID: formatID(data.thread_fbid.toString()),
1359
+ participants: data.participants.map(formatID),
1360
+ participantIDs: data.participants.map(formatID),
1361
+ name: data.name,
1362
+ nicknames: data.custom_nickname,
1363
+ snippet: data.snippet,
1364
+ snippetAttachments: data.snippet_attachments,
1365
+ snippetSender: formatID((data.snippet_sender || "").toString()),
1366
+ unreadCount: data.unread_count,
1367
+ messageCount: data.message_count,
1368
+ imageSrc: data.image_src,
1369
+ timestamp: data.timestamp,
1370
+ serverTimestamp: data.server_timestamp, // what is this?
1371
+ muteUntil: data.mute_until,
1372
+ isCanonicalUser: data.is_canonical_user,
1373
+ isCanonical: data.is_canonical,
1374
+ isSubscribed: data.is_subscribed,
1375
+ folder: data.folder,
1376
+ isArchived: data.is_archived,
1377
+ recipientsLoadable: data.recipients_loadable,
1378
+ hasEmailParticipant: data.has_email_participant,
1379
+ readOnly: data.read_only,
1380
+ canReply: data.can_reply,
1381
+ cannotReplyReason: data.cannot_reply_reason,
1382
+ lastMessageTimestamp: data.last_message_timestamp,
1383
+ lastReadTimestamp: data.last_read_timestamp,
1384
+ lastMessageType: data.last_message_type,
1385
+ emoji: data.custom_like_icon,
1386
+ color: data.custom_color,
1387
+ adminIDs: data.admin_ids,
1388
+ threadType: data.thread_type
1389
+ };
1390
+ }
1391
+
1392
+ function getType(obj) {
1393
+ return Object.prototype.toString.call(obj).slice(8, -1);
1394
+ }
1395
+
1396
+ function formatProxyPresence(presence, userID) {
1397
+ if (presence.lat === undefined || presence.p === undefined) return null;
1398
+ return {
1399
+ type: "presence",
1400
+ timestamp: presence.lat * 1000,
1401
+ userID: userID,
1402
+ statuses: presence.p
1403
+ };
1404
+ }
1405
+
1406
+ function formatPresence(presence, userID) {
1407
+ return {
1408
+ type: "presence",
1409
+ timestamp: presence.la * 1000,
1410
+ userID: userID,
1411
+ statuses: presence.a
1412
+ };
1413
+ }
1414
+
1415
+ function decodeClientPayload(payload) {
1416
+ /*
1417
+ Special function which Client using to "encode" clients JSON payload
1418
+ */
1419
+ return JSON.parse(String.fromCharCode.apply(null, payload));
1420
+ }
1421
+
1422
+ function getAppState(jar) {
1423
+ return jar
1424
+ .getCookies("https://www.facebook.com")
1425
+ .concat(jar.getCookies("https://facebook.com"))
1426
+ .concat(jar.getCookies("https://www.messenger.com"));
1427
+ }
1428
+ module.exports = {
1429
+ CustomError,
1430
+ isReadableStream,
1431
+ get,
1432
+ post,
1433
+ postFormData,
1434
+ generateThreadingID,
1435
+ generateOfflineThreadingID,
1436
+ getGUID,
1437
+ getFrom,
1438
+ makeParsable,
1439
+ arrToForm,
1440
+ getSignatureID,
1441
+ getJar: request.jar,
1442
+ generateTimestampRelative,
1443
+ makeDefaults,
1444
+ parseAndCheckLogin,
1445
+ saveCookies,
1446
+ getType,
1447
+ _formatAttachment,
1448
+ formatHistoryMessage,
1449
+ formatID,
1450
+ formatMessage,
1451
+ formatDeltaEvent,
1452
+ formatDeltaMessage,
1453
+ formatProxyPresence,
1454
+ formatPresence,
1455
+ formatTyp,
1456
+ formatDeltaReadReceipt,
1457
+ formatCookie,
1458
+ formatThread,
1459
+ formatReadReceipt,
1460
+ formatRead,
1461
+ generatePresence,
1462
+ generateAccessiblityCookie,
1463
+ formatDate,
1464
+ decodeClientPayload,
1465
+ getAppState,
1466
+ getAdminTextMessageType,
1467
+ setProxy,
1468
+ checkLiveCookie
1469
+ };
1470
+