Activitypub_notice.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. <?php
  2. /**
  3. * GNU social - a federating social network
  4. *
  5. * ActivityPubPlugin implementation for GNU Social
  6. *
  7. * LICENCE: This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Affero General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU Affero General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Affero General Public License
  18. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. *
  20. * @category Plugin
  21. * @package GNUsocial
  22. * @author Diogo Cordeiro <diogo@fc.up.pt>
  23. * @copyright 2018 Free Software Foundation http://fsf.org
  24. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  25. * @link https://www.gnu.org/software/social/
  26. */
  27. if (!defined('GNUSOCIAL')) {
  28. exit(1);
  29. }
  30. /**
  31. * ActivityPub notice representation
  32. *
  33. * @category Plugin
  34. * @package GNUsocial
  35. * @author Diogo Cordeiro <diogo@fc.up.pt>
  36. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  37. * @link http://www.gnu.org/software/social/
  38. */
  39. class Activitypub_notice extends Managed_DataObject
  40. {
  41. /**
  42. * Generates a pretty notice from a Notice object
  43. *
  44. * @author Diogo Cordeiro <diogo@fc.up.pt>
  45. * @param Notice $notice
  46. * @return pretty array to be used in a response
  47. */
  48. public static function notice_to_array($notice)
  49. {
  50. $profile = $notice->getProfile();
  51. $attachments = [];
  52. foreach ($notice->attachments() as $attachment) {
  53. $attachments[] = Activitypub_attachment::attachment_to_array($attachment);
  54. }
  55. $tags = [];
  56. foreach ($notice->getTags() as $tag) {
  57. if ($tag != "") { // Hacky workaround to avoid stupid outputs
  58. $tags[] = Activitypub_tag::tag_to_array($tag);
  59. }
  60. }
  61. $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])];
  62. foreach ($notice->getAttentionProfiles() as $to_profile) {
  63. $cc[] = $href = $to_profile->getUri();
  64. $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST));
  65. }
  66. // In a world without walls and fences, we should make everything Public!
  67. $to[]= 'https://www.w3.org/ns/activitystreams#Public';
  68. $item = [
  69. '@context' => 'https://www.w3.org/ns/activitystreams',
  70. 'id' => common_local_url('apNotice', ['id' => $notice->getID()]),
  71. 'type' => 'Note',
  72. 'published' => str_replace(' ', 'T', $notice->getCreated()).'Z',
  73. 'url' => $notice->getUrl(),
  74. 'attributedTo' => ActivityPubPlugin::actor_uri($profile),
  75. 'to' => ['https://www.w3.org/ns/activitystreams#Public'],
  76. 'cc' => $cc,
  77. 'atomUri' => $notice->getUrl(),
  78. 'conversation' => $notice->getConversationUrl(),
  79. 'content' => $notice->getRendered(),
  80. 'isLocal' => $notice->isLocal(),
  81. 'attachment' => $attachments,
  82. 'tag' => $tags
  83. ];
  84. // Is this a reply?
  85. if (!empty($notice->reply_to)) {
  86. $item['inReplyTo'] = common_local_url('apNotice', ['id' => $notice->getID()]);
  87. $item['inReplyToAtomUri'] = Notice::getById($notice->reply_to)->getUrl();
  88. }
  89. // Do we have a location for this notice?
  90. try {
  91. $location = Notice_location::locFromStored($notice);
  92. $item['latitude'] = $location->lat;
  93. $item['longitude'] = $location->lon;
  94. } catch (Exception $e) {
  95. // Apparently no.
  96. }
  97. return $item;
  98. }
  99. /**
  100. * Create a Notice via ActivityPub Note Object.
  101. * Returns created Notice.
  102. *
  103. * @author Diogo Cordeiro <diogo@fc.up.pt>
  104. * @param Array $object
  105. * @param Profile|null $actor_profile
  106. * @return Notice
  107. * @throws Exception
  108. */
  109. public static function create_notice($object, $actor_profile = null)
  110. {
  111. $id = $object['id']; // int32
  112. $url = $object['url']; // string
  113. $content = $object['content']; // string
  114. // possible keys: ['inReplyTo', 'latitude', 'longitude', 'attachment']
  115. $settings = [];
  116. if (isset($object['inReplyTo'])) {
  117. $settings['inReplyTo'] = $object['inReplyTo'];
  118. }
  119. if (isset($object['latitude'])) {
  120. $settings['latitude'] = $object['latitude'];
  121. }
  122. if (isset($object['longitude'])) {
  123. $settings['longitude'] = $object['longitude'];
  124. }
  125. if (isset($object['attachment'])) {
  126. $settings['attachment'] = $object['attachment'];
  127. }
  128. // Ensure Actor Profile
  129. if (is_null($actor_profile)) {
  130. $actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']);
  131. }
  132. $act = new Activity();
  133. $act->verb = ActivityVerb::POST;
  134. $act->time = time();
  135. $act->actor = $actor_profile->asActivityObject();
  136. $act->context = new ActivityContext();
  137. $options = ['source' => 'ActivityPub', 'uri' => $id, 'url' => $url];
  138. // Do we have an attachment?
  139. if (isset($settings['attachment'][0])) {
  140. $attach = $settings['attachment'][0];
  141. $attach_url = $settings['attachment'][0]['url'];
  142. // Is it an image?
  143. if (ActivityPubPlugin::$store_images_from_remote_notes_attachments && substr($attach["mediaType"], 0, 5) == "image") {
  144. $temp_filename = tempnam(sys_get_temp_dir(), 'apCreateNoteAttach_');
  145. try {
  146. $imgData = HTTPClient::quickGet($attach_url);
  147. // Make sure it's at least an image file. ImageFile can do the rest.
  148. if (false === getimagesizefromstring($imgData)) {
  149. common_debug('ActivityPub Create Notice: Failed because the downloaded image: '.$attach_url. 'is not valid.');
  150. throw new UnsupportedMediaException('Downloaded image was not an image.');
  151. }
  152. file_put_contents($temp_filename, $imgData);
  153. common_debug('ActivityPub Create Notice: Stored dowloaded image in: '.$temp_filename);
  154. $id = $actor_profile->getID();
  155. $imagefile = new ImageFile(null, $temp_filename);
  156. $filename = hash(File::FILEHASH_ALG, $imgData).image_type_to_extension($imagefile->type);
  157. unset($imgData); // No need to carry this in memory.
  158. rename($temp_filename, File::path($filename));
  159. common_debug('ActivityPub Create Notice: Moved image from: '.$temp_filename.' to '.$filename);
  160. $mediaFile = new MediaFile($filename, $attach['mediaType']);
  161. $act->enclosures[] = $mediaFile->getEnclosure();
  162. } catch (Exception $e) {
  163. common_debug('ActivityPub Create Notice: Something went wrong while processing the image from: '.$attach_url.' details: '.$e->getMessage());
  164. unlink($temp_filename);
  165. }
  166. }
  167. $content .= ($content==='' ? '' : ' ') . '<br><a href="'.$attach_url.'">Remote Attachment Source</a>';
  168. }
  169. // Is this a reply?
  170. if (isset($settings['inReplyTo'])) {
  171. try {
  172. $inReplyTo = ActivityPubPlugin::grab_notice_from_url($settings['inReplyTo']);
  173. $act->context->replyToID = $inReplyTo->getUri();
  174. $act->context->replyToUrl = $inReplyTo->getUrl();
  175. } catch (Exception $e) {
  176. // It failed to grab, maybe we got this note from another source
  177. // (e.g.: OStatus) that handles this differently or we really
  178. // failed to get it...
  179. // Welp, nothing that we can do about, let's
  180. // just fake we don't have such notice.
  181. }
  182. } else {
  183. $inReplyTo = null;
  184. }
  185. // Mentions
  186. $mentions = [];
  187. if (isset($object['tag']) && is_array($object['tag'])) {
  188. foreach ($object['tag'] as $tag) {
  189. if ($tag['type'] == 'Mention') {
  190. $mentions[] = $tag['href'];
  191. }
  192. }
  193. }
  194. $mentions_profiles = [];
  195. $discovery = new Activitypub_explorer;
  196. foreach ($mentions as $mention) {
  197. try {
  198. $mentions_profiles[] = $discovery->lookup($mention)[0];
  199. } catch (Exception $e) {
  200. // Invalid actor found, just let it go. // TODO: Fallback to OStatus
  201. }
  202. }
  203. unset($discovery);
  204. foreach ($mentions_profiles as $mp) {
  205. $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = 'http://activitystrea.ms/schema/1.0/person';
  206. }
  207. // Add location if that is set
  208. if (isset($settings['latitude'], $settings['longitude'])) {
  209. $act->context->location = Location::fromLatLon($settings['latitude'], $settings['longitude']);
  210. }
  211. // Reject notice if it is too long (without the HTML)
  212. if (Notice::contentTooLong($content)) {
  213. //throw new Exception('That\'s too long. Maximum notice size is %d character.');
  214. }
  215. $actobj = new ActivityObject();
  216. $actobj->type = ActivityObject::NOTE;
  217. $actobj->content = strip_tags($content, '<p><b><i><u><a><ul><ol><li>');
  218. // Finally add the activity object to our activity
  219. $act->objects[] = $actobj;
  220. $note = Notice::saveActivity($act, $actor_profile, $options);
  221. if (ActivityPubPlugin::$store_images_from_remote_notes_attachments && isset($mediaFile)) {
  222. $mediaFile->attachToNotice($note);
  223. }
  224. return $note;
  225. }
  226. /**
  227. * Validates a note.
  228. *
  229. * @author Diogo Cordeiro <diogo@fc.up.pt>
  230. * @param Array $object
  231. * @throws Exception
  232. */
  233. public static function validate_note($object)
  234. {
  235. if (!isset($object['attributedTo'])) {
  236. common_debug('ActivityPub Notice Validator: Rejected because attributedTo was not specified.');
  237. throw new Exception('No attributedTo specified.');
  238. }
  239. if (!isset($object['id'])) {
  240. common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.');
  241. throw new Exception('Object ID not specified.');
  242. } elseif (!filter_var($object['id'], FILTER_VALIDATE_URL)) {
  243. common_debug('ActivityPub Notice Validator: Rejected because Object ID is invalid.');
  244. throw new Exception('Invalid Object ID.');
  245. }
  246. if (!isset($object['type']) || $object['type'] !== 'Note') {
  247. common_debug('ActivityPub Notice Validator: Rejected because of Type.');
  248. throw new Exception('Invalid Object type.');
  249. }
  250. if (!isset($object['content'])) {
  251. common_debug('ActivityPub Notice Validator: Rejected because Content was not specified.');
  252. throw new Exception('Object content was not specified.');
  253. }
  254. if (!isset($object['url'])) {
  255. throw new Exception('Object URL was not specified.');
  256. } elseif (!filter_var($object['url'], FILTER_VALIDATE_URL)) {
  257. common_debug('ActivityPub Notice Validator: Rejected because Object URL is invalid.');
  258. throw new Exception('Invalid Object URL.');
  259. }
  260. if (!isset($object['cc'])) {
  261. common_debug('ActivityPub Notice Validator: Rejected because Object CC was not specified.');
  262. throw new Exception('Object CC was not specified.');
  263. }
  264. return true;
  265. }
  266. }