diff --git a/README.md b/README.md index d7d24ba..f22c564 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ The contents of the URL is checked in the following order: * A silo URL from one of the following websites: ** Instagram +** Twitter ** (more coming soon) * h-entry, h-event, h-card * OEmbed (coming soon) @@ -44,6 +45,18 @@ url=https://aaronparecki.com/2016/01/16/11/ &token=12341234123412341234 ``` +### Twitter Authentication + +XRay uses the Twitter API to fetch posts, and the Twitter API requires authentication. In order to keep XRay stateless, it is required that you pass in Twitter credentials to the parse call. You can register an application on the Twitter developer website, and generate an access token for your account without writing any code, and then use those credentials when making an API request to XRay. + +You should only send Twitter credentials when the URL you are trying to parse is a Twitter URL, so you'll want to check for whether the hostname is `twitter.com` before you include credentials in this call. + +* twitter_api_key - Your application's API key +* twitter_api_secret - Your application's API secret +* twitter_access_token - Your Twitter access token +* twitter_access_token_secret - Your Twitter secret access token + + ### Error Response ```json diff --git a/composer.json b/composer.json index fb644f4..a33d5ae 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "league/route": "1.*", "mf2/mf2": "~0.3", "ezyang/htmlpurifier": "4.*", - "indieweb/link-rel-parser": "0.1.*" + "indieweb/link-rel-parser": "0.1.*", + "dg/twitter-php": "^3.6" }, "autoload": { "files": [ @@ -19,6 +20,7 @@ "lib/HTTP.php", "lib/Formats/Mf2.php", "lib/Formats/Instagram.php", + "lib/Formats/Twitter.php", "lib/Formats/HTMLPurifier_AttrDef_HTML_Microformats2.php" ] }, diff --git a/composer.lock b/composer.lock index 711aa68..39f4662 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,50 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "b55b9f1fabddb79bf5e8b0daf6b4f88e", - "content-hash": "a791e61b2f956830a8b7d9b0a2493148", + "content-hash": "e5c15a7976be20fd9ed6f7cc48096672", "packages": [ + { + "name": "dg/twitter-php", + "version": "v3.6", + "source": { + "type": "git", + "url": "https://github.com/dg/twitter-php.git", + "reference": "dd872ad12121ff919b358989e61f7f08ba6cc7a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/twitter-php/zipball/dd872ad12121ff919b358989e61f7f08ba6cc7a8", + "reference": "dd872ad12121ff919b358989e61f7f08ba6cc7a8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.2.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Small and easy Twitter library for PHP", + "homepage": "https://github.com/dg/twitter-php", + "keywords": [ + "oauth", + "twitter" + ], + "time": "2016-08-15T16:46:22+00:00" + }, { "name": "ezyang/htmlpurifier", "version": "v4.8.0", @@ -49,7 +90,7 @@ "keywords": [ "html" ], - "time": "2016-07-16 12:58:58" + "time": "2016-07-16T12:58:58+00:00" }, { "name": "indieweb/link-rel-parser", @@ -95,7 +136,7 @@ "indieweb", "microformats2" ], - "time": "2016-04-13 17:48:59" + "time": "2016-04-13T17:48:59+00:00" }, { "name": "ircmaxell/password-compat", @@ -137,7 +178,7 @@ "hashing", "password" ], - "time": "2014-11-20 16:49:30" + "time": "2014-11-20T16:49:30+00:00" }, { "name": "league/container", @@ -195,7 +236,7 @@ "injection", "league" ], - "time": "2015-04-05 17:14:48" + "time": "2015-04-05T17:14:48+00:00" }, { "name": "league/plates", @@ -247,7 +288,7 @@ "templating", "views" ], - "time": "2015-07-09 02:14:40" + "time": "2015-07-09T02:14:40+00:00" }, { "name": "league/route", @@ -305,7 +346,7 @@ "league", "route" ], - "time": "2015-09-11 07:40:31" + "time": "2015-09-11T07:40:31+00:00" }, { "name": "mf2/mf2", @@ -358,7 +399,7 @@ "parser", "semantic" ], - "time": "2016-03-14 12:13:34" + "time": "2016-03-14T12:13:34+00:00" }, { "name": "nikic/fast-route", @@ -401,7 +442,7 @@ "router", "routing" ], - "time": "2016-03-25 23:46:52" + "time": "2016-03-25T23:46:52+00:00" }, { "name": "symfony/http-foundation", @@ -456,7 +497,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2016-03-27 12:57:53" + "time": "2016-03-27T12:57:53+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -515,7 +556,7 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-01-20T09:13:37+00:00" }, { "name": "symfony/polyfill-php54", @@ -573,7 +614,7 @@ "portable", "shim" ], - "time": "2016-01-25 19:13:00" + "time": "2016-01-25T19:13:00+00:00" }, { "name": "symfony/polyfill-php55", @@ -629,7 +670,7 @@ "portable", "shim" ], - "time": "2016-01-20 09:13:37" + "time": "2016-01-20T09:13:37+00:00" } ], "packages-dev": [], diff --git a/controllers/Parse.php b/controllers/Parse.php index 7e03100..584b094 100644 --- a/controllers/Parse.php +++ b/controllers/Parse.php @@ -90,6 +90,45 @@ class Parse { $url = \normalize_url($url); + // Check if this is a Twitter URL and if they've provided API credentials, use the API + if(preg_match('/https?:\/\/(?:mobile\.twitter\.com|twitter\.com|twtr\.io)\/(?:[a-z0-9_\/!#]+statuse?s?\/([0-9]+)|([a-zA-Z0-9_]+))/', $url, $match)) { + $fields = ['twitter_api_key','twitter_api_secret','twitter_access_token','twitter_access_token_secret']; + $creds = []; + foreach($fields as $f) { + if($v=$request->get($f)) + $creds[$f] = $v; + } + $data = false; + if(count($creds) == 4) { + list($data, $parsed) = Formats\Twitter::parse($url, $match[1], $creds); + } elseif(count($creds) > 0) { + // If only some Twitter credentials were present, return an error + return $this->respond($response, 400, [ + 'error' => 'missing_parameters', + 'error_description' => 'All 4 Twitter credentials must be included in the request' + ]); + } else { + // Accept Tweet JSON and parse that if provided + $json = $request->get('json'); + if($json) { + list($data, $parsed) = Formats\Twitter::parse($url, $match[1], null, $json); + } + // Skip parsing from the Twitter API if they didn't include credentials + } + + if($data) { + if($request->get('include_original')) + $data['original'] = $parsed; + return $this->respond($response, 200, $data); + } else { + return $this->respond($response, 200, [ + 'data' => [ + 'type' => 'unknown' + ] + ]); + } + } + // Now fetch the URL and check for any curl errors // Don't cache the response if a token is used to fetch it if($this->mc && !$request->get('token')) { @@ -145,6 +184,7 @@ class Parse { // Check for known services $host = parse_url($result['url'], PHP_URL_HOST); + if(in_array($host, ['www.instagram.com','instagram.com'])) { list($data, $parsed) = Formats\Instagram::parse($result['body'], $result['url'], $this->http); if($request->get('include_original')) @@ -152,7 +192,6 @@ class Parse { return $this->respond($response, 200, $data); } - // attempt to parse the page as HTML $doc = new DOMDocument(); @$doc->loadHTML(self::toHtmlEntities($result['body'])); diff --git a/lib/Formats/Twitter.php b/lib/Formats/Twitter.php new file mode 100644 index 0000000..c6c8b88 --- /dev/null +++ b/lib/Formats/Twitter.php @@ -0,0 +1,231 @@ +request('statuses/show/'.$tweet_id, 'GET', ['tweet_mode'=>'extended']); + } + + if(!$tweet) + return false; + + $entry = array( + 'type' => 'entry', + 'url' => $url, + 'author' => [ + 'type' => 'card', + 'name' => null, + 'nickname' => null, + 'photo' => null, + 'url' => null + ] + ); + $refs = []; + + // Only use the "display" segment of the text + $text = mb_substr($tweet->full_text, $tweet->display_text_range[0], $tweet->display_text_range[1]-$tweet->display_text_range[0]); + + if(property_exists($tweet, 'retweeted_status')) { + // No content for retweets + + $reposted = $tweet->retweeted_status; + $repostOf = 'https://twitter.com/' . $reposted->user->screen_name . '/status/' . $reposted->id_str; + $entry['repost-of'] = $repostOf; + + list($repostedEntry) = self::parse($repostOf, $reposted->id_str, null, $reposted); + if(isset($repostedEntry['refs'])) { + foreach($repostedEntry['refs'] as $k=>$v) { + $refs[$k] = $v; + } + } + + $refs[$repostOf] = $repostedEntry['data']; + + } else { + // Twitter escapes & as & in the text + $text = html_entity_decode($text); + + $text = self::expandTweetURLs($text, $tweet); + + $entry['content'] = ['text' => $text]; + } + + // Published date + $published = new DateTime($tweet->created_at); + if(property_exists($tweet->user, 'utc_offset')) { + $tz = new DateTimeZone($tweet->user->utc_offset / 3600); + $published->setTimeZone($tz); + } + $entry['published'] = $published->format('c'); + + // Hashtags + if(property_exists($tweet, 'entities') && property_exists($tweet->entities, 'hashtags')) { + if(count($tweet->entities->hashtags)) { + $entry['category'] = []; + foreach($tweet->entities->hashtags as $hashtag) { + $entry['category'][] = $hashtag->text; + } + } + } + + // Photos and Videos + if(property_exists($tweet, 'extended_entities') && property_exists($tweet->extended_entities, 'media')) { + foreach($tweet->extended_entities->media as $media) { + if($media->type == 'photo') { + if(!array_key_exists('photo', $entry)) + $entry['photo'] = []; + + $entry['photo'][] = $media->media_url_https; + + } elseif($media->type == 'video') { + if(!array_key_exists('video', $entry)) + $entry['video'] = []; + + // Find the highest bitrate video that is mp4 + $videos = $media->video_info->variants; + $videos = array_filter($videos, function($v) { + return property_exists($v, 'bitrate') && $v->content_type == 'video/mp4'; + }); + if(count($videos)) { + usort($videos, function($a,$b) { + return $a->bitrate < $b->bitrate; + }); + $entry['video'][] = $videos[0]->url; + } + } + } + } + + // Place + if(property_exists($tweet, 'place') && $tweet->place) { + $place = $tweet->place; + if($place->place_type == 'city') { + $entry['location'] = $place->url; + $refs[$place->url] = [ + 'type' => 'adr', + 'name' => $place->full_name, + 'locality' => $place->name, + 'country-name' => $place->country, + ]; + } + } + + // Quoted Status + if(property_exists($tweet, 'quoted_status')) { + $quoteOf = 'https://twitter.com/' . $tweet->quoted_status->user->screen_name . '/status/' . $tweet->quoted_status_id_str; + list($quoted) = self::parse($quoteOf, $tweet->quoted_status_id_str, null, $tweet->quoted_status); + if(isset($quoted['refs'])) { + foreach($quoted['refs'] as $k=>$v) { + $refs[$k] = $v; + } + } + $refs[$quoteOf] = $quoted['data']; + } + + if($author = self::_buildHCardFromTwitterProfile($tweet->user)) { + $entry['author'] = $author; + } + + $response = [ + 'data' => $entry + ]; + + if(count($refs)) { + $response['refs'] = $refs; + } + + return [$response, $tweet]; + } + + private static function _buildHCardFromTwitterProfile($profile) { + if(!$profile) return false; + + $author = [ + 'type' => 'card' + ]; + + $author['nickname'] = $profile->screen_name; + $author['location'] = $profile->location; + $author['bio'] = self::expandTwitterObjectURLs($profile->description, $profile, 'description'); + + if($profile->name) + $author['name'] = $profile->name; + else + $author['name'] = $profile->screen_name; + + if($profile->url) + $author['url'] = $profile->entities->url->urls[0]->expanded_url; + else + $author['url'] = 'https://twitter.com/' . $profile->screen_name; + + $author['photo'] = $profile->profile_image_url_https; + + return $author; + } + + private static function expandTweetURLs($text, $object) { + if(property_exists($object, 'entities') && property_exists($object->entities, 'urls')) { + foreach($object->entities->urls as $url) { + $text = str_replace($url->url, $url->expanded_url, $text); + } + } + return $text; + } + + private static function expandTwitterObjectURLs($text, $object, $key) { + if(property_exists($object, 'entities') + && property_exists($object->entities, $key) + && property_exists($object->entities->{$key}, 'urls')) { + foreach($object->entities->{$key}->urls as $url) { + $text = str_replace($url->url, $url->expanded_url, $text); + } + } + return $text; + } + + /** + * Converts base 60 to base 10, with error checking + * http://tantek.pbworks.com/NewBase60 + * @param string $s + * @return int + */ + function b60to10($s) + { + $n = 0; + for($i = 0; $i < strlen($s); $i++) // iterate from first to last char of $s + { + $c = ord($s[$i]); // put current ASCII of char into $c + if ($c>=48 && $c<=57) { $c=bcsub($c,48); } + else if ($c>=65 && $c<=72) { $c=bcsub($c,55); } + else if ($c==73 || $c==108) { $c=1; } // typo capital I, lowercase l to 1 + else if ($c>=74 && $c<=78) { $c=bcsub($c,56); } + else if ($c==79) { $c=0; } // error correct typo capital O to 0 + else if ($c>=80 && $c<=90) { $c=bcsub($c,57); } + else if ($c==95) { $c=34; } // underscore + else if ($c>=97 && $c<=107) { $c=bcsub($c,62); } + else if ($c>=109 && $c<=122) { $c=bcsub($c,63); } + else { $c = 0; } // treat all other noise as 0 + $n = bcadd(bcmul(60, $n), $c); + } + return $n; + } + +} diff --git a/tests/InstagramTest.php b/tests/InstagramTest.php new file mode 100644 index 0000000..c771fec --- /dev/null +++ b/tests/InstagramTest.php @@ -0,0 +1,94 @@ +client = new Parse(); + $this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); + $this->client->mc = null; + } + + private function parse($params) { + $request = new Request($params); + $response = new Response(); + return $this->client->parse($request, $response); + } + + public function testInstagramPhoto() { + $url = 'http://www.instagram.com/photo.html'; + $response = $this->parse(['url' => $url]); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body, true); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('2017-01-05T23:31:32+00:00', $data['data']['published']); + $this->assertContains('planning', $data['data']['category']); + $this->assertContains('2017', $data['data']['category']); + $this->assertEquals('Kind of crazy to see the whole year laid out like this. #planning #2017', $data['data']['content']['text']); + $this->assertEquals(1, count($data['data']['photo'])); + $this->assertEquals(['https://scontent.cdninstagram.com/t51.2885-15/e35/15803256_1832278043695907_4846092951052353536_n.jpg?ig_cache_key=MTQyMTM1Nzk0NTMwNTEwMDkwNg%3D%3D.2'], $data['data']['photo']); + $this->assertEquals('http://aaronparecki.com/', $data['data']['author']['url']); + $this->assertEquals('Aaron Parecki', $data['data']['author']['name']); + $this->assertEquals('https://scontent.cdninstagram.com/t51.2885-19/s320x320/14240576_268350536897085_1129715662_a.jpg', $data['data']['author']['photo']); + } + + public function testInstagramVideo() { + $url = 'http://www.instagram.com/video.html'; + $response = $this->parse(['url' => $url]); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body, true); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertContains('100daysofmusic', $data['data']['category']); + $this->assertEquals('Day 18. Maple and Spruce #100daysofmusic #100daysproject #the100dayproject https://aaronparecki.com/2017/01/07/14/day18', $data['data']['content']['text']); + $this->assertEquals(1, count($data['data']['photo'])); + $this->assertEquals(['https://scontent.cdninstagram.com/t51.2885-15/s640x640/e15/15624670_548881701986735_8264383763249627136_n.jpg?ig_cache_key=MTQyMjkzMTczMTg0MjE3NjE3Nw%3D%3D.2'], $data['data']['photo']); + $this->assertEquals(1, count($data['data']['video'])); + $this->assertEquals(['https://scontent.cdninstagram.com/t50.2886-16/15921147_1074837002642259_2269307616507199488_n.mp4'], $data['data']['video']); + $this->assertEquals('http://aaronparecki.com/', $data['data']['author']['url']); + $this->assertEquals('Aaron Parecki', $data['data']['author']['name']); + $this->assertEquals('https://scontent.cdninstagram.com/t51.2885-19/s320x320/14240576_268350536897085_1129715662_a.jpg', $data['data']['author']['photo']); + } + + public function testInstagramPhotoWithPersonTag() { + $url = 'http://www.instagram.com/photo_with_person_tag.html'; + $response = $this->parse(['url' => $url]); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body, true); + + $this->assertEquals(2, count($data['data']['category'])); + $this->assertContains('https://kmikeym.com/', $data['data']['category']); + $this->assertArrayHasKey('https://kmikeym.com/', $data['refs']); + $this->assertEquals(['type'=>'card','name'=>'Mike Merrill','url'=>'https://kmikeym.com/','photo'=>'https://scontent.cdninstagram.com/t51.2885-19/s320x320/12627953_686238411518831_1544976311_a.jpg'], $data['refs']['https://kmikeym.com/']); + } + + public function testInstagramPhotoWithVenue() { + $url = 'http://www.instagram.com/photo_with_venue.html'; + $response = $this->parse(['url' => $url]); + + $body = $response->getContent(); + $this->assertEquals(200, $response->getStatusCode()); + $data = json_decode($body, true); + + $this->assertEquals(1, count($data['data']['location'])); + $this->assertContains('https://www.instagram.com/explore/locations/109284789535230/', $data['data']['location']); + $this->assertArrayHasKey('https://www.instagram.com/explore/locations/109284789535230/', $data['refs']); + $venue = $data['refs']['https://www.instagram.com/explore/locations/109284789535230/']; + $this->assertEquals('XOXO Outpost', $venue['name']); + $this->assertEquals('45.5261002', $venue['latitude']); + $this->assertEquals('-122.6558081', $venue['longitude']); + // Setting a venue should set the timezone + $this->assertEquals('2016-12-10T21:48:56-08:00', $data['data']['published']); + } + +} diff --git a/tests/ParseTest.php b/tests/ParseTest.php index 2864baf..534f328 100644 --- a/tests/ParseTest.php +++ b/tests/ParseTest.php @@ -383,77 +383,4 @@ class ParseTest extends PHPUnit_Framework_TestCase { $this->assertFalse($data['info']['found_fragment']); } - public function testInstagramPhoto() { - $url = 'http://www.instagram.com/photo.html'; - $response = $this->parse(['url' => $url]); - - $body = $response->getContent(); - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body, true); - - $this->assertEquals('entry', $data['data']['type']); - $this->assertEquals('2017-01-05T23:31:32+00:00', $data['data']['published']); - $this->assertContains('planning', $data['data']['category']); - $this->assertContains('2017', $data['data']['category']); - $this->assertEquals('Kind of crazy to see the whole year laid out like this. #planning #2017', $data['data']['content']['text']); - $this->assertEquals(1, count($data['data']['photo'])); - $this->assertEquals(['https://scontent.cdninstagram.com/t51.2885-15/e35/15803256_1832278043695907_4846092951052353536_n.jpg?ig_cache_key=MTQyMTM1Nzk0NTMwNTEwMDkwNg%3D%3D.2'], $data['data']['photo']); - $this->assertEquals('http://aaronparecki.com/', $data['data']['author']['url']); - $this->assertEquals('Aaron Parecki', $data['data']['author']['name']); - $this->assertEquals('https://scontent.cdninstagram.com/t51.2885-19/s320x320/14240576_268350536897085_1129715662_a.jpg', $data['data']['author']['photo']); - } - - public function testInstagramVideo() { - $url = 'http://www.instagram.com/video.html'; - $response = $this->parse(['url' => $url]); - - $body = $response->getContent(); - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body, true); - - $this->assertEquals('entry', $data['data']['type']); - $this->assertContains('100daysofmusic', $data['data']['category']); - $this->assertEquals('Day 18. Maple and Spruce #100daysofmusic #100daysproject #the100dayproject https://aaronparecki.com/2017/01/07/14/day18', $data['data']['content']['text']); - $this->assertEquals(1, count($data['data']['photo'])); - $this->assertEquals(['https://scontent.cdninstagram.com/t51.2885-15/s640x640/e15/15624670_548881701986735_8264383763249627136_n.jpg?ig_cache_key=MTQyMjkzMTczMTg0MjE3NjE3Nw%3D%3D.2'], $data['data']['photo']); - $this->assertEquals(1, count($data['data']['video'])); - $this->assertEquals(['https://scontent.cdninstagram.com/t50.2886-16/15921147_1074837002642259_2269307616507199488_n.mp4'], $data['data']['video']); - $this->assertEquals('http://aaronparecki.com/', $data['data']['author']['url']); - $this->assertEquals('Aaron Parecki', $data['data']['author']['name']); - $this->assertEquals('https://scontent.cdninstagram.com/t51.2885-19/s320x320/14240576_268350536897085_1129715662_a.jpg', $data['data']['author']['photo']); - } - - public function testInstagramPhotoWithPersonTag() { - $url = 'http://www.instagram.com/photo_with_person_tag.html'; - $response = $this->parse(['url' => $url]); - - $body = $response->getContent(); - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body, true); - - $this->assertEquals(2, count($data['data']['category'])); - $this->assertContains('https://kmikeym.com/', $data['data']['category']); - $this->assertArrayHasKey('https://kmikeym.com/', $data['refs']); - $this->assertEquals(['type'=>'card','name'=>'Mike Merrill','url'=>'https://kmikeym.com/','photo'=>'https://scontent.cdninstagram.com/t51.2885-19/s320x320/12627953_686238411518831_1544976311_a.jpg'], $data['refs']['https://kmikeym.com/']); - } - - public function testInstagramPhotoWithVenue() { - $url = 'http://www.instagram.com/photo_with_venue.html'; - $response = $this->parse(['url' => $url]); - - $body = $response->getContent(); - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($body, true); - - $this->assertEquals(1, count($data['data']['location'])); - $this->assertContains('https://www.instagram.com/explore/locations/109284789535230/', $data['data']['location']); - $this->assertArrayHasKey('https://www.instagram.com/explore/locations/109284789535230/', $data['refs']); - $venue = $data['refs']['https://www.instagram.com/explore/locations/109284789535230/']; - $this->assertEquals('XOXO Outpost', $venue['name']); - $this->assertEquals('45.5261002', $venue['latitude']); - $this->assertEquals('-122.6558081', $venue['longitude']); - // Setting a venue should set the timezone - $this->assertEquals('2016-12-10T21:48:56-08:00', $data['data']['published']); - } - } diff --git a/tests/TwitterTest.php b/tests/TwitterTest.php new file mode 100644 index 0000000..1f3c9e8 --- /dev/null +++ b/tests/TwitterTest.php @@ -0,0 +1,143 @@ +client = new Parse(); + $this->client->mc = null; + } + + private function parse($params) { + $request = new Request($params); + $response = new Response(); + $result = $this->client->parse($request, $response); + $body = $result->getContent(); + $this->assertEquals(200, $result->getStatusCode()); + return json_decode($body, true); + } + + private function loadTweet($id) { + $url = 'https://twitter.com/_/status/'.$id; + $json = file_get_contents(dirname(__FILE__).'/data/api.twitter.com/'.$id.'.json'); + return [$url, $json]; + } + + public function testBasicProfileInfo() { + list($url, $json) = $this->loadTweet('818912506496229376'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('aaronpk dev', $data['data']['author']['name']); + $this->assertEquals('pkdev', $data['data']['author']['nickname']); + $this->assertEquals('https://aaronparecki.com/', $data['data']['author']['url']); + $this->assertEquals('Portland, OR', $data['data']['author']['location']); + $this->assertEquals('Dev account for testing Twitter things. Follow me here: https://twitter.com/aaronpk', $data['data']['author']['bio']); + $this->assertEquals('https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg', $data['data']['author']['photo']); + } + + public function testBasicTestStuff() { + list($url, $json) = $this->loadTweet('818913630569664512'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('A tweet with a URL https://indieweb.org/ #and #some #hashtags', $data['data']['content']['text']); + $this->assertContains('and', $data['data']['category']); + $this->assertContains('some', $data['data']['category']); + $this->assertContains('hashtags', $data['data']['category']); + // Published date should be set to the timezone of the user + $this->assertEquals('2017-01-10T12:13:18-08:00', $data['data']['published']); + } + + public function testTweetWithEmoji() { + list($url, $json) = $this->loadTweet('818943244553699328'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Here 🎉 have an emoji', $data['data']['content']['text']); + } + + public function testHTMLEscaping() { + list($url, $json) = $this->loadTweet('818928092383166465'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Double escaping & & amp', $data['data']['content']['text']); + } + + public function testTweetWithPhoto() { + list($url, $json) = $this->loadTweet('818912506496229376'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Tweet with a photo and a location', $data['data']['content']['text']); + $this->assertEquals('https://pbs.twimg.com/media/C11cfRJUoAI26h9.jpg', $data['data']['photo'][0]); + } + + public function testTweetWithTwoPhotos() { + list($url, $json) = $this->loadTweet('818935308813103104'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Two photos', $data['data']['content']['text']); + $this->assertContains('https://pbs.twimg.com/media/C11xS1wUcAAeaKF.jpg', $data['data']['photo']); + $this->assertContains('https://pbs.twimg.com/media/C11wtndUoAE1WfE.jpg', $data['data']['photo']); + } + + public function testTweetWithVideo() { + list($url, $json) = $this->loadTweet('818913178260160512'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Tweet with a video', $data['data']['content']['text']); + $this->assertEquals('https://video.twimg.com/ext_tw_video/818913089248595970/pr/vid/1280x720/qP-sDx-Q0Hs-ckVv.mp4', $data['data']['video'][0]); + } + + public function testTweetWithLocation() { + list($url, $json) = $this->loadTweet('818912506496229376'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Tweet with a photo and a location', $data['data']['content']['text']); + $this->assertEquals('https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json', $data['data']['location']); + $location = $data['refs']['https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json']; + $this->assertEquals('adr', $location['type']); + $this->assertEquals('Portland', $location['locality']); + $this->assertEquals('United States', $location['country-name']); + $this->assertEquals('Portland, OR', $location['name']); + } + + public function testRetweet() { + list($url, $json) = $this->loadTweet('818913351623245824'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertArrayNotHasKey('content', $data['data']); + $repostOf = 'https://twitter.com/aaronpk/status/817414679131660288'; + $this->assertEquals($repostOf, $data['data']['repost-of']); + $tweet = $data['refs'][$repostOf]; + $this->assertEquals('Yeah that\'s me http://xkcd.com/1782/', $tweet['content']['text']); + } + + public function testQuotedTweet() { + list($url, $json) = $this->loadTweet('818913488609251331'); + + $data = $this->parse(['url' => $url, 'json' => $json]); + + $this->assertEquals('entry', $data['data']['type']); + $this->assertEquals('Quoted tweet with a #hashtag https://twitter.com/aaronpk/status/817414679131660288', $data['data']['content']['text']); + $tweet = $data['refs']['https://twitter.com/aaronpk/status/817414679131660288']; + $this->assertEquals('Yeah that\'s me http://xkcd.com/1782/', $tweet['content']['text']); + } + +} diff --git a/tests/data/api.twitter.com/818912506496229376.json b/tests/data/api.twitter.com/818912506496229376.json new file mode 100644 index 0000000..291350f --- /dev/null +++ b/tests/data/api.twitter.com/818912506496229376.json @@ -0,0 +1,227 @@ +{ + "created_at": "Tue Jan 10 20:08:50 +0000 2017", + "id": 818912506496229376, + "id_str": "818912506496229376", + "full_text": "Tweet with a photo and a location https://t.co/GwEzHTHlUC", + "truncated": false, + "display_text_range": [ + 0, + 33 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + + ], + "media": [ + { + "id": 818912399499501570, + "id_str": "818912399499501570", + "indices": [ + 34, + 57 + ], + "media_url": "http://pbs.twimg.com/media/C11cfRJUoAI26h9.jpg", + "media_url_https": "https://pbs.twimg.com/media/C11cfRJUoAI26h9.jpg", + "url": "https://t.co/GwEzHTHlUC", + "display_url": "pic.twitter.com/GwEzHTHlUC", + "expanded_url": "https://twitter.com/pkdev/status/818912506496229376/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 800, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 453, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1365, + "resize": "fit" + } + } + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 818912399499501570, + "id_str": "818912399499501570", + "indices": [ + 34, + 57 + ], + "media_url": "http://pbs.twimg.com/media/C11cfRJUoAI26h9.jpg", + "media_url_https": "https://pbs.twimg.com/media/C11cfRJUoAI26h9.jpg", + "url": "https://t.co/GwEzHTHlUC", + "display_url": "pic.twitter.com/GwEzHTHlUC", + "expanded_url": "https://twitter.com/pkdev/status/818912506496229376/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 800, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 453, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1365, + "resize": "fit" + } + } + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 31, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": false, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} diff --git a/tests/data/api.twitter.com/818913178260160512.json b/tests/data/api.twitter.com/818913178260160512.json new file mode 100644 index 0000000..3decb31 --- /dev/null +++ b/tests/data/api.twitter.com/818913178260160512.json @@ -0,0 +1,262 @@ +{ + "created_at": "Tue Jan 10 20:11:31 +0000 2017", + "id": 818913178260160512, + "id_str": "818913178260160512", + "full_text": "Tweet with a video https://t.co/6hyv5rr3FL", + "truncated": false, + "display_text_range": [ + 0, + 18 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + + ], + "media": [ + { + "id": 818913089248595970, + "id_str": "818913089248595970", + "indices": [ + 19, + 42 + ], + "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/818913089248595970/pr/img/qVoEjF03Y41SKpNt.jpg", + "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/818913089248595970/pr/img/qVoEjF03Y41SKpNt.jpg", + "url": "https://t.co/6hyv5rr3FL", + "display_url": "pic.twitter.com/6hyv5rr3FL", + "expanded_url": "https://twitter.com/pkdev/status/818913178260160512/video/1", + "type": "photo", + "sizes": { + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 600, + "h": 338, + "resize": "fit" + }, + "small": { + "w": 340, + "h": 191, + "resize": "fit" + }, + "large": { + "w": 1024, + "h": 576, + "resize": "fit" + } + } + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 818913089248595970, + "id_str": "818913089248595970", + "indices": [ + 19, + 42 + ], + "media_url": "http://pbs.twimg.com/ext_tw_video_thumb/818913089248595970/pr/img/qVoEjF03Y41SKpNt.jpg", + "media_url_https": "https://pbs.twimg.com/ext_tw_video_thumb/818913089248595970/pr/img/qVoEjF03Y41SKpNt.jpg", + "url": "https://t.co/6hyv5rr3FL", + "display_url": "pic.twitter.com/6hyv5rr3FL", + "expanded_url": "https://twitter.com/pkdev/status/818913178260160512/video/1", + "type": "video", + "sizes": { + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 600, + "h": 338, + "resize": "fit" + }, + "small": { + "w": 340, + "h": 191, + "resize": "fit" + }, + "large": { + "w": 1024, + "h": 576, + "resize": "fit" + } + }, + "video_info": { + "aspect_ratio": [ + 16, + 9 + ], + "duration_millis": 41534, + "variants": [ + { + "content_type": "application/x-mpegURL", + "url": "https://video.twimg.com/ext_tw_video/818913089248595970/pr/pl/TrPaTlyUsAN8GjxN.m3u8" + }, + { + "bitrate": 320000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/818913089248595970/pr/vid/320x180/XMltLv_V-HjjJw3B.mp4" + }, + { + "bitrate": 2176000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/818913089248595970/pr/vid/1280x720/qP-sDx-Q0Hs-ckVv.mp4" + }, + { + "content_type": "application/dash+xml", + "url": "https://video.twimg.com/ext_tw_video/818913089248595970/pr/pl/TrPaTlyUsAN8GjxN.mpd" + }, + { + "bitrate": 832000, + "content_type": "video/mp4", + "url": "https://video.twimg.com/ext_tw_video/818913089248595970/pr/vid/640x360/1oP83JGgjXpDd4WY.mp4" + } + ] + }, + "additional_media_info": { + "monetizable": false + } + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 36, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818913351623245824.json b/tests/data/api.twitter.com/818913351623245824.json new file mode 100644 index 0000000..f6722b6 --- /dev/null +++ b/tests/data/api.twitter.com/818913351623245824.json @@ -0,0 +1,482 @@ +{ + "created_at": "Tue Jan 10 20:12:12 +0000 2017", + "id": 818913351623245824, + "id_str": "818913351623245824", + "full_text": "RT @aaronpk: Yeah that's me https://t.co/6ZjcRmb3ec https://t.co/n0k56i1nSl", + "truncated": false, + "display_text_range": [ + 0, + 75 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + { + "screen_name": "aaronpk", + "name": "Aaron Parecki", + "id": 14447132, + "id_str": "14447132", + "indices": [ + 3, + 11 + ] + } + ], + "urls": [ + { + "url": "https://t.co/6ZjcRmb3ec", + "expanded_url": "http://xkcd.com/1782/", + "display_url": "xkcd.com/1782/", + "indices": [ + 28, + 51 + ] + } + ], + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 52, + 75 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + }, + "source_status_id": 817414679131660288, + "source_status_id_str": "817414679131660288", + "source_user_id": 14447132, + "source_user_id_str": "14447132" + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 52, + 75 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + }, + "source_status_id": 817414679131660288, + "source_status_id_str": "817414679131660288", + "source_user_id": 14447132, + "source_user_id_str": "14447132" + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 37, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": null, + "contributors": null, + "retweeted_status": { + "created_at": "Fri Jan 06 16:57:00 +0000 2017", + "id": 817414679131660288, + "id_str": "817414679131660288", + "full_text": "Yeah that's me https://t.co/6ZjcRmb3ec https://t.co/n0k56i1nSl", + "truncated": false, + "display_text_range": [ + 0, + 38 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + { + "url": "https://t.co/6ZjcRmb3ec", + "expanded_url": "http://xkcd.com/1782/", + "display_url": "xkcd.com/1782/", + "indices": [ + 15, + 38 + ] + } + ], + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 39, + 62 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + } + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 39, + 62 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + } + } + ] + }, + "source": "Silo Pub for p3k", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 14447132, + "id_str": "14447132", + "name": "Aaron Parecki", + "screen_name": "aaronpk", + "location": "Portland, OR", + "description": "Cofounder of #indieweb/@indiewebcamp • I maintain https://t.co/zBgRc3oyhG • Editor of https://t.co/JxW2Ws0Ctv and https://t.co/bjsh7VQ4ZI • @W7APK", + "url": "https://t.co/LvwC5LPQlU", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/LvwC5LPQlU", + "expanded_url": "http://aaronparecki.com", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "https://t.co/zBgRc3oyhG", + "expanded_url": "http://oauth.net", + "display_url": "oauth.net", + "indices": [ + 50, + 73 + ] + }, + { + "url": "https://t.co/JxW2Ws0Ctv", + "expanded_url": "http://w3.org/TR/micropub", + "display_url": "w3.org/TR/micropub", + "indices": [ + 86, + 109 + ] + }, + { + "url": "https://t.co/bjsh7VQ4ZI", + "expanded_url": "http://w3.org/TR/webmention", + "display_url": "w3.org/TR/webmention", + "indices": [ + 114, + 137 + ] + } + ] + } + }, + "protected": false, + "followers_count": 3379, + "friends_count": 1036, + "listed_count": 346, + "created_at": "Sat Apr 19 22:38:15 +0000 2008", + "favourites_count": 3703, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": true, + "statuses_count": 6675, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "7A9AAF", + "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/185835062/4786064324_b7049fbec8_b.jpg", + "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/185835062/4786064324_b7049fbec8_b.jpg", + "profile_background_tile": true, + "profile_image_url": "http://pbs.twimg.com/profile_images/775755455188512768/CA3YxGa4_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/775755455188512768/CA3YxGa4_normal.jpg", + "profile_banner_url": "https://pbs.twimg.com/profile_banners/14447132/1398201184", + "profile_link_color": "FF5900", + "profile_sidebar_border_color": "87BC44", + "profile_sidebar_fill_color": "94C8FF", + "profile_text_color": "000000", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": false, + "default_profile_image": false, + "following": false, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": { + "type": "Point", + "coordinates": [ + 45.53553, + -122.62139 + ] + }, + "coordinates": { + "type": "Point", + "coordinates": [ + -122.62139, + 45.53553 + ] + }, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 3, + "favorite_count": 6, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" + }, + "is_quote_status": false, + "retweet_count": 3, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818913488609251331.json b/tests/data/api.twitter.com/818913488609251331.json new file mode 100644 index 0000000..12569a9 --- /dev/null +++ b/tests/data/api.twitter.com/818913488609251331.json @@ -0,0 +1,431 @@ +{ + "created_at": "Tue Jan 10 20:12:45 +0000 2017", + "id": 818913488609251331, + "id_str": "818913488609251331", + "full_text": "Quoted tweet with a #hashtag https://t.co/m8RAfr0S3e", + "truncated": false, + "display_text_range": [ + 0, + 52 + ], + "entities": { + "hashtags": [ + { + "text": "hashtag", + "indices": [ + 20, + 28 + ] + } + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + { + "url": "https://t.co/m8RAfr0S3e", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288", + "display_url": "twitter.com/aaronpk/status…", + "indices": [ + 29, + 52 + ] + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 37, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": true, + "quoted_status_id": 817414679131660288, + "quoted_status_id_str": "817414679131660288", + "quoted_status": { + "created_at": "Fri Jan 06 16:57:00 +0000 2017", + "id": 817414679131660288, + "id_str": "817414679131660288", + "full_text": "Yeah that's me https://t.co/6ZjcRmb3ec https://t.co/n0k56i1nSl", + "truncated": false, + "display_text_range": [ + 0, + 38 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + { + "url": "https://t.co/6ZjcRmb3ec", + "expanded_url": "http://xkcd.com/1782/", + "display_url": "xkcd.com/1782/", + "indices": [ + 15, + 38 + ] + } + ], + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 39, + 62 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + } + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 817414678586372096, + "id_str": "817414678586372096", + "indices": [ + 39, + 62 + ], + "media_url": "http://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "media_url_https": "https://pbs.twimg.com/media/C1gKUb9UsAAbi2R.jpg", + "url": "https://t.co/n0k56i1nSl", + "display_url": "pic.twitter.com/n0k56i1nSl", + "expanded_url": "https://twitter.com/aaronpk/status/817414679131660288/photo/1", + "type": "photo", + "sizes": { + "small": { + "w": 680, + "h": 290, + "resize": "fit" + }, + "large": { + "w": 700, + "h": 299, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "medium": { + "w": 700, + "h": 299, + "resize": "fit" + } + } + } + ] + }, + "source": "Silo Pub for p3k", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 14447132, + "id_str": "14447132", + "name": "Aaron Parecki", + "screen_name": "aaronpk", + "location": "Portland, OR", + "description": "Cofounder of #indieweb/@indiewebcamp • I maintain https://t.co/zBgRc3oyhG • Editor of https://t.co/JxW2Ws0Ctv and https://t.co/bjsh7VQ4ZI • @W7APK", + "url": "https://t.co/LvwC5LPQlU", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/LvwC5LPQlU", + "expanded_url": "http://aaronparecki.com", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "https://t.co/zBgRc3oyhG", + "expanded_url": "http://oauth.net", + "display_url": "oauth.net", + "indices": [ + 50, + 73 + ] + }, + { + "url": "https://t.co/JxW2Ws0Ctv", + "expanded_url": "http://w3.org/TR/micropub", + "display_url": "w3.org/TR/micropub", + "indices": [ + 86, + 109 + ] + }, + { + "url": "https://t.co/bjsh7VQ4ZI", + "expanded_url": "http://w3.org/TR/webmention", + "display_url": "w3.org/TR/webmention", + "indices": [ + 114, + 137 + ] + } + ] + } + }, + "protected": false, + "followers_count": 3379, + "friends_count": 1036, + "listed_count": 346, + "created_at": "Sat Apr 19 22:38:15 +0000 2008", + "favourites_count": 3703, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": true, + "statuses_count": 6675, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "7A9AAF", + "profile_background_image_url": "http://pbs.twimg.com/profile_background_images/185835062/4786064324_b7049fbec8_b.jpg", + "profile_background_image_url_https": "https://pbs.twimg.com/profile_background_images/185835062/4786064324_b7049fbec8_b.jpg", + "profile_background_tile": true, + "profile_image_url": "http://pbs.twimg.com/profile_images/775755455188512768/CA3YxGa4_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/775755455188512768/CA3YxGa4_normal.jpg", + "profile_banner_url": "https://pbs.twimg.com/profile_banners/14447132/1398201184", + "profile_link_color": "FF5900", + "profile_sidebar_border_color": "87BC44", + "profile_sidebar_fill_color": "94C8FF", + "profile_text_color": "000000", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": false, + "default_profile_image": false, + "following": false, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": { + "type": "Point", + "coordinates": [ + 45.53553, + -122.62139 + ] + }, + "coordinates": { + "type": "Point", + "coordinates": [ + -122.62139, + 45.53553 + ] + }, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 3, + "favorite_count": 6, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" + }, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818913630569664512.json b/tests/data/api.twitter.com/818913630569664512.json new file mode 100644 index 0000000..8e09f86 --- /dev/null +++ b/tests/data/api.twitter.com/818913630569664512.json @@ -0,0 +1,177 @@ +{ + "created_at": "Tue Jan 10 20:13:18 +0000 2017", + "id": 818913630569664512, + "id_str": "818913630569664512", + "full_text": "A tweet with a URL https://t.co/sVq9EOlcNs #and #some #hashtags", + "truncated": false, + "display_text_range": [ + 0, + 63 + ], + "entities": { + "hashtags": [ + { + "text": "and", + "indices": [ + 43, + 47 + ] + }, + { + "text": "some", + "indices": [ + 48, + 53 + ] + }, + { + "text": "hashtags", + "indices": [ + 54, + 63 + ] + } + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + { + "url": "https://t.co/sVq9EOlcNs", + "expanded_url": "https://indieweb.org/", + "display_url": "indieweb.org", + "indices": [ + 19, + 42 + ] + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 35, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": false, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818928092383166465.json b/tests/data/api.twitter.com/818928092383166465.json new file mode 100644 index 0000000..c95c469 --- /dev/null +++ b/tests/data/api.twitter.com/818928092383166465.json @@ -0,0 +1,147 @@ +{ + "created_at": "Tue Jan 10 21:10:46 +0000 2017", + "id": 818928092383166465, + "id_str": "818928092383166465", + "full_text": "Double escaping &amp; & amp", + "truncated": false, + "display_text_range": [ + 0, + 35 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 36, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818935308813103104.json b/tests/data/api.twitter.com/818935308813103104.json new file mode 100644 index 0000000..452ee89 --- /dev/null +++ b/tests/data/api.twitter.com/818935308813103104.json @@ -0,0 +1,263 @@ +{ + "created_at": "Tue Jan 10 21:39:27 +0000 2017", + "id": 818935308813103104, + "id_str": "818935308813103104", + "full_text": "Two photos https://t.co/esnJCdKVol", + "truncated": false, + "display_text_range": [ + 0, + 10 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + + ], + "media": [ + { + "id": 818934636239691777, + "id_str": "818934636239691777", + "indices": [ + 11, + 34 + ], + "media_url": "http://pbs.twimg.com/media/C11wtndUoAE1WfE.jpg", + "media_url_https": "https://pbs.twimg.com/media/C11wtndUoAE1WfE.jpg", + "url": "https://t.co/esnJCdKVol", + "display_url": "pic.twitter.com/esnJCdKVol", + "expanded_url": "https://twitter.com/pkdev/status/818935308813103104/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 800, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 453, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1365, + "resize": "fit" + } + } + } + ] + }, + "extended_entities": { + "media": [ + { + "id": 818934636239691777, + "id_str": "818934636239691777", + "indices": [ + 11, + 34 + ], + "media_url": "http://pbs.twimg.com/media/C11wtndUoAE1WfE.jpg", + "media_url_https": "https://pbs.twimg.com/media/C11wtndUoAE1WfE.jpg", + "url": "https://t.co/esnJCdKVol", + "display_url": "pic.twitter.com/esnJCdKVol", + "expanded_url": "https://twitter.com/pkdev/status/818935308813103104/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 800, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 453, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1365, + "resize": "fit" + } + } + }, + { + "id": 818935275732627456, + "id_str": "818935275732627456", + "indices": [ + 11, + 34 + ], + "media_url": "http://pbs.twimg.com/media/C11xS1wUcAAeaKF.jpg", + "media_url_https": "https://pbs.twimg.com/media/C11xS1wUcAAeaKF.jpg", + "url": "https://t.co/esnJCdKVol", + "display_url": "pic.twitter.com/esnJCdKVol", + "expanded_url": "https://twitter.com/pkdev/status/818935308813103104/photo/1", + "type": "photo", + "sizes": { + "medium": { + "w": 1200, + "h": 800, + "resize": "fit" + }, + "small": { + "w": 680, + "h": 453, + "resize": "fit" + }, + "thumb": { + "w": 150, + "h": 150, + "resize": "crop" + }, + "large": { + "w": 2048, + "h": 1365, + "resize": "fit" + } + } + } + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 36, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "possibly_sensitive": false, + "possibly_sensitive_appealable": false, + "lang": "en" +} \ No newline at end of file diff --git a/tests/data/api.twitter.com/818943244553699328.json b/tests/data/api.twitter.com/818943244553699328.json new file mode 100644 index 0000000..a071688 --- /dev/null +++ b/tests/data/api.twitter.com/818943244553699328.json @@ -0,0 +1,147 @@ +{ + "created_at": "Tue Jan 10 22:10:59 +0000 2017", + "id": 818943244553699328, + "id_str": "818943244553699328", + "full_text": "Here 🎉 have an emoji", + "truncated": false, + "display_text_range": [ + 0, + 20 + ], + "entities": { + "hashtags": [ + + ], + "symbols": [ + + ], + "user_mentions": [ + + ], + "urls": [ + + ] + }, + "source": "Twitter Web Client", + "in_reply_to_status_id": null, + "in_reply_to_status_id_str": null, + "in_reply_to_user_id": null, + "in_reply_to_user_id_str": null, + "in_reply_to_screen_name": null, + "user": { + "id": 143883456, + "id_str": "143883456", + "name": "aaronpk dev", + "screen_name": "pkdev", + "location": "Portland, OR", + "description": "Dev account for testing Twitter things. Follow me here: http://t.co/DtzRLfxayu", + "url": "https://t.co/fXLomQaMAd", + "entities": { + "url": { + "urls": [ + { + "url": "https://t.co/fXLomQaMAd", + "expanded_url": "https://aaronparecki.com/", + "display_url": "aaronparecki.com", + "indices": [ + 0, + 23 + ] + } + ] + }, + "description": { + "urls": [ + { + "url": "http://t.co/DtzRLfxayu", + "expanded_url": "https://twitter.com/aaronpk", + "display_url": "twitter.com/aaronpk", + "indices": [ + 56, + 78 + ] + } + ] + } + }, + "protected": true, + "followers_count": 4, + "friends_count": 1, + "listed_count": 1, + "created_at": "Fri May 14 17:47:15 +0000 2010", + "favourites_count": 1, + "utc_offset": -28800, + "time_zone": "Pacific Time (US & Canada)", + "geo_enabled": true, + "verified": false, + "statuses_count": 37, + "lang": "en", + "contributors_enabled": false, + "is_translator": false, + "is_translation_enabled": false, + "profile_background_color": "C0DEED", + "profile_background_image_url": "http://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_image_url_https": "https://abs.twimg.com/images/themes/theme1/bg.png", + "profile_background_tile": false, + "profile_image_url": "http://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_image_url_https": "https://pbs.twimg.com/profile_images/638125135904436224/qd_d94Qn_normal.jpg", + "profile_link_color": "1DA1F2", + "profile_sidebar_border_color": "C0DEED", + "profile_sidebar_fill_color": "DDEEF6", + "profile_text_color": "333333", + "profile_use_background_image": true, + "has_extended_profile": true, + "default_profile": true, + "default_profile_image": false, + "following": true, + "follow_request_sent": false, + "notifications": false, + "translator_type": "none" + }, + "geo": null, + "coordinates": null, + "place": { + "id": "ac88a4f17a51c7fc", + "url": "https://api.twitter.com/1.1/geo/id/ac88a4f17a51c7fc.json", + "place_type": "city", + "name": "Portland", + "full_name": "Portland, OR", + "country_code": "US", + "country": "United States", + "contained_within": [ + + ], + "bounding_box": { + "type": "Polygon", + "coordinates": [ + [ + [ + -122.7900653, + 45.421863 + ], + [ + -122.471751, + 45.421863 + ], + [ + -122.471751, + 45.6509405 + ], + [ + -122.7900653, + 45.6509405 + ] + ] + ] + }, + "attributes": { + } + }, + "contributors": null, + "is_quote_status": false, + "retweet_count": 0, + "favorite_count": 0, + "favorited": false, + "retweeted": false, + "lang": "en" +} \ No newline at end of file