Activitypub_notice.php 11 KB

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