keybase-proofs 2.3.23 → 2.3.25

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 2.3.24 (2020-12-23)
2
+
3
+ - Use oembed API for twitter scraping.
4
+
1
5
  ## 2.3.23 (2020-03-10)
2
6
 
3
7
  - update WOT schema again
package/lib/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- // Generated by IcedCoffeeScript 108.0.11
1
+ // Generated by IcedCoffeeScript 108.0.12
2
2
  (function() {
3
3
  var constants, d, k, v, _ref;
4
4
 
package/lib/schema3.js CHANGED
@@ -103,6 +103,7 @@
103
103
  var keys;
104
104
  keys = _arg.keys;
105
105
  this._keys = keys;
106
+ this._allow_extra_keys = false;
106
107
  Dict.__super__.constructor.apply(this, arguments);
107
108
  }
108
109
 
@@ -116,6 +117,9 @@
116
117
  v = obj[k];
117
118
  new_path = path.extend(k);
118
119
  if ((checker = this._keys[k]) == null) {
120
+ if (this._allow_extra_keys) {
121
+ continue;
122
+ }
119
123
  return mkerr(new_path, "key is not supported");
120
124
  }
121
125
  if ((err = this._check_value({
@@ -154,6 +158,11 @@
154
158
  return this._keys[k] = v;
155
159
  };
156
160
 
161
+ Dict.prototype.allow_extra_keys = function() {
162
+ this._allow_extra_keys = true;
163
+ return this;
164
+ };
165
+
157
166
  return Dict;
158
167
 
159
168
  })(Node);
@@ -1,6 +1,6 @@
1
- // Generated by IcedCoffeeScript 108.0.11
1
+ // Generated by IcedCoffeeScript 108.0.12
2
2
  (function() {
3
- var BaseScraper, b64find, constants, decode_sig, iced, parse_int_or_undefined, pkg, sncmp, space_normalize, urlmod, user_agent, v_codes, __iced_k, __iced_k_noop;
3
+ var BaseScraper, b64find, callbackify, constants, decode_sig, iced, parse_int_or_undefined, pkg, sncmp, space_normalize, urlmod, user_agent, v_codes, __iced_k, __iced_k_noop;
4
4
 
5
5
  iced = require('iced-runtime');
6
6
  __iced_k = __iced_k_noop = function() {};
@@ -19,6 +19,8 @@
19
19
 
20
20
  urlmod = require('url');
21
21
 
22
+ callbackify = require('util').callbackify;
23
+
22
24
  exports.user_agent = user_agent = constants.user_agent + pkg.version;
23
25
 
24
26
  BaseScraper = (function() {
@@ -92,7 +94,7 @@
92
94
  (function(__iced_k) {
93
95
  __iced_deferrals = new iced.Deferrals(__iced_k, {
94
96
  parent: ___iced_passed_deferral,
95
- filename: "/Users/joshblum/go/src/github.com/keybase/proofs/src/scrapers/base.iced",
97
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/base.iced",
96
98
  funcname: "BaseScraper.validate"
97
99
  });
98
100
  _this.check_status(args, __iced_deferrals.defer({
@@ -102,7 +104,7 @@
102
104
  return rc = arguments[1];
103
105
  };
104
106
  })(),
105
- lineno: 52
107
+ lineno: 53
106
108
  }));
107
109
  __iced_deferrals._fulfill();
108
110
  })(__iced_k);
@@ -140,7 +142,7 @@
140
142
  };
141
143
 
142
144
  BaseScraper.prototype._get_url_body = function(opts, cb) {
143
- var err, rc, response, rl_limit, rl_remaining, rl_reset, ___iced_passed_deferral, __iced_deferrals, __iced_k, _base;
145
+ var body, err, f, rc, response, rl_limit, rl_remaining, rl_reset, ___iced_passed_deferral, __iced_deferrals, __iced_k, _base;
144
146
  __iced_k = __iced_k_noop;
145
147
  ___iced_passed_deferral = iced.findDeferral(arguments);
146
148
 
@@ -158,7 +160,7 @@
158
160
  return (function(__iced_k) {
159
161
  __iced_deferrals = new iced.Deferrals(__iced_k, {
160
162
  parent: ___iced_passed_deferral,
161
- filename: "/Users/joshblum/go/src/github.com/keybase/proofs/src/scrapers/base.iced",
163
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/base.iced",
162
164
  funcname: "BaseScraper._get_url_body"
163
165
  });
164
166
  _this.libs.fetch(opts.url, opts, __iced_deferrals.defer({
@@ -168,7 +170,7 @@
168
170
  return response = arguments[1];
169
171
  };
170
172
  })(),
171
- lineno: 86
173
+ lineno: 87
172
174
  }));
173
175
  __iced_deferrals._fulfill();
174
176
  });
