[ActivityPub] Fix issues concerning Activity URIs

And some other minor bugs.
This commit is contained in:
Diogo Cordeiro 2020-08-28 01:12:40 +01:00
parent 11ebb98919
commit c75bf1a19d
13 changed files with 169 additions and 67 deletions

View File

@ -180,6 +180,14 @@ class ActivityPubPlugin extends Plugin
$acceptHeaders $acceptHeaders
); );
// v3
$m->connect(
'activity/:id',
['action' => 'apNotice'],
['id' => '[0-9]+'],
);
// v2
$m->connect( $m->connect(
'notice/:id', 'notice/:id',
['action' => 'apNotice'], ['action' => 'apNotice'],

View File

@ -53,11 +53,12 @@ class apNoticeAction extends ManagedAction
try { try {
$notice = Notice::getByID($this->trimmed('id')); $notice = Notice::getByID($this->trimmed('id'));
} catch (Exception $e) { } catch (Exception $e) {
ActivityPubReturn::error('Invalid Notice URI.', 404); ActivityPubReturn::error('Invalid Activity URI.', 404);
} }
if (!$notice->isLocal()) { 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); $res = Activitypub_notice::notice_to_array($notice);

View File

@ -54,7 +54,7 @@ class ActivityPubQueueHandler extends QueueHandler
public function handle($notice): bool public function handle($notice): bool
{ {
if (!($notice instanceof Notice)) { 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; return true;
} }
@ -74,7 +74,7 @@ class ActivityPubQueueHandler extends QueueHandler
); );
// Handling a Create? // 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); return $this->handle_create($profile, $notice, $other);
} }
@ -143,7 +143,7 @@ class ActivityPubQueueHandler extends QueueHandler
// That was it // That was it
$postman = new Activitypub_postman($profile, $other); $postman = new Activitypub_postman($profile, $other);
$postman->announce($repeated_notice); $postman->announce($notice, $repeated_notice);
} }
// either made the announce or found nothing to repeat // 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. * Notify remote users when their notices get favourited.
* *
* @param Profile $profile of local user doing the faving * @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 * @return bool return value
* @throws HTTP_Request2_Exception * @throws HTTP_Request2_Exception
* @throws InvalidUrlException * @throws InvalidUrlException
@ -168,10 +168,10 @@ class ActivityPubQueueHandler extends QueueHandler
*/ */
public function onEndFavorNotice(Profile $profile, Notice $notice, $other) public function onEndFavorNotice(Profile $profile, Notice $notice, $other)
{ {
$notice = $notice->getParent(); $notice_liked = $notice->getParent();
if ($notice->reply_to) { if ($notice_liked->reply_to) {
try { try {
$parent_notice = $notice->getParent(); $parent_notice = $notice_liked->getParent();
try { try {
$other[] = Activitypub_profile::from_profile($parent_notice->getProfile()); $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) // This is not a reply to something (has no parent)
} catch (NoResultException $e) { } catch (NoResultException $e) {
// Parent author's profile not found! Complain louder? // 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) // This is not a reply to something (has no parent)
} catch (NoResultException $e) { } catch (NoResultException $e) {
// Parent author's profile not found! Complain louder? // 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());
} }
} }

View File

@ -331,7 +331,7 @@ class Activitypub_inbox_handler
private function handle_like() private function handle_like()
{ {
$notice = ActivityPubPlugin::grab_notice_from_url($this->object); $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() 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); Fave::removeEntry($this->actor, $notice);
} }
@ -402,7 +402,7 @@ class Activitypub_inbox_handler
*/ */
private function handle_announce() private function handle_announce()
{ {
$object_notice = ActivityPubPlugin::grab_notice_from_url($this->object); $notice = ActivityPubPlugin::grab_notice_from_url($this->object);
$object_notice->repeat($this->actor, 'ActivityPub'); Activitypub_announce::repeat($this->activity['id'], $this->actor, $notice);
} }
} }

View File

@ -41,13 +41,16 @@ class Activitypub_announce
* *
* @param Profile $actor * @param Profile $actor
* @param Notice $notice * @param Notice $notice
* @param Notice $repeat_of
* @return array pretty array to be used in a response * @return array pretty array to be used in a response
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
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(); $actor_uri = $actor->getUri();
$notice_url = Activitypub_notice::getUrl($notice);
$to = [common_local_url('apActorFollowers', ['id' => $actor->getID()])]; $to = [common_local_url('apActorFollowers', ['id' => $actor->getID()])];
foreach ($notice->getAttentionProfiles() as $to_profile) { foreach ($notice->getAttentionProfiles() as $to_profile) {
@ -58,13 +61,48 @@ class Activitypub_announce
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => common_root_url().'share_from_'.urlencode($actor_uri).'_to_'.urlencode($notice_url), 'id' => Activitypub_notice::getUri($notice),
"type" => "Announce", 'type' => 'Announce',
"actor" => $actor_uri, 'actor' => $actor_uri,
"object" => $notice_url, 'object' => Activitypub_notice::getUri($repeat_of),
"to" => $to, 'to' => $to,
"cc" => $cc 'cc' => $cc,
]; ];
return $res; 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
);
}
} }

