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.

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