request('statuses/show/'.$tweet_id, 'GET', ['tweet_mode'=>'extended']); } catch(\TwitterException $e) { return [ 'error' => 'twitter_error', 'error_description' => $e->getMessage() ]; } return [ 'url' => $url, 'body' => $tweet, 'code' => 200, ]; } public static function parse($json, $url) { if(is_string($json)) $tweet = json_decode($json); else $tweet = $json; if(!$tweet) { return self::_unknown(); } $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], 'UTF-8'); 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; $repostedEntry = self::parse($reposted, $repostOf); if(isset($repostedEntry['data']['refs'])) { foreach($repostedEntry['data']['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(sprintf('%+d', $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; } } } // Don't include the RT'd photo or video in the main object. // They get included in the reposted object instead. if(!property_exists($tweet, 'retweeted_status')) { // 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; $quotedEntry = self::parse($tweet->quoted_status, $quoteOf); if(isset($quotedEntry['data']['refs'])) { foreach($quotedEntry['data']['refs'] as $k=>$v) { $refs[$k] = $v; } } $refs[$quoteOf] = $quotedEntry['data']; $entry['quotation-of'] = $quoteOf; } if($author = self::_buildHCardFromTwitterProfile($tweet->user)) { $entry['author'] = $author; } if(count($refs)) { $entry['refs'] = $refs; } return [ 'data' => $entry, 'original' => $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) { if($profile->entities->url->urls[0]->expanded_url) $author['url'] = $profile->entities->url->urls[0]->expanded_url; else $author['url'] = $profile->entities->url->urls[0]->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; } }