View File

@ -49,7 +49,7 @@ class Activitypub_create
{ {
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $object['id'] . '/create', 'id' => $object['id'] . '#create',
'type' => 'Create', 'type' => 'Create',
'directMessage' => $directMessage, 'directMessage' => $directMessage,
'to' => $object['to'], 'to' => $object['to'],

View File

@ -48,7 +48,7 @@ class Activitypub_delete
{ {
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $object.'/delete', 'id' => $object . '#delete',
'type' => 'Delete', 'type' => 'Delete',
'to' => ['https://www.w3.org/ns/activitystreams#Public'], 'to' => ['https://www.w3.org/ns/activitystreams#Public'],
'actor' => $actor, 'actor' => $actor,

View File

@ -39,20 +39,73 @@ class Activitypub_like
/** /**
* Generates an ActivityPub representation of a Like * Generates an ActivityPub representation of a Like
* *
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $actor Actor URI * @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 * @return array pretty array to be used in a response
* @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public static function like_to_array($actor, $object) public static function like_to_array(string $actor, Notice $notice): array
{ {
$res = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => common_root_url().'like_from_'.urlencode($actor).'_to_'.urlencode($object), 'id' => Activitypub_notice::getUri($notice),
"type" => "Like", 'type' => 'Like',
"actor" => $actor, 'actor' => $actor,
"object" => $object 'object' => Activitypub_notice::getUri($notice->getParent()),
]; ];
return $res; 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);
}
} }

View File

@ -47,7 +47,7 @@ class Activitypub_notice
* @throws Exception * @throws Exception
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public static function notice_to_array($notice) public static function notice_to_array(Notice $notice): array
{ {
$profile = $notice->getProfile(); $profile = $notice->getProfile();
$attachments = []; $attachments = [];
@ -81,10 +81,10 @@ class Activitypub_notice
$item = [ $item = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => self::getUrl($notice), 'id' => self::getUri($notice),
'type' => 'Note', 'type' => 'Note',
'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z', 'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z',
'url' => self::getUrl($notice), 'url' => $notice->getUrl(),
'attributedTo' => $profile->getUri(), 'attributedTo' => $profile->getUri(),
'to' => $to, 'to' => $to,
'cc' => $cc, 'cc' => $cc,
@ -97,7 +97,7 @@ class Activitypub_notice
// Is this a reply? // Is this a reply?
if (!empty($notice->reply_to)) { 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? // 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) /* Reject notice if it is too long (without the HTML)
if (Notice::contentTooLong($content)) { if (Notice::contentTooLong($content)) {
throw new Exception('That\'s too long. Maximum notice size is %d character.'); throw new Exception('That\'s too long. Maximum notice size is %d character.');
}*/ }*/
// Attachments (first part) // Attachments (first part)
$attachments = []; $attachments = [];
@ -253,7 +253,7 @@ class Activitypub_notice
* @throws Exception if invalid ActivityPub object * @throws Exception if invalid ActivityPub object
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public static function validate_note($object) public static function validate_note(array $object): bool
{ {
if (!isset($object['id'])) { if (!isset($object['id'])) {
common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.'); common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.');
@ -290,7 +290,7 @@ class Activitypub_notice
* @throws Exception * @throws Exception
* @author Bruno Casteleiro <brunoccast@fc.up.pt> * @author Bruno Casteleiro <brunoccast@fc.up.pt>
*/ */
public static function getUrl(Notice $notice): string public static function getUri(Notice $notice): string
{ {
if ($notice->isLocal()) { if ($notice->isLocal()) {
return common_local_url('apNotice', ['id' => $notice->getID()]); return common_local_url('apNotice', ['id' => $notice->getID()]);

View File

@ -43,7 +43,7 @@ class Activitypub_reject
* @param array $object * @param array $object
* @return array pretty array to be used in a response * @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 = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',

View File

@ -40,15 +40,15 @@ class Activitypub_tag
* Generates a pretty tag from a Tag object * Generates a pretty tag from a Tag object
* *
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
* @param array Tag $tag * @param string $tag
* @return array pretty array to be used in a response * @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 = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'name' => $tag, 'name' => $tag,
'url' => common_local_url('tag', ['tag' => $tag]) 'url' => common_local_url('tag', ['tag' => $tag]),
]; ];
return $res; return $res;
} }

View File

@ -43,11 +43,11 @@ class Activitypub_undo
* @param array $object * @param array $object
* @return array pretty array to be used in a response * @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 = [ $res = [
'@context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $object['id'].'/undo', 'id' => $object['id'] . '#undo',
'type' => 'Undo', 'type' => 'Undo',
'actor' => $object['actor'], 'actor' => $object['actor'],
'object' => $object 'object' => $object
@ -63,7 +63,7 @@ class Activitypub_undo
* @throws Exception * @throws Exception
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public static function validate_object($object) public static function validate_object(array $object): bool
{ {
if (!is_array($object)) { if (!is_array($object)) {
throw new Exception('Invalid Object Format for Undo Activity.'); throw new Exception('Invalid Object Format for Undo Activity.');

View File

@ -141,8 +141,8 @@ class Activitypub_postman
Activitypub_follow::follow_to_array( Activitypub_follow::follow_to_array(
$this->actor_uri, $this->actor_uri,
$this->to[0]->getUrl() $this->to[0]->getUrl()
) )
); );
$res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
$res_body = json_decode($res->getBody(), true); $res_body = json_decode($res->getBody(), true);
@ -174,8 +174,8 @@ class Activitypub_postman
$this->to[0]->getUrl(), $this->to[0]->getUrl(),
$this->actor_uri, $this->actor_uri,
$id $id
) )
); );
$res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox()); $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
$res_body = json_decode($res->getBody(), true); $res_body = json_decode($res->getBody(), true);
@ -199,18 +199,18 @@ class Activitypub_postman
* @throws Exception * @throws Exception
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public function like($notice) public function like(Notice $notice): void
{ {
$data = Activitypub_like::like_to_array( $data = Activitypub_like::like_to_array(
$this->actor_uri, $this->actor_uri,
Activitypub_notice::getUrl($notice) $notice
); );
$data = json_encode($data, JSON_UNESCAPED_SLASHES); $data = json_encode($data, JSON_UNESCAPED_SLASHES);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$res = $this->send($data, $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)) { if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
$res_body = json_decode($res->getBody(), true); $res_body = json_decode($res->getBody(), true);
$errors[] = isset($res_body['error']) ? $errors[] = isset($res_body['error']) ?
@ -237,9 +237,9 @@ class Activitypub_postman
$data = Activitypub_undo::undo_to_array( $data = Activitypub_undo::undo_to_array(
Activitypub_like::like_to_array( Activitypub_like::like_to_array(
$this->actor_uri, $this->actor_uri,
Activitypub_notice::getUrl($notice) $notice
) )
); );
$data = json_encode($data, JSON_UNESCAPED_SLASHES); $data = json_encode($data, JSON_UNESCAPED_SLASHES);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
@ -273,7 +273,7 @@ class Activitypub_postman
$data = Activitypub_create::create_to_array( $data = Activitypub_create::create_to_array(
$this->actor_uri, $this->actor_uri,
Activitypub_notice::notice_to_array($notice) Activitypub_notice::notice_to_array($notice)
); );
$data = json_encode($data, JSON_UNESCAPED_SLASHES); $data = json_encode($data, JSON_UNESCAPED_SLASHES);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
@ -327,14 +327,16 @@ class Activitypub_postman
* Send a Announce notification to remote instances * Send a Announce notification to remote instances
* *
* @param Notice $notice * @param Notice $notice
* @param Notice $repeat_of
* @throws HTTP_Request2_Exception * @throws HTTP_Request2_Exception
* @throws Exception
* @author Diogo Cordeiro <diogo@fc.up.pt> * @author Diogo Cordeiro <diogo@fc.up.pt>
*/ */
public function announce($notice) public function announce(Notice $notice, Notice $repeat_of): void
{ {
$data = json_encode(Activitypub_announce::announce_to_array($this->actor, $notice), $data = json_encode(
JSON_UNESCAPED_SLASHES); Activitypub_announce::announce_to_array($this->actor, $notice, $repeat_of),
JSON_UNESCAPED_SLASHES
);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$res = $this->send($data, $inbox); $res = $this->send($data, $inbox);
@ -365,8 +367,8 @@ class Activitypub_postman
{ {
$data = Activitypub_delete::delete_to_array( $data = Activitypub_delete::delete_to_array(
$notice->getProfile()->getUri(), $notice->getProfile()->getUri(),
Activitypub_notice::getUrl($notice) Activitypub_notice::getUri($notice)
); );
$errors = []; $errors = [];
$data = json_encode($data, JSON_UNESCAPED_SLASHES); $data = json_encode($data, JSON_UNESCAPED_SLASHES);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {