alicezetion 1.6.3 → 1.6.5

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