@@ -192,22 +194,45 @@
192
194
  }
193
195
  rc = typeof err !== "undefined" && err !== null ? err.message.includes('network timeout') ? v_codes.TIMEOUT : v_codes.HOST_UNREACHABLE : ((_ref = response.status) === 401 || _ref === 403) ? v_codes.PERMISSION_DENIED : response.status === 200 ? v_codes.OK : response.status >= 500 ? v_codes.HTTP_500 : response.status >= 400 ? v_codes.HTTP_400 : response.status >= 300 ? v_codes.HTTP_300 : v_codes.HTTP_OTHER;
194
196
  if (rc === v_codes.OK) {
195
- try {
196
- if (opts.json) {
197
- return response.json().then(function(body) {
198
- return cb(err, rc, body);
199
- });
200
- } else {
201
- return response.text().then(function(body) {
202
- return cb(err, rc, body);
203
- });
197
+ (function(__iced_k) {
198
+ try {
199
+ if (opts.json) {
200
+ f = callbackify(response.json);
201
+ (function(__iced_k) {
202
+ __iced_deferrals = new iced.Deferrals(__iced_k, {
203
+ parent: ___iced_passed_deferral,
204
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/base.iced",
205
+ funcname: "BaseScraper._get_url_body"
206
+ });
207
+ f.call(response, __iced_deferrals.defer({
208
+ assign_fn: (function() {
209
+ return function() {
210
+ err = arguments[0];
211
+ return body = arguments[1];
212
+ };
213
+ })(),
214
+ lineno: 109
215
+ }));
216
+ __iced_deferrals._fulfill();
217
+ })(function() {
218
+ if (err) {
219
+ _this.log("| _get_url_body response.json() failed with: " + (err.toString()));
220
+ rc = v_codes.CONTENT_FAILURE;
221
+ }
222
+ return __iced_k(cb(err, rc, body));
223
+ });
224
+ } else {
225
+ return __iced_k(response.text().then(function(body) {
226
+ return cb(err, rc, body);
227
+ }));
228
+ }
229
+ } catch (_error) {
230
+ err = _error;
231
+ return cb(err, rc, null);
204
232
  }
205
- } catch (_error) {
206
- err = _error;
207
- return cb(err, rc, null);
208
- }
233
+ })(__iced_k);
209
234
  } else {
210
- return cb(err, rc, null);
235
+ return __iced_k(cb(err, rc, null));
211
236
  }
212
237
  };
213
238
  })(this));
@@ -74,7 +74,7 @@
74
74
  return [err, rc];
75
75
  }
76
76
  found = obj.find(function(x) {
77
- return sncmp(x.kb_username, kb_username) && sncmp(x.sig_hash, sig_id);
77
+ return x && sncmp(x.kb_username, kb_username) && sncmp(x.sig_hash, sig_id);
78
78
  });
