alicezetion 1.6.2 → 1.6.4

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.
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
+ };