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.

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