79
79
  if (found != null) {
80
80
  rc = v_codes.OK;
@@ -100,7 +100,7 @@
100
100
  return (function(__iced_k) {
101
101
  __iced_deferrals = new iced.Deferrals(__iced_k, {
102
102
  parent: ___iced_passed_deferral,
103
- filename: "/Users/max/src/keybase/proofs/src/scrapers/generic_social.iced",
103
+ filename: "/home/sy/go/src/github.com/keybase/proofs/src/scrapers/generic_social.iced",
104
104
  funcname: "GenericSocialScraper.check_status"
105
105
  });
106
106
  _this._get_url_body({
@@ -1,6 +1,6 @@
1
- // Generated by IcedCoffeeScript 108.0.11
1
+ // Generated by IcedCoffeeScript 108.0.12
2
2
  (function() {
3
- var BaseScraper, BearerToken, Lock, TwitterScraper, bearer_token, constants, decode_sig, iced, make_ids, sncmp, urlmod, v_codes, ws_normalize, __iced_k, __iced_k_noop, _bearer_token, _ref,
3
+ var BaseScraper, BearerToken, Lock, TwitterScraper, bearer_token, constants, decode_sig, iced, make_ids, schema, sncmp, urlmod, v_codes, ws_normalize, __iced_k, __iced_k_noop, _bearer_token, _ref,
4
4
  __hasProp = {}.hasOwnProperty,
5
5
  __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
6
6
 
@@ -21,6 +21,8 @@
21
21
 
22
22
  urlmod = require('url');
23
23
 
24
+ schema = require('../schema3');
25
+
24
26
  ws_normalize = function(x) {
25
27
  var v;
26
28
  v = x.split(/[\t\r\n ]+/);
@@ -50,11 +52,11 @@
50
52
  return (function(__iced_k) {
51
53
  __iced_deferrals = new iced.Deferrals(__iced_k, {
52
54
  parent: ___iced_passed_deferral,
53
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
55
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
54
56
  funcname: "BearerToken.get"
55
57
  });
56
58
  _this._lock.acquire(__iced_deferrals.defer({
57
- lineno: 31
59
+ lineno: 32
58
60
  }));
59
61
  __iced_deferrals._fulfill();
60
62
  });
@@ -80,7 +82,7 @@
80
82
  (function(__iced_k) {
81
83
  __iced_deferrals = new iced.Deferrals(__iced_k, {
82
84
  parent: ___iced_passed_deferral,
83
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
85
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
84
86
  funcname: "BearerToken.get"
85
87
  });
86
88
  _this.base._get_url_body(opts, __iced_deferrals.defer({
@@ -91,7 +93,7 @@
91
93
  return body = arguments[2];
92
94
  };
93
95
  })(),
94
- lineno: 51
96
+ lineno: 52
95
97
  }));
96
98
  __iced_deferrals._fulfill();
97
99
  })(function() {
@@ -193,7 +195,7 @@
193
195
  return (function(__iced_k) {
194
196
  __iced_deferrals = new iced.Deferrals(__iced_k, {
195
197
  parent: ___iced_passed_deferral,
196
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
198
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
197
199
  funcname: "TwitterScraper.hunt2"
198
200
  });
199
201
  _this._get_body_api({
@@ -206,7 +208,7 @@
206
208
  return json = arguments[2];
207
209
  };
208
210
  })(),
209
- lineno: 123
211
+ lineno: 124
210
212
  }));
211
213
  __iced_deferrals._fulfill();
212
214
  });
@@ -293,7 +295,7 @@
293
295
  (function(__iced_k) {
294
296
  __iced_deferrals = new iced.Deferrals(__iced_k, {
295
297
  parent: ___iced_passed_deferral,
296
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
298
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
297
299
  funcname: "TwitterScraper.users_lookup"
298
300
  });
299
301
  _this._get_body_api({
@@ -306,7 +308,7 @@
306
308
  return json = arguments[2];
307
309
  };
308
310
  })(),
309
- lineno: 171
311
+ lineno: 172
310
312
  }));
311
313
  __iced_deferrals._fulfill();
312
314
  })(function() {
@@ -336,11 +338,11 @@
336
338
  (function(__iced_k) {
337
339
  __iced_deferrals = new iced.Deferrals(__iced_k, {
338
340
  parent: ___iced_passed_deferral,
339
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
341
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
340
342
  funcname: "TwitterScraper.users_lookup"
341
343
  });
342
344
  setTimeout(__iced_deferrals.defer({
343
- lineno: 185
345
+ lineno: 186
344
346
  }), cursor_wait);
345
347
  __iced_deferrals._fulfill();
346
348
  })(__iced_k);
@@ -422,7 +424,7 @@
422
424
  (function(__iced_k) {
423
425
  __iced_deferrals = new iced.Deferrals(__iced_k, {
424
426
  parent: ___iced_passed_deferral,
425
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
427
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
426
428
  funcname: "TwitterScraper.get_follower_ids"
427
429
  });
428
430
  _this._get_body_api({
@@ -435,7 +437,7 @@
435
437
  return json = arguments[2];
436
438
  };
437
439
  })(),
438
- lineno: 223
440
+ lineno: 224
439
441
  }));
