You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

234 lines
6.9 KiB

  1. <?php
  2. namespace XRay\Formats;
  3. use DOMDocument, DOMXPath;
  4. use DateTime, DateTimeZone;
  5. use Parse;
  6. class Twitter {
  7. public static function parse($url, $tweet_id, $creds, $json=null) {
  8. $host = parse_url($url, PHP_URL_HOST);
  9. if($host == 'twtr.io') {
  10. $tweet_id = self::b60to10($tweet_id);
  11. }
  12. if($json) {
  13. if(is_string($json))
  14. $tweet = json_decode($json);
  15. else
  16. $tweet = $json;
  17. } else {
  18. $twitter = new \Twitter($creds['twitter_api_key'], $creds['twitter_api_secret'], $creds['twitter_access_token'], $creds['twitter_access_token_secret']);
  19. $tweet = $twitter->request('statuses/show/'.$tweet_id, 'GET', ['tweet_mode'=>'extended']);
  20. }
  21. if(!$tweet)
  22. return false;
  23. $entry = array(
  24. 'type' => 'entry',
  25. 'url' => $url,
  26. 'author' => [
  27. 'type' => 'card',
  28. 'name' => null,
  29. 'nickname' => null,
  30. 'photo' => null,
  31. 'url' => null
  32. ]
  33. );
  34. $refs = [];
  35. // Only use the "display" segment of the text
  36. $text = mb_substr($tweet->full_text,
  37. $tweet->display_text_range[0],
  38. $tweet->display_text_range[1]-$tweet->display_text_range[0],
  39. 'UTF-8');
  40. if(property_exists($tweet, 'retweeted_status')) {
  41. // No content for retweets
  42. $reposted = $tweet->retweeted_status;
  43. $repostOf = 'https://twitter.com/' . $reposted->user->screen_name . '/status/' . $reposted->id_str;
  44. $entry['repost-of'] = $repostOf;
  45. list($repostedEntry) = self::parse($repostOf, $reposted->id_str, null, $reposted);
  46. if(isset($repostedEntry['refs'])) {
  47. foreach($repostedEntry['refs'] as $k=>$v) {
  48. $refs[$k] = $v;
  49. }
  50. }
  51. $refs[$repostOf] = $repostedEntry['data'];
  52. } else {
  53. // Twitter escapes & as &amp; in the text
  54. $text = html_entity_decode($text);
  55. $text = self::expandTweetURLs($text, $tweet);
  56. $entry['content'] = ['text' => $text];
  57. }
  58. // Published date
  59. $published = new DateTime($tweet->created_at);
  60. if(property_exists($tweet->user, 'utc_offset')) {
  61. $tz = new DateTimeZone($tweet->user->utc_offset / 3600);
  62. $published->setTimeZone($tz);
  63. }
  64. $entry['published'] = $published->format('c');
  65. // Hashtags
  66. if(property_exists($tweet, 'entities') && property_exists($tweet->entities, 'hashtags')) {
  67. if(count($tweet->entities->hashtags)) {
  68. $entry['category'] = [];
  69. foreach($tweet->entities->hashtags as $hashtag) {
  70. $entry['category'][] = $hashtag->text;
  71. }
  72. }
  73. }
  74. // Photos and Videos
  75. if(property_exists($tweet, 'extended_entities') && property_exists($tweet->extended_entities, 'media')) {
  76. foreach($tweet->extended_entities->media as $media) {
  77. if($media->type == 'photo') {
  78. if(!array_key_exists('photo', $entry))
  79. $entry['photo'] = [];
  80. $entry['photo'][] = $media->media_url_https;
  81. } elseif($media->type == 'video') {
  82. if(!array_key_exists('video', $entry))
  83. $entry['video'] = [];
  84. // Find the highest bitrate video that is mp4
  85. $videos = $media->video_info->variants;
  86. $videos = array_filter($videos, function($v) {
  87. return property_exists($v, 'bitrate') && $v->content_type == 'video/mp4';
  88. });
  89. if(count($videos)) {
  90. usort($videos, function($a,$b) {
  91. return $a->bitrate < $b->bitrate;
  92. });
  93. $entry['video'][] = $videos[0]->url;
  94. }
  95. }
  96. }
  97. }
  98. // Place
  99. if(property_exists($tweet, 'place') && $tweet->place) {
  100. $place = $tweet->place;
  101. if($place->place_type == 'city') {
  102. $entry['location'] = $place->url;
  103. $refs[$place->url] = [
  104. 'type' => 'adr',
  105. 'name' => $place->full_name,
  106. 'locality' => $place->name,
  107. 'country-name' => $place->country,
  108. ];
  109. }
  110. }
  111. // Quoted Status
  112. if(property_exists($tweet, 'quoted_status')) {
  113. $quoteOf = 'https://twitter.com/' . $tweet->quoted_status->user->screen_name . '/status/' . $tweet->quoted_status_id_str;
  114. list($quoted) = self::parse($quoteOf, $tweet->quoted_status_id_str, null, $tweet->quoted_status);
  115. if(isset($quoted['refs'])) {
  116. foreach($quoted['refs'] as $k=>$v) {
  117. $refs[$k] = $v;
  118. }
  119. }
  120. $refs[$quoteOf] = $quoted['data'];
  121. }
  122. if($author = self::_buildHCardFromTwitterProfile($tweet->user)) {
  123. $entry['author'] = $author;
  124. }
  125. $response = [
  126. 'data' => $entry
  127. ];
  128. if(count($refs)) {
  129. $response['refs'] = $refs;
  130. }
  131. return [$response, $tweet];
  132. }
  133. private static function _buildHCardFromTwitterProfile($profile) {
  134. if(!$profile) return false;
  135. $author = [
  136. 'type' => 'card'
  137. ];
  138. $author['nickname'] = $profile->screen_name;
  139. $author['location'] = $profile->location;
  140. $author['bio'] = self::expandTwitterObjectURLs($profile->description, $profile, 'description');
  141. if($profile->name)
  142. $author['name'] = $profile->name;
  143. else
  144. $author['name'] = $profile->screen_name;
  145. if($profile->url)
  146. $author['url'] = $profile->entities->url->urls[0]->expanded_url;
  147. else
  148. $author['url'] = 'https://twitter.com/' . $profile->screen_name;
  149. $author['photo'] = $profile->profile_image_url_https;
  150. return $author;
  151. }
  152. private static function expandTweetURLs($text, $object) {
  153. if(property_exists($object, 'entities') && property_exists($object->entities, 'urls')) {
  154. foreach($object->entities->urls as $url) {
  155. $text = str_replace($url->url, $url->expanded_url, $text);
  156. }
  157. }
  158. return $text;
  159. }
  160. private static function expandTwitterObjectURLs($text, $object, $key) {
  161. if(property_exists($object, 'entities')
  162. && property_exists($object->entities, $key)
  163. && property_exists($object->entities->{$key}, 'urls')) {
  164. foreach($object->entities->{$key}->urls as $url) {
  165. $text = str_replace($url->url, $url->expanded_url, $text);
  166. }
  167. }
  168. return $text;
  169. }
  170. /**
  171. * Converts base 60 to base 10, with error checking
  172. * http://tantek.pbworks.com/NewBase60
  173. * @param string $s
  174. * @return int
  175. */
  176. function b60to10($s)
  177. {
  178. $n = 0;
  179. for($i = 0; $i < strlen($s); $i++) // iterate from first to last char of $s
  180. {
  181. $c = ord($s[$i]); // put current ASCII of char into $c
  182. if ($c>=48 && $c<=57) { $c=bcsub($c,48); }
  183. else if ($c>=65 && $c<=72) { $c=bcsub($c,55); }
  184. else if ($c==73 || $c==108) { $c=1; } // typo capital I, lowercase l to 1
  185. else if ($c>=74 && $c<=78) { $c=bcsub($c,56); }
  186. else if ($c==79) { $c=0; } // error correct typo capital O to 0
  187. else if ($c>=80 && $c<=90) { $c=bcsub($c,57); }
  188. else if ($c==95) { $c=34; } // underscore
  189. else if ($c>=97 && $c<=107) { $c=bcsub($c,62); }
  190. else if ($c>=109 && $c<=122) { $c=bcsub($c,63); }
  191. else { $c = 0; } // treat all other noise as 0
  192. $n = bcadd(bcmul(60, $n), $c);
  193. }
  194. return $n;
  195. }
  196. }