From c75bf1a19d8816eadf464c499741b1ffea32af8a Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Fri, 28 Aug 2020 01:12:40 +0100 Subject: [PATCH] [ActivityPub] Fix issues concerning Activity URIs And some other minor bugs. --- plugins/ActivityPub/ActivityPubPlugin.php | 8 +++ plugins/ActivityPub/actions/apnotice.php | 5 +- .../lib/activitypubqueuehandler.php | 18 ++--- plugins/ActivityPub/lib/inbox_handler.php | 8 +-- .../lib/models/Activitypub_announce.php | 56 +++++++++++++--- .../lib/models/Activitypub_create.php | 2 +- .../lib/models/Activitypub_delete.php | 2 +- .../lib/models/Activitypub_like.php | 67 +++++++++++++++++-- .../lib/models/Activitypub_notice.php | 18 ++--- .../lib/models/Activitypub_reject.php | 2 +- .../lib/models/Activitypub_tag.php | 6 +- .../lib/models/Activitypub_undo.php | 6 +- plugins/ActivityPub/lib/postman.php | 38 ++++++----- 13 files changed, 169 insertions(+), 67 deletions(-) diff --git a/plugins/ActivityPub/ActivityPubPlugin.php b/plugins/ActivityPub/ActivityPubPlugin.php index 5744e55bfe..cc0f45c826 100644 --- a/plugins/ActivityPub/ActivityPubPlugin.php +++ b/plugins/ActivityPub/ActivityPubPlugin.php @@ -180,6 +180,14 @@ class ActivityPubPlugin extends Plugin $acceptHeaders ); + // v3 + $m->connect( + 'activity/:id', + ['action' => 'apNotice'], + ['id' => '[0-9]+'], + ); + + // v2 $m->connect( 'notice/:id', ['action' => 'apNotice'], diff --git a/plugins/ActivityPub/actions/apnotice.php b/plugins/ActivityPub/actions/apnotice.php index ff85e36898..c3b643ac32 100644 --- a/plugins/ActivityPub/actions/apnotice.php +++ b/plugins/ActivityPub/actions/apnotice.php @@ -53,11 +53,12 @@ class apNoticeAction extends ManagedAction try { $notice = Notice::getByID($this->trimmed('id')); } catch (Exception $e) { - ActivityPubReturn::error('Invalid Notice URI.', 404); + ActivityPubReturn::error('Invalid Activity URI.', 404); } if (!$notice->isLocal()) { - ActivityPubReturn::error("This is not a local notice.", 403); + // We have no authority on the requested activity. + ActivityPubReturn::error("This is not a local activity.", 403); } $res = Activitypub_notice::notice_to_array($notice); diff --git a/plugins/ActivityPub/lib/activitypubqueuehandler.php b/plugins/ActivityPub/lib/activitypubqueuehandler.php index 54074f9f1e..8408916537 100644 --- a/plugins/ActivityPub/lib/activitypubqueuehandler.php +++ b/plugins/ActivityPub/lib/activitypubqueuehandler.php @@ -54,7 +54,7 @@ class ActivityPubQueueHandler extends QueueHandler public function handle($notice): bool { if (!($notice instanceof Notice)) { - common_log(LOG_ERR, "Got a bogus notice, not distributing"); + common_log(LOG_ERR, 'Got a bogus notice, not distributing'); return true; } @@ -74,7 +74,7 @@ class ActivityPubQueueHandler extends QueueHandler ); // Handling a Create? - if (ActivityUtils::compareVerbs($notice->verb, [ActivityVerb::POST])) { + if (ActivityUtils::compareVerbs($notice->verb, [ActivityVerb::POST, ActivityVerb::SHARE])) { return $this->handle_create($profile, $notice, $other); } @@ -143,7 +143,7 @@ class ActivityPubQueueHandler extends QueueHandler // That was it $postman = new Activitypub_postman($profile, $other); - $postman->announce($repeated_notice); + $postman->announce($notice, $repeated_notice); } // either made the announce or found nothing to repeat @@ -160,7 +160,7 @@ class ActivityPubQueueHandler extends QueueHandler * Notify remote users when their notices get favourited. * * @param Profile $profile of local user doing the faving - * @param Notice $notice Notice being favored + * @param Notice $notice_liked Notice being favored * @return bool return value * @throws HTTP_Request2_Exception * @throws InvalidUrlException @@ -168,10 +168,10 @@ class ActivityPubQueueHandler extends QueueHandler */ public function onEndFavorNotice(Profile $profile, Notice $notice, $other) { - $notice = $notice->getParent(); - if ($notice->reply_to) { + $notice_liked = $notice->getParent(); + if ($notice_liked->reply_to) { try { - $parent_notice = $notice->getParent(); + $parent_notice = $notice_liked->getParent(); try { $other[] = Activitypub_profile::from_profile($parent_notice->getProfile()); @@ -189,7 +189,7 @@ class ActivityPubQueueHandler extends QueueHandler // This is not a reply to something (has no parent) } catch (NoResultException $e) { // Parent author's profile not found! Complain louder? - common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + common_log(LOG_ERR, "Parent notice's author not found: " . $e->getMessage()); } } @@ -242,7 +242,7 @@ class ActivityPubQueueHandler extends QueueHandler // This is not a reply to something (has no parent) } catch (NoResultException $e) { // Parent author's profile not found! Complain louder? - common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + common_log(LOG_ERR, "Parent notice's author not found: " . $e->getMessage()); } } diff --git a/plugins/ActivityPub/lib/inbox_handler.php b/plugins/ActivityPub/lib/inbox_handler.php index b3f802ac06..9fb466d6c3 100644 --- a/plugins/ActivityPub/lib/inbox_handler.php +++ b/plugins/ActivityPub/lib/inbox_handler.php @@ -331,7 +331,7 @@ class Activitypub_inbox_handler private function handle_like() { $notice = ActivityPubPlugin::grab_notice_from_url($this->object); - Fave::addNew($this->actor, $notice); + Activitypub_like::addNew($this->activity['id'], $this->actor, $notice); } /** @@ -390,7 +390,7 @@ class Activitypub_inbox_handler */ private function handle_undo_like() { - $notice = ActivityPubPlugin::grab_notice_from_url($this->object['object']); + $notice = ActivityPubPlugin::grab_notice_from_url($this->activity['id']); Fave::removeEntry($this->actor, $notice); } @@ -402,7 +402,7 @@ class Activitypub_inbox_handler */ private function handle_announce() { - $object_notice = ActivityPubPlugin::grab_notice_from_url($this->object); - $object_notice->repeat($this->actor, 'ActivityPub'); + $notice = ActivityPubPlugin::grab_notice_from_url($this->object); + Activitypub_announce::repeat($this->activity['id'], $this->actor, $notice); } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_announce.php b/plugins/ActivityPub/lib/models/Activitypub_announce.php index 7ffa6541f3..84d24b1359 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_announce.php +++ b/plugins/ActivityPub/lib/models/Activitypub_announce.php @@ -41,13 +41,16 @@ class Activitypub_announce * * @param Profile $actor * @param Notice $notice + * @param Notice $repeat_of * @return array pretty array to be used in a response * @author Diogo Cordeiro */ - public static function announce_to_array(Profile $actor, Notice $notice): array - { + public static function announce_to_array( + Profile $actor, + Notice $notice, + Notice $repeat_of + ): array { $actor_uri = $actor->getUri(); - $notice_url = Activitypub_notice::getUrl($notice); $to = [common_local_url('apActorFollowers', ['id' => $actor->getID()])]; foreach ($notice->getAttentionProfiles() as $to_profile) { @@ -58,13 +61,48 @@ class Activitypub_announce $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => common_root_url().'share_from_'.urlencode($actor_uri).'_to_'.urlencode($notice_url), - "type" => "Announce", - "actor" => $actor_uri, - "object" => $notice_url, - "to" => $to, - "cc" => $cc + 'id' => Activitypub_notice::getUri($notice), + 'type' => 'Announce', + 'actor' => $actor_uri, + 'object' => Activitypub_notice::getUri($repeat_of), + 'to' => $to, + 'cc' => $cc, ]; return $res; } + + /** + * Convenience function for posting a repeat of an existing message. + * + * @param string $uri + * @param Profile $actor Profile which is doing the repeat + * @param Notice $target + * @return Notice + */ + public static function repeat(string $uri, Profile $actor, Notice $target): Notice + { + // TRANS: Message used to repeat a notice. RT is the abbreviation of 'retweet'. + // TRANS: %1$s is the repeated user's name, %2$s is the repeated notice. + $content = sprintf( + _('RT @%1$s %2$s'), + $actor->getNickname(), + $target->getContent() + ); + + $options = [ + 'source' => 'ActivityPub', + 'uri' => $uri, + 'is_local' => ($actor->isLocal() ? Notice::LOCAL_PUBLIC : Notice::REMOTE), + 'repeat_of' => $target->getParent()->getID(), + 'scope' => $target->getScope(), + ]; + + // Scope is same as this one's + return Notice::saveNew( + $actor->getID(), + $content, + 'ActivityPub', + $options + ); + } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_create.php b/plugins/ActivityPub/lib/models/Activitypub_create.php index 15207a61e7..0a6c0e50a6 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_create.php +++ b/plugins/ActivityPub/lib/models/Activitypub_create.php @@ -49,7 +49,7 @@ class Activitypub_create { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $object['id'] . '/create', + 'id' => $object['id'] . '#create', 'type' => 'Create', 'directMessage' => $directMessage, 'to' => $object['to'], diff --git a/plugins/ActivityPub/lib/models/Activitypub_delete.php b/plugins/ActivityPub/lib/models/Activitypub_delete.php index 6cfc97412e..439bd953db 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_delete.php +++ b/plugins/ActivityPub/lib/models/Activitypub_delete.php @@ -48,7 +48,7 @@ class Activitypub_delete { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $object.'/delete', + 'id' => $object . '#delete', 'type' => 'Delete', 'to' => ['https://www.w3.org/ns/activitystreams#Public'], 'actor' => $actor, diff --git a/plugins/ActivityPub/lib/models/Activitypub_like.php b/plugins/ActivityPub/lib/models/Activitypub_like.php index 9aa72c5b16..a6e9864532 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_like.php +++ b/plugins/ActivityPub/lib/models/Activitypub_like.php @@ -39,20 +39,73 @@ class Activitypub_like /** * Generates an ActivityPub representation of a Like * - * @author Diogo Cordeiro * @param string $actor Actor URI - * @param string $object Notice URI + * @param Notice $notice Notice URI * @return array pretty array to be used in a response + * @author Diogo Cordeiro */ - public static function like_to_array($actor, $object) + public static function like_to_array(string $actor, Notice $notice): array { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => common_root_url().'like_from_'.urlencode($actor).'_to_'.urlencode($object), - "type" => "Like", - "actor" => $actor, - "object" => $object + 'id' => Activitypub_notice::getUri($notice), + 'type' => 'Like', + 'actor' => $actor, + 'object' => Activitypub_notice::getUri($notice->getParent()), ]; return $res; } + + /** + * Save a favorite record. + * + * @param string $uri + * @param Profile $actor the local or remote Profile who favorites + * @param Notice $target the notice that is favorited + * @return Notice record on success + * @throws AlreadyFulfilledException + * @throws ClientException + * @throws NoticeSaveException + * @throws ServerException + */ + public static function addNew(string $uri, Profile $actor, Notice $target): Notice + { + if (Fave::existsForProfile($target, $actor)) { + // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite. + throw new AlreadyFulfilledException(_m('You have already favorited this!')); + } + + $act = new Activity(); + $act->type = ActivityObject::ACTIVITY; + $act->verb = ActivityVerb::FAVORITE; + $act->time = time(); + $act->id = $uri; + $act->title = _m('Favor'); + // TRANS: Message that is the "content" of a favorite (%1$s is the actor's nickname, %2$ is the favorited + // notice's nickname and %3$s is the content of the favorited notice.) + $act->content = sprintf( + _m('%1$s favorited something by %2$s: %3$s'), + $actor->getNickname(), + $target->getProfile()->getNickname(), + $target->getRendered() + ); + $act->actor = $actor->asActivityObject(); + $act->target = $target->asActivityObject(); + $act->objects = [clone($act->target)]; + + $url = common_local_url('AtomPubShowFavorite', ['profile'=>$actor->id, 'notice'=>$target->id]); + $act->selfLink = $url; + $act->editLink = $url; + + $options = [ + 'source' => 'ActivityPub', + 'uri' => $act->id, + 'url' => $url, + 'is_local' => ($actor->isLocal() ? Notice::LOCAL_PUBLIC : Notice::REMOTE), + 'scope' => $target->getScope(), + ]; + + // saveActivity will in turn also call Fave::saveActivityObject + return Notice::saveActivity($act, $actor, $options); + } } diff --git a/plugins/ActivityPub/lib/models/Activitypub_notice.php b/plugins/ActivityPub/lib/models/Activitypub_notice.php index 92c28bffec..cc1312b292 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_notice.php +++ b/plugins/ActivityPub/lib/models/Activitypub_notice.php @@ -47,7 +47,7 @@ class Activitypub_notice * @throws Exception * @author Diogo Cordeiro */ - public static function notice_to_array($notice) + public static function notice_to_array(Notice $notice): array { $profile = $notice->getProfile(); $attachments = []; @@ -81,10 +81,10 @@ class Activitypub_notice $item = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => self::getUrl($notice), + 'id' => self::getUri($notice), 'type' => 'Note', 'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z', - 'url' => self::getUrl($notice), + 'url' => $notice->getUrl(), 'attributedTo' => $profile->getUri(), 'to' => $to, 'cc' => $cc, @@ -97,7 +97,7 @@ class Activitypub_notice // Is this a reply? if (!empty($notice->reply_to)) { - $item['inReplyTo'] = self::getUrl(Notice::getById($notice->reply_to)); + $item['inReplyTo'] = self::getUri(Notice::getById($notice->reply_to)); } // Do we have a location for this notice? @@ -207,9 +207,9 @@ class Activitypub_notice } /* Reject notice if it is too long (without the HTML) - if (Notice::contentTooLong($content)) { - throw new Exception('That\'s too long. Maximum notice size is %d character.'); - }*/ + if (Notice::contentTooLong($content)) { + throw new Exception('That\'s too long. Maximum notice size is %d character.'); + }*/ // Attachments (first part) $attachments = []; @@ -253,7 +253,7 @@ class Activitypub_notice * @throws Exception if invalid ActivityPub object * @author Diogo Cordeiro */ - public static function validate_note($object) + public static function validate_note(array $object): bool { if (!isset($object['id'])) { common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.'); @@ -290,7 +290,7 @@ class Activitypub_notice * @throws Exception * @author Bruno Casteleiro */ - public static function getUrl(Notice $notice): string + public static function getUri(Notice $notice): string { if ($notice->isLocal()) { return common_local_url('apNotice', ['id' => $notice->getID()]); diff --git a/plugins/ActivityPub/lib/models/Activitypub_reject.php b/plugins/ActivityPub/lib/models/Activitypub_reject.php index 53c2ec4293..2d5bf2bbb1 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_reject.php +++ b/plugins/ActivityPub/lib/models/Activitypub_reject.php @@ -43,7 +43,7 @@ class Activitypub_reject * @param array $object * @return array pretty array to be used in a response */ - public static function reject_to_array($object) + public static function reject_to_array(array $object): array { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', diff --git a/plugins/ActivityPub/lib/models/Activitypub_tag.php b/plugins/ActivityPub/lib/models/Activitypub_tag.php index 549f771f98..3aaa9289f8 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_tag.php +++ b/plugins/ActivityPub/lib/models/Activitypub_tag.php @@ -40,15 +40,15 @@ class Activitypub_tag * Generates a pretty tag from a Tag object * * @author Diogo Cordeiro - * @param array Tag $tag + * @param string $tag * @return array pretty array to be used in a response */ - public static function tag_to_array($tag) + public static function tag_to_array(string $tag): array { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', 'name' => $tag, - 'url' => common_local_url('tag', ['tag' => $tag]) + 'url' => common_local_url('tag', ['tag' => $tag]), ]; return $res; } diff --git a/plugins/ActivityPub/lib/models/Activitypub_undo.php b/plugins/ActivityPub/lib/models/Activitypub_undo.php index e0dd5d154a..2e497985a7 100644 --- a/plugins/ActivityPub/lib/models/Activitypub_undo.php +++ b/plugins/ActivityPub/lib/models/Activitypub_undo.php @@ -43,11 +43,11 @@ class Activitypub_undo * @param array $object * @return array pretty array to be used in a response */ - public static function undo_to_array($object) + public static function undo_to_array(array $object): array { $res = [ '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $object['id'].'/undo', + 'id' => $object['id'] . '#undo', 'type' => 'Undo', 'actor' => $object['actor'], 'object' => $object @@ -63,7 +63,7 @@ class Activitypub_undo * @throws Exception * @author Diogo Cordeiro */ - public static function validate_object($object) + public static function validate_object(array $object): bool { if (!is_array($object)) { throw new Exception('Invalid Object Format for Undo Activity.'); diff --git a/plugins/ActivityPub/lib/postman.php b/plugins/ActivityPub/lib/postman.php index 3d360f9674..db82e3f1f6 100644 --- a/plugins/ActivityPub/lib/postman.php +++ b/plugins/ActivityPub/lib/postman.php @@ -141,8 +141,8 @@ class Activitypub_postman Activitypub_follow::follow_to_array( $this->actor_uri, $this->to[0]->getUrl() - ) - ); + ) + ); $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); $res_body = json_decode($res->getBody(), true); @@ -174,8 +174,8 @@ class Activitypub_postman $this->to[0]->getUrl(), $this->actor_uri, $id - ) - ); + ) + ); $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); $res_body = json_decode($res->getBody(), true); @@ -199,18 +199,18 @@ class Activitypub_postman * @throws Exception * @author Diogo Cordeiro */ - public function like($notice) + public function like(Notice $notice): void { $data = Activitypub_like::like_to_array( $this->actor_uri, - Activitypub_notice::getUrl($notice) - ); + $notice + ); $data = json_encode($data, JSON_UNESCAPED_SLASHES); foreach ($this->to_inbox() as $inbox) { $res = $this->send($data, $inbox); - // accummulate errors for later use, if needed + // accumulate errors for later use, if needed if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) { $res_body = json_decode($res->getBody(), true); $errors[] = isset($res_body['error']) ? @@ -237,9 +237,9 @@ class Activitypub_postman $data = Activitypub_undo::undo_to_array( Activitypub_like::like_to_array( $this->actor_uri, - Activitypub_notice::getUrl($notice) - ) - ); + $notice + ) + ); $data = json_encode($data, JSON_UNESCAPED_SLASHES); foreach ($this->to_inbox() as $inbox) { @@ -273,7 +273,7 @@ class Activitypub_postman $data = Activitypub_create::create_to_array( $this->actor_uri, Activitypub_notice::notice_to_array($notice) - ); + ); $data = json_encode($data, JSON_UNESCAPED_SLASHES); foreach ($this->to_inbox() as $inbox) { @@ -327,14 +327,16 @@ class Activitypub_postman * Send a Announce notification to remote instances * * @param Notice $notice + * @param Notice $repeat_of * @throws HTTP_Request2_Exception - * @throws Exception * @author Diogo Cordeiro */ - public function announce($notice) + public function announce(Notice $notice, Notice $repeat_of): void { - $data = json_encode(Activitypub_announce::announce_to_array($this->actor, $notice), - JSON_UNESCAPED_SLASHES); + $data = json_encode( + Activitypub_announce::announce_to_array($this->actor, $notice, $repeat_of), + JSON_UNESCAPED_SLASHES + ); foreach ($this->to_inbox() as $inbox) { $res = $this->send($data, $inbox); @@ -365,8 +367,8 @@ class Activitypub_postman { $data = Activitypub_delete::delete_to_array( $notice->getProfile()->getUri(), - Activitypub_notice::getUrl($notice) - ); + Activitypub_notice::getUri($notice) + ); $errors = []; $data = json_encode($data, JSON_UNESCAPED_SLASHES); foreach ($this->to_inbox() as $inbox) {