440
442
  __iced_deferrals._fulfill();
441
443
  })(function() {
@@ -468,11 +470,11 @@
468
470
  (function(__iced_k) {
469
471
  __iced_deferrals = new iced.Deferrals(__iced_k, {
470
472
  parent: ___iced_passed_deferral,
471
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
473
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
472
474
  funcname: "TwitterScraper.get_follower_ids"
473
475
  });
474
476
  setTimeout(__iced_deferrals.defer({
475
- lineno: 239
477
+ lineno: 240
476
478
  }), cursor_wait);
477
479
  __iced_deferrals._fulfill();
478
480
  })(__iced_k);
@@ -555,7 +557,7 @@
555
557
  };
556
558
 
557
559
  TwitterScraper.prototype.check_status = function(_arg, cb) {
558
- var $, api_url, div, err, found_tweet_id, found_username, html, new_api_url, proof_text_check, rc, remote_id, u, username, ___iced_passed_deferral, __iced_deferrals, __iced_k;
560
+ var $, api_url, api_url_matches, author_url_matches, author_username, body_obj, err, new_api_url, proof_text_check, rc, remote_id, schm, tweet_id, tweet_p, u, username, username_from_url, _, ___iced_passed_deferral, __iced_deferrals, __iced_k;
559
561
  __iced_k = __iced_k_noop;
560
562
  ___iced_passed_deferral = iced.findDeferral(arguments);
561
563
  username = _arg.username, api_url = _arg.api_url, proof_text_check = _arg.proof_text_check, remote_id = _arg.remote_id;
@@ -565,48 +567,85 @@
565
567
  this.log("null api_url for " + remote_id + "/" + username);
566
568
  return cb(err, rc);
567
569
  }
568
- u = urlmod.parse(api_url);
569
- u.host = u.hostname = 'mobile.twitter.com';
570
+ u = new urlmod.URL('https://api.twitter.com/1/statuses/oembed.json');
571
+ u.searchParams.set('id', remote_id.toString());
572
+ u.searchParams.set('omit_script', '1');
570
573
  new_api_url = urlmod.format(u);
571
- this.log("| rewrite " + api_url + " -> " + new_api_url + " (since twitter was going to redirect us anyways)");
572
- api_url = new_api_url;
574
+ this.log("| use oembed API " + new_api_url + " for tweet at " + api_url + " (remote_id=" + remote_id + ")");
573
575
  (function(_this) {
574
576
  return (function(__iced_k) {
575
577
  __iced_deferrals = new iced.Deferrals(__iced_k, {
576
578
  parent: ___iced_passed_deferral,
577
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
579
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
578
580
  funcname: "TwitterScraper.check_status"
579
581
  });
580
582
  _this._get_url_body({
581
- url: api_url
583
+ url: new_api_url,
584
+ json: true
582
585
  }, __iced_deferrals.defer({
583
586
  assign_fn: (function() {
584
587
  return function() {
585
588
  err = arguments[0];
586
589
  rc = arguments[1];
587
- return html = arguments[2];
590
+ return body_obj = arguments[2];
588
591
  };
589
592
  })(),
590
- lineno: 321
593
+ lineno: 331
591
594
  }));
592
595
  __iced_deferrals._fulfill();
593
596
  });
594
597
  })(this)((function(_this) {
595
598
  return function() {
596
- var _ref1;
597
- if (rc === v_codes.OK) {
598
- $ = _this.libs.cheerio.load(html);
599
- div = $('.main-tweet .tweet-text');
600
- if (div.length !== 1) {
601
- rc = v_codes.FAILED_PARSE;
602
- } else {
603
- found_username = (_ref1 = $('.main-tweet .username').text()) != null ? _ref1.trim().replace(/^@/g, '') : void 0;
604
- found_tweet_id = "" + (div.data('id'));
605
- rc = !(sncmp(username, found_username)) ? v_codes.BAD_USERNAME : ("" + remote_id) !== found_tweet_id ? v_codes.BAD_REMOTE_ID : _this.find_sig_in_tweet({
606
- tweet_p: div,
607
- proof_text_check: proof_text_check
608
- });
599
+ if (!err && rc === v_codes.OK) {
600
+ schm = schema.dict({
601
+ url: schema.string().name('url'),
602
+ author_url: schema.string().name('author_url'),
603
+ html: schema.string().name('html')
604
+ }).allow_extra_keys();
605
+ if (err = schm.check(body_obj)) {
606
+ return cb(err, v_codes.CONTENT_FAILURE);
607
+ }
608
+ if (!(sncmp(body_obj.url, api_url))) {
609
+ err = new Error("returned url field doesn't match api_url (found: " + body_obj.url + ", expected: " + api_url + ")");
610
+ return cb(err, v_codes.CONTENT_FAILURE);
611
+ }
612
+ api_url_matches = api_url.match(new RegExp("^https://twitter\\.com/([^/]+)/status/(\\d+)(.*)$"));
613
+ if (!api_url_matches) {
614
+ err = new Error("api_url field doesn't match regexp, got: " + api_url_matches);
615
+ return cb(err, v_codes.CONTENT_FAILURE);
616
+ }
617
+ _ = api_url_matches[0], username_from_url = api_url_matches[1], tweet_id = api_url_matches[2];
618
+ if (!(sncmp(username, username_from_url))) {
619
+ err = new Error("username from api_url didn't match, expected: " + username + ", got: " + username_from_url);
620
+ return cb(err, v_codes.BAD_USERNAME);
621
+ }
622
+ if (tweet_id !== remote_id.toString()) {
623
+ return cb(err, v_codes.BAD_REMOTE_ID);
624
+ }
625
+ author_url_matches = body_obj.author_url.match(new RegExp("^https://twitter\\.com/(.+)$"));
626
+ if (!author_url_matches) {
627
+ err = new Error("author_url doesn't match regexp, got: " + body_obj.author_url);
628
+ return cb(err, v_codes.CONTENT_FAILURE);
629
+ }
630
+ _ = author_url_matches[0], author_username = author_url_matches[1];
631
+ if (!(sncmp(username, author_username))) {
632
+ err = new Error("username from author_url didn't match, expected: " + username + ", got: " + author_username);
633
+ return cb(err, v_codes.BAD_USERNAME);
634
+ }
635
+ if (body_obj.html.length > 1000) {
636
+ err = new Error("html is " + body_obj.html.length + " characters, not trying to parse it");
637
+ return cb(err, v_codes.CONTENT_FAILURE);
638
+ }
639
+ $ = _this.libs.cheerio.load(body_obj.html);
640
+ tweet_p = $('blockquote.twitter-tweet p');
641
+ if (tweet_p.length !== 1) {
642
+ err = new Error("failed to find tweet <p> in returned 'html' field");
643
+ return cb(err, v_codes.FAILED_PARSE);
609
644
  }
645
+ rc = _this.find_sig_in_tweet({
646
+ tweet_p: tweet_p,
647
+ proof_text_check: proof_text_check
648
+ });
610
649
  }
611
650
  return cb(err, rc);
612
651
  };
@@ -624,7 +663,7 @@
624
663
  return (function(__iced_k) {
625
664
  __iced_deferrals = new iced.Deferrals(__iced_k, {
626
665
  parent: ___iced_passed_deferral,
627
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
666
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
628
667
  funcname: "TwitterScraper._get_bearer_token"
629
668
  });
630
669
  bt.get(__iced_deferrals.defer({
@@ -634,7 +673,7 @@
634
673
  return tok = arguments[1];
635
674
  };
636
675
  })(),
637
- lineno: 350
676
+ lineno: 392
638
677
  }));
639
678
  __iced_deferrals._fulfill();
640
679
  });
@@ -656,7 +695,7 @@
656
695
  return (function(__iced_k) {
657
696
  __iced_deferrals = new iced.Deferrals(__iced_k, {
658
697
  parent: ___iced_passed_deferral,
659
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
698
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
660
699
  funcname: "TwitterScraper._get_body_api"
661
700
  });
662
701
  _this._get_bearer_token(__iced_deferrals.defer({
@@ -667,7 +706,7 @@
667
706
  return tok = arguments[2];
668
707
  };
669
708
  })(),
670
- lineno: 359
709
+ lineno: 401
671
710
  }));
672
711
  __iced_deferrals._fulfill();
673
712
  });
