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.

232 lines
6.6 KiB

  1. <?php
  2. namespace p3k\XRay\Formats;
  3. use DateTime;
  4. use \p3k\XRay\PostType;
  5. class ActivityStreams extends Format {
  6. public static function is_as2_json($document) {
  7. if(is_array($document) && isset($document['@context'])) {
  8. if(is_string($document['@context']) && $document['@context'] == 'https://www.w3.org/ns/activitystreams')
  9. return true;
  10. if(is_array($document['@context']) && in_array('https://www.w3.org/ns/activitystreams', $document['@context']))
  11. return true;
  12. }
  13. return false;
  14. }
  15. public static function matches_host($url) {
  16. return true;
  17. }
  18. public static function matches($url) {
  19. return true;
  20. }
  21. public static function parse($as2, $url, $http, $opts=[]) {
  22. if(!isset($as2['type']))
  23. return false;
  24. switch($as2['type']) {
  25. case 'Person':
  26. return self::parseAsHCard($as2, $url, $http, $opts);
  27. case 'Article':
  28. case 'Note':
  29. case 'Announce': // repost
  30. case 'Like': // like
  31. return self::parseAsHEntry($as2, $url, $http, $opts);
  32. }
  33. $result = [
  34. 'data' => [
  35. 'type' => 'unknown',
  36. ],
  37. 'url' => $url,
  38. ];
  39. return $result;
  40. }
  41. private static function parseAsHEntry($as2, $url, $http, $opts) {
  42. $data = [
  43. 'type' => 'entry'
  44. ];
  45. $refs = [];
  46. if(isset($as2['url']))
  47. $data['url'] = $as2['url'];
  48. elseif(isset($as2['id']))
  49. $data['url'] = $as2['id'];
  50. if(isset($as2['published'])) {
  51. try {
  52. $date = new DateTime($as2['published']);
  53. $data['published'] = $date->format('c');
  54. } catch(\Exception $e){}
  55. } elseif(isset($as2['signature']['created'])) {
  56. // Pull date from the signature if there isn't one in the activity
  57. try {
  58. $date = new DateTime($as2['signature']['created']);
  59. $data['published'] = $date->format('c');
  60. } catch(\Exception $e){}
  61. }
  62. if(isset($as2['name'])) {
  63. $data['name'] = $as2['name'];
  64. }
  65. if(isset($as2['summary'])) {
  66. $data['summary'] = $as2['summary'];
  67. }
  68. if(isset($as2['content'])) {
  69. $html = trim(self::sanitizeHTML($as2['content']));
  70. $text = trim(self::stripHTML($html));
  71. $data['content'] = [
  72. 'text' => $text
  73. ];
  74. if($html && $text && $text != $html) {
  75. $data['content']['html'] = $html;
  76. }
  77. }
  78. if(isset($as2['tag']) && is_array($as2['tag'])) {
  79. $emoji = [];
  80. $category = [];
  81. foreach($as2['tag'] as $tag) {
  82. if(is_array($tag) && isset($tag['name']) && isset($tag['type']) && $tag['type'] == 'Hashtag')
  83. $category[] = trim($tag['name'], '#');
  84. if(is_array($tag) && isset($tag['type']) && $tag['type'] == 'Emoji' && isset($tag['icon']['url'])) {
  85. $emoji[$tag['name']] = $tag['icon']['url'];
  86. }
  87. }
  88. if(count($category))
  89. $data['category'] = $category;
  90. if(count($emoji) && isset($data['content']['html'])) {
  91. foreach($emoji as $code=>$img) {
  92. $data['content']['html'] = str_replace($code, '<img src="'.$img.'" alt="'.$code.'" title="'.$code.'" height="24" class="xray-custom-emoji">', $data['content']['html']);
  93. }
  94. }
  95. }
  96. if(isset($as2['inReplyTo'])) {
  97. $data['in-reply-to'] = [$as2['inReplyTo']];
  98. }
  99. // Photos and Videos
  100. if(isset($as2['attachment'])) {
  101. $photos = [];
  102. $videos = [];
  103. foreach($as2['attachment'] as $attachment) {
  104. if(strpos($attachment['mediaType'], 'image/') !== false) {
  105. $photos[] = $attachment['url'];
  106. }
  107. if(strpos($attachment['mediaType'], 'video/') !== false) {
  108. $videos[] = $attachment['url'];
  109. }
  110. }
  111. if(count($photos))
  112. $data['photo'] = $photos;
  113. if(count($videos))
  114. $data['video'] = $videos;
  115. }
  116. // Fetch the author info, which requires an HTTP request
  117. $authorURL = false;
  118. if(isset($as2['attributedTo']) && is_string($as2['attributedTo'])) {
  119. $authorURL = $as2['attributedTo'];
  120. } elseif(isset($as2['actor']) && is_string($as2['actor'])) {
  121. $authorURL = $as2['actor'];
  122. }
  123. if($authorURL) {
  124. $authorResponse = $http->get($authorURL, ['Accept: application/activity+json,application/json']);
  125. if($authorResponse && !empty($authorResponse['body'])) {
  126. $authorProfile = json_decode($authorResponse['body'], true);
  127. $author = self::parseAsHCard($authorProfile, $authorURL, $http, $opts);
  128. if($author && !empty($author['data']))
  129. $data['author'] = $author['data'];
  130. }
  131. }
  132. // If this is a repost, fetch the reposted content
  133. if($as2['type'] == 'Announce' && isset($as2['object']) && is_string($as2['object'])) {
  134. $data['repost-of'] = [$as2['object']];
  135. $reposted = $http->get($as2['object'], ['Accept: application/activity+json,application/json']);
  136. if($reposted && !empty($reposted['body'])) {
  137. $repostedData = json_decode($reposted['body'], true);
  138. if($repostedData) {
  139. $repost = self::parse($repostedData, $as2['object'], $http, $opts);
  140. if($repost) {
  141. $refs[$as2['object']] = $repost;
  142. }
  143. }
  144. }
  145. }
  146. // If this is a like, fetch the liked post
  147. if($as2['type'] == 'Like' && isset($as2['object']) && is_string($as2['object'])) {
  148. $data['like-of'] = [$as2['object']];
  149. $liked = $http->get($as2['object'], ['Accept: application/activity+json,application/json']);
  150. if($liked && !empty($liked['body'])) {
  151. $likedData = json_decode($liked['body'], true);
  152. if($likedData) {
  153. $like = self::parse($likedData, $as2['object'], $http, $opts);
  154. if($like) {
  155. $refs[$as2['object']] = $like;
  156. }
  157. }
  158. }
  159. }
  160. $data['post-type'] = PostType::discover($data);
  161. $response = [
  162. 'data' => $data,
  163. ];
  164. if(count($refs)) {
  165. $response['data']['refs'] = $refs;
  166. }
  167. return $response;
  168. }
  169. private static function parseAsHCard($as2, $url, $http, $opts) {
  170. $data = [
  171. 'type' => 'card',
  172. 'name' => null,
  173. 'url' => null,
  174. 'photo' => null
  175. ];
  176. if(!empty($as2['name']))
  177. $data['name'] = $as2['name'];
  178. elseif(isset($as2['preferredUsername']))
  179. $data['name'] = $as2['preferredUsername'];
  180. if(isset($as2['preferredUsername']))
  181. $data['nickname'] = $as2['preferredUsername'];
  182. if(isset($as2['url']))
  183. $data['url'] = $as2['url'];
  184. if(isset($as2['icon']) && isset($as2['icon']['url']))
  185. $data['photo'] = $as2['icon']['url'];
  186. // TODO: featured image for h-cards?
  187. // if(isset($as2['image']) && isset($as2['image']['url']))
  188. // $data['featured'] = $as2['image']['url'];
  189. $response = [
  190. 'data' => $data
  191. ];
  192. return $response;
  193. }
  194. }