@@ -687,7 +726,7 @@
687
726
  (function(__iced_k) {
688
727
  __iced_deferrals = new iced.Deferrals(__iced_k, {
689
728
  parent: ___iced_passed_deferral,
690
- filename: "/Users/miles/go/src/github.com/keybase/proofs/src/scrapers/twitter.iced",
729
+ filename: "/home/zapu/Projects/KB/proofs/src/scrapers/twitter.iced",
691
730
  funcname: "TwitterScraper._get_body_api"
692
731
  });
693
732
  _this._get_url_body(args, __iced_deferrals.defer({
@@ -698,7 +737,7 @@
698
737
  return body = arguments[2];
699
738
  };
700
739
  })(),
701
- lineno: 368
740
+ lineno: 410
702
741
  }));
703
742
  __iced_deferrals._fulfill();
704
743
  })(__iced_k);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keybase-proofs",
3
- "version": "2.3.23",
3
+ "version": "2.3.25",
4
4
  "description": "Publicly-verifiable proofs of identity",
5
5
  "main": "lib/main.js",
6
6
  "scripts": {
@@ -36,4 +36,4 @@
36
36
  "iced-coffee-script": "^108.0.11",
37
37
  "iced-test": "0.0.22"
38
38
  }
39
- }
39
+ }
package/src/schema3.iced CHANGED
@@ -39,6 +39,8 @@ class Node
39
39
  class Dict extends Node
40
40
  constructor : ({keys}) ->
41
41
  @_keys = keys
42
+ # do not fail if there are extra keys unknown to schema
43
+ @_allow_extra_keys = false
42
44
  super
43
45
 
44
46
  _check : ({path, obj}) ->
@@ -46,7 +48,9 @@ class Dict extends Node
46
48
  return mkerr path, "need a dictionary"
47
49
  for k,v of obj
48
50
  new_path = path.extend(k)
49
- if not (checker = @_keys[k])? then return mkerr new_path, "key is not supported"
51
+ if not (checker = @_keys[k])?
52
+ if @_allow_extra_keys then continue
53
+ return mkerr new_path, "key is not supported"
50
54
  if (err = @_check_value { checker, path : new_path, obj : v }) then return err
51
55
  for k,v of @_keys
52
56
  new_path = path.extend(k)
@@ -62,6 +66,10 @@ class Dict extends Node
62
66
  set_key : (k,v) ->
63
67
  @_keys[k] = v
64
68
 
69
+ allow_extra_keys : () ->
70
+ @_allow_extra_keys = true
71
+ @
72
+
65
73
  class Array extends Node
66
74
 
67
75
  constructor : ({elem}) ->
@@ -5,6 +5,7 @@ pkg = require '../../package.json'
5
5
  {space_normalize} = require '../util'
6
6
  {b64find} = require '../b64extract'
7
7
  urlmod = require 'url'
8
+ {callbackify} = require 'util'
8
9
 
9
10
  #==============================================================
10
11
 
@@ -105,8 +106,12 @@ class BaseScraper
105
106
  if rc is v_codes.OK
106
107
  try
107
108
  if opts.json
108
- response.json().then (body) ->
109
- cb err, rc, body
109
+ f = callbackify response.json
110
+ await f.call response, defer err, body
111
+ if err
112
+ @log "| _get_url_body response.json() failed with: #{err.toString()}"
113
+ rc = v_codes.CONTENT_FAILURE
114
+ cb err, rc, body
110
115
  else
111
116
  response.text().then (body) ->
112
117
  cb err, rc, body
@@ -52,7 +52,7 @@ exports.GenericSocialScraper = class GenericSocialScraper extends BaseScraper
52
52
  unless obj? and Array.isArray obj
53
53
  err = new Error "did not find proof list on #{check_path.join('.')} in json data"
54
54
  return [err, rc]
55
- found = obj.find (x) -> sncmp(x.kb_username, kb_username) and sncmp(x.sig_hash, sig_id)
55
+ found = obj.find (x) -> x and sncmp(x.kb_username, kb_username) and sncmp(x.sig_hash, sig_id)
56
56
  if found?
57
57
  rc = v_codes.OK
58
58
  else
@@ -5,6 +5,7 @@
5
5
  {v_codes} = constants
6
6
  {decode_sig} = require('kbpgp').ukm
7
7
  urlmod = require 'url'
8
+ schema = require '../schema3'
8
9
 
9
10
  #================================================================================
10
11
 
@@ -312,35 +313,76 @@ exports.TwitterScraper = class TwitterScraper extends BaseScraper
312
313
  @log "null api_url for #{remote_id}/#{username}"
313
314
  return cb err, rc
314
315
 
315
- u = urlmod.parse api_url
316
- u.host = u.hostname = 'mobile.twitter.com'
316
+ # Do not try to fetch api_url which is of form:
317
+ # "https://twitter.com/tacovontaco/status/673931888088625152"
318
+ # because Twitter serves a JavaScript-based page there and the contents of
319
+ # tweet are not available. Instead, construct a new url to oembed API with
320
+ # the tweet ID. That API doesn't require authentication and claims not to
321
+ # be rate limited:
322
+ # https://developer.twitter.com/en/docs/twitter-api/v1/tweets/post-and-engage/api-reference/get-statuses-oembed
323
+ u = new urlmod.URL('https://api.twitter.com/1/statuses/oembed.json')
324
+ u.searchParams.set('id', remote_id.toString())
325
+ # tell twitter not to include their <script> tag in result.
326
+ u.searchParams.set('omit_script', '1')
327
+
317
328
  new_api_url = urlmod.format u
318
- @log "| rewrite #{api_url} -> #{new_api_url} (since twitter was going to redirect us anyways)"
319
- api_url = new_api_url
329
+ @log "| use oembed API #{new_api_url} for tweet at #{api_url} (remote_id=#{remote_id})"
320
330
 
321
331
  # calls back with a v_code or null if it was ok
322
- await @_get_url_body { url : api_url }, defer err, rc, html
323
-
324
- if rc is v_codes.OK
325
-
326
- $ = @libs.cheerio.load html
327
- #
328
- # only look inside the permalink tweet container
329
- #
330
- div = $('.main-tweet .tweet-text')
331
- if div.length isnt 1
332
- rc = v_codes.FAILED_PARSE
333
- else
334
- #
335
- # make sure both the username and tweet id match our query,
336
- # in case twitter printed other tweets into the page
337
- # inside this container
338
- #
339
- found_username = $('.main-tweet .username').text()?.trim().replace( /^@/g , '')
340
- found_tweet_id = "#{div.data('id')}"
341
- rc = if not(sncmp(username, found_username)) then v_codes.BAD_USERNAME
342
- else if (("" + remote_id) isnt (found_tweet_id)) then v_codes.BAD_REMOTE_ID
343
- else @find_sig_in_tweet { tweet_p : div, proof_text_check }
332
+ await @_get_url_body { url : new_api_url, json : true }, defer err, rc, body_obj
333
+
334
+ if not err and rc is v_codes.OK
335
+ schm = schema.dict({
336
+ url: schema.string().name('url')
337
+ author_url: schema.string().name('author_url')
338
+ html: schema.string().name('html')
339
+ }).allow_extra_keys()
340
+ if err = schm.check body_obj
341
+ return cb err, v_codes.CONTENT_FAILURE
342
+
343
+ # "url" field returned by the API should match api_url.
344
+ if not(sncmp(body_obj.url, api_url))
345
+ err = new Error "returned url field doesn't match api_url (found: #{body_obj.url}, expected: #{api_url})"
346
+ return cb err, v_codes.CONTENT_FAILURE
347
+
348
+ # Extract username from URL, do not use "author_name" because it's the
349
+ # full name.
350
+ api_url_matches = api_url.match(new RegExp("^https://twitter\\.com/([^/]+)/status/(\\d+)(.*)$"))
351
+ if not api_url_matches
352
+ err = new Error "api_url field doesn't match regexp, got: #{api_url_matches}"
353
+ return cb err, v_codes.CONTENT_FAILURE
354
+
355
+ # Check username and tweet ID.
356
+ [_, username_from_url, tweet_id] = api_url_matches
357
+ if not(sncmp(username, username_from_url))
358
+ err = new Error("username from api_url didn't match, expected: #{username}, got: #{username_from_url}")
359
+ return cb err, v_codes.BAD_USERNAME
360
+ if tweet_id isnt remote_id.toString()
361
+ return cb err, v_codes.BAD_REMOTE_ID
362
+
363
+ # Check "author_url" field, it contains a link to user's twitter profile.
364
+ # It should match our username.
365
+ author_url_matches = body_obj.author_url.match(new RegExp("^https://twitter\\.com/(.+)$"))
366
+ if not author_url_matches
367
+ err = new Error("author_url doesn't match regexp, got: #{body_obj.author_url}")
368
+ return cb err, v_codes.CONTENT_FAILURE
369
+
370
+ [_, author_username] = author_url_matches
371
+ if not(sncmp(username, author_username))
372
+ err = new Error("username from author_url didn't match, expected: #{username}, got: #{author_username}")
373
+ return cb err, v_codes.BAD_USERNAME
374
+
375
+ if body_obj.html.length > 1000
376
+ err = new Error("html is #{body_obj.html.length} characters, not trying to parse it")
377
+ return cb err, v_codes.CONTENT_FAILURE
378
+
379
+ $ = @libs.cheerio.load body_obj.html
380
+ tweet_p = $('blockquote.twitter-tweet p')
381
+ if tweet_p.length isnt 1
382
+ err = new Error("failed to find tweet <p> in returned 'html' field")
383
+ return cb err, v_codes.FAILED_PARSE
384
+
385
+ rc = @find_sig_in_tweet { tweet_p, proof_text_check }
344
386
 
345
387
  cb err, rc
346
388