diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php index 474d8f6..3d0b9ab 100644 --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -52,7 +52,7 @@ class ActivityPubPlugin extends Plugin ['action' => 'showstream'], ['nickname' => Nickname::DISPLAY_FMT], 'apActorProfile'); - + $m->connect (':nickname/liked.json', ['action' => 'apActorLikedCollection'], ['nickname' => Nickname::DISPLAY_FMT]); @@ -101,7 +101,7 @@ class ActivityPubPlugin extends Plugin $schema->ensureTable ('Activitypub_profile', Activitypub_profile::schemaDef()); return true; } - + /******************************************************** * Delivery Events * ********************************************************/ @@ -176,13 +176,41 @@ class ActivityPubPlugin extends Plugin } $other = array (); - foreach ($notice->getAttentionProfileIDs () as $to_id) { + try { + $other[] = Activitypub_profile::from_profile($notice->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + foreach ($notice->getAttentionProfiles() as $to_profile) { try { - $other[] = Activitypub_profile::from_profile (Profile::getById ($to_id)); + $other[] = Activitypub_profile::from_profile ($to_profile); } catch (Exception $e) { // Local user can be ignored } } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent ()->getAttentionProfiles (); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile ($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // 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()); + } + } + $postman = new Activitypub_postman ($profile, $other); $postman->like ($notice); @@ -206,13 +234,41 @@ class ActivityPubPlugin extends Plugin } $other = array (); - foreach ($notice->getAttentionProfileIDs () as $to_id) { + try { + $other[] = Activitypub_profile::from_profile($notice->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + foreach ($notice->getAttentionProfiles() as $to_profile) { try { - $other[] = Activitypub_profile::from_profile (Profile::getById ($to_id)); + $other[] = Activitypub_profile::from_profile ($to_profile); } catch (Exception $e) { // Local user can be ignored } } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent ()->getAttentionProfiles (); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile ($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // 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()); + } + } + $postman = new Activitypub_postman ($profile, $other); $postman->undo_like ($notice); @@ -236,17 +292,123 @@ class ActivityPubPlugin extends Plugin } $other = array (); - foreach ($notice->getAttentionProfileIDs () as $to_id) { + + foreach ($notice->getAttentionProfiles() as $to_profile) { try { - $other[] = Activitypub_profile::from_profile (Profile::getById ($to_id)); + $other[] = Activitypub_profile::from_profile ($to_profile); } catch (Exception $e) { // Local user can be ignored } } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent ()->getAttentionProfiles (); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile ($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // 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()); + } + } + + $postman = new Activitypub_postman ($profile, $other); + $postman->delete ($notice); + return true; + } + + /** + * Insert notifications for replies, mentions and repeats + * + * @return boolean hook flag + */ + function onStartNoticeDistribute ($notice) + { + assert ($notice->id > 0); // Ignore if not a valid notice + + $profile = Profile::getKV ($notice->profile_id); + + $other = array (); + try { + $other[] = Activitypub_profile::from_profile($notice->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + foreach ($notice->getAttentionProfiles() as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile ($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + + // Is Announce + if ($notice->isRepeat ()) { + $repeated_notice = Notice::getKV ('id', $notice->repeat_of); + if ($repeated_notice instanceof Notice) { + try { + $other[] = Activitypub_profile::from_profile ($repeated_notice->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + + $postman = new Activitypub_postman ($profile, $other); + + // That was it + $postman->announce ($repeated_notice); + return true; + } + } + + // Ignore for activity/non-post-verb notices + if (method_exists ('ActivityUtils', 'compareVerbs')) { + $is_post_verb = ActivityUtils::compareVerbs ($notice->verb, + array (ActivityVerb::POST)); + } else { + $is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false); + } + if ($notice->source == 'activity' || !$is_post_verb) { + return true; + } + + // Create + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile ($notice->getParent ()->getProfile ()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent ()->getAttentionProfiles (); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile ($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // 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()); + } + } $postman = new Activitypub_postman ($profile, $other); - $postman->delete ($notice); - + // That was it + $postman->create ($notice); return true; } } diff --git a/actions/apsharedinbox.php b/actions/apsharedinbox.php index 007d392..cec4e42 100644 --- a/actions/apsharedinbox.php +++ b/actions/apsharedinbox.php @@ -58,13 +58,13 @@ class apSharedInboxAction extends ManagedAction $data = json_decode (file_get_contents ('php://input')); // Validate data - if (!isset($data->type)) { + if (!isset ($data->type)) { ActivityPubReturn::error ("Type was not specified."); } - if (!isset($data->actor)) { + if (!isset ($data->actor)) { ActivityPubReturn::error ("Actor was not specified."); } - if (!isset($data->object)) { + if (!isset ($data->object)) { ActivityPubReturn::error ("Object was not specified."); } @@ -91,7 +91,7 @@ class apSharedInboxAction extends ManagedAction if (!isset($data->to)) { ActivityPubReturn::error ("To was not specified."); } - $discovery = new Activitypub_Discovery; + $discovery = new Activitypub_explorer; $to_profiles = array (); // Generate To objects if (is_array ($data->to)) { @@ -128,6 +128,9 @@ class apSharedInboxAction extends ManagedAction case "Undo": require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Undo.php"; break; + case "Delete": + require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Delete.php"; + break; default: ActivityPubReturn::error ("Invalid type value."); } diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php index 4bffcaf..c12309a 100644 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -38,6 +38,9 @@ if (!(isset ($data->object->type) && in_array ($data->object->type, $valid_objec if (!isset ($data->object->content)) { ActivityPubReturn::error ("Object content was not specified."); } +if (!isset ($data->object->url)) { + ActivityPubReturn::error ("Object url was not specified."); +} $content = $data->object->content; @@ -50,9 +53,13 @@ $act->context = new ActivityContext (); // Is this a reply? if (isset ($data->object->reply_to)) { - $reply_to = Notice::getByUri ($data->object->reply_to); - $act->context->replyToID = $reply_to->getUri (); - $act->context->replyToUrl = $data->object->reply_to; + try { + $reply_to = Notice::getByUri ($data->object->reply_to); + } catch (Exception $e) { + ActivityPubReturn::error ("Invalid Object reply_to value."); + } + $act->context->replyToID = $reply_to->getUri (); + $act->context->replyToUrl = $reply_to->getUrl (); } else { $reply_to = null; } @@ -70,7 +77,7 @@ if (Notice::contentTooLong ($content)) { ActivityPubReturn::error ("That's too long. Maximum notice size is %d character."); } -$options = array ('source' => 'ActivityPub', 'uri' => $data->id); +$options = array ('source' => 'ActivityPub', 'uri' => $data->id, 'url' => $data->object->url); // $options gets filled with possible scoping settings ToSelector::fillActivity ($this, $act, $options); @@ -84,6 +91,7 @@ $act->objects[] = $actobj; try { $res = array ("@context" => "https://www.w3.org/ns/activitystreams", "id" => $data->id, + "url" => $data->object->url, "type" => "Create", "actor" => $data->actor, "object" => Activitypub_notice::notice_to_array (Notice::saveActivity ($act, $actor_profile, $options))); diff --git a/actions/inbox/Delete.php b/actions/inbox/Delete.php index 19cb891..f4e60a8 100644 --- a/actions/inbox/Delete.php +++ b/actions/inbox/Delete.php @@ -30,7 +30,7 @@ if (!defined ('GNUSOCIAL')) { } try { - Activitypub_notice::getKV ("url", $data->object)->deleteAs ($actor_profile); + Notice::getByUri ($data->object)->deleteAs ($actor_profile); $res = array ("@context" => "https://www.w3.org/ns/activitystreams", "type" => "Delete", "actor" => $data->actor, diff --git a/actions/inbox/Like.php b/actions/inbox/Like.php index b96ef38..7c64e81 100644 --- a/actions/inbox/Like.php +++ b/actions/inbox/Like.php @@ -30,7 +30,7 @@ if (!defined ('GNUSOCIAL')) { } try { - Fave::addNew ($actor_profile, Notice::getKV ("url", $data->object)); + Fave::addNew ($actor_profile, Notice::getByUri ($data->object)); $res = array ("@context" => "https://www.w3.org/ns/activitystreams", "type" => "Like", "actor" => $data->actor, diff --git a/actions/inbox/Undo.php b/actions/inbox/Undo.php index e4f93f2..723c421 100644 --- a/actions/inbox/Undo.php +++ b/actions/inbox/Undo.php @@ -41,8 +41,7 @@ case "Like": if (!isset ($data->object->object)) { ActivityPubReturn::error ("Object Notice URL was not specified."); } - - Fave::removeEntry ($actor_profile, Notice::getKV ("url", $data->object->object)); + Fave::removeEntry ($actor_profile, Notice::getByUri ($data->object->object)); ActivityPubReturn::answer ("Notice disfavorited successfully."); } catch (Exception $e) { ActivityPubReturn::error ($e->getMessage (), 403); diff --git a/classes/Activitypub_profile.php b/classes/Activitypub_profile.php index d62a150..a1cb4da 100644 --- a/classes/Activitypub_profile.php +++ b/classes/Activitypub_profile.php @@ -1,4 +1,5 @@ isLocal ()) { + // create one! + $aprofile = self::create_from_local_profile ($profile); + } else { + throw new Exception ('No Activitypub_profile for Profile ID: '.$profile_id. ', this probably is a local profile.'); + } } foreach ($profile as $key => $value) { @@ -188,6 +195,35 @@ class Activitypub_profile extends Profile return $aprofile; } + /** + * Given an existent local profile creates an ActivityPub profile. + * One must be careful not to give a user profile to this function + * as only remote users have ActivityPub_profiles on local instance + * + * @param Profile $profile + * @return Activitypub_profile + */ + private static function create_from_local_profile (Profile $profile) + { + $url = $profile->getURL (); + $inboxes = Activitypub_explorer::get_actor_inboxes_uri ($url); + + $aprofile->created = $aprofile->modified = common_sql_now (); + + $aprofile = new Activitypub_profile; + $aprofile->profile_id = $profile->getID (); + $aprofile->uri = $url; + $aprofile->nickname = $profile->getNickname (); + $aprofile->fullname = $profile->getFullname (); + $aprofile->bio = substr ($profile->getDescription (), 0, 1000); + $aprofile->inboxuri = $inboxes["inbox"]; + $aprofile->sharedInboxuri = $inboxes["sharedInbox"]; + + $aprofile->insert (); + + return $aprofile; + } + /** * Returns sharedInbox if possible, inbox otherwise * diff --git a/utils/explorer.php b/utils/explorer.php index 67b05c9..8a70c5e 100644 --- a/utils/explorer.php +++ b/utils/explorer.php @@ -141,7 +141,7 @@ class Activitypub_explorer $this->_lookup ($res["next"]); } return true; - } else if ($this->validate_remote_response ($res)) { + } else if (self::validate_remote_response ($res)) { $this->discovered_actor_profiles[]= $this->store_profile ($res); return true; } @@ -163,7 +163,7 @@ class Activitypub_explorer $aprofile->fullname = $res["display_name"]; $aprofile->bio = substr ($res["summary"], 0, 1000); $aprofile->inboxuri = $res["inbox"]; - $aprofile->sharedInboxuri = $res["sharedInbox"]; + $aprofile->sharedInboxuri = isset ($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"]; $aprofile->do_insert (); @@ -177,9 +177,9 @@ class Activitypub_explorer * @param array $res remote response * @return boolean success state */ - private function validate_remote_response ($res) + private static function validate_remote_response ($res) { - if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"], $res["sharedInbox"])) { + if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"])) { return false; } @@ -210,4 +210,29 @@ class Activitypub_explorer } return $i; } + + /** + * Given a valid actor profile url returns its inboxes + * + * @param string $url of Actor profile + * @return boolean|array false if fails | array with inbox and shared inbox if successful + */ + static function get_actor_inboxes_uri ($url) + { + $client = new HTTPClient (); + $headers = array(); + $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; + $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; + $response = $client->get ($url, $headers); + if (!$response->isOk ()) { + throw new Exception ("Invalid Actor URL."); + } + $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); + if (self::validate_remote_response ($res)) { + return array ("inbox" => $res["inbox"], + "sharedInbox" => isset ($res["sharedInbox"]) ? $res["sharedInbox"] : $res["inbox"]); + } + + return false; + } } diff --git a/utils/postman.php b/utils/postman.php index fca622d..d7ae125 100644 --- a/utils/postman.php +++ b/utils/postman.php @@ -104,7 +104,7 @@ class Activitypub_postman $data = array ("@context" => "https://www.w3.org/ns/activitystreams", "type" => "Like", "actor" => $this->actor->getUrl (), - "object" => $notice->getUrl ()); + "object" => $notice->getUri ()); $this->client->setBody (json_encode ($data)); foreach ($this->to_inbox () as $inbox) { $this->client->post ($inbox, $this->headers); @@ -123,7 +123,7 @@ class Activitypub_postman "actor" => $this->actor->getUrl (), "object" => array ( "type" => "Like", - "object" => $notice->getUrl () + "object" => $notice->getUri () ) ); $this->client->setBody (json_encode ($data)); @@ -132,6 +132,54 @@ class Activitypub_postman } } + /** + * Send a Announce notification to remote instances + * + * @param Notice $notice + */ + public function announce ($notice) + { + $data = array ("@context" => "https://www.w3.org/ns/activitystreams", + "id" => $notice->getUri (), + "url" => $notice->getUrl (), + "type" => "Announce", + "actor" => $this->actor->getUrl (), + "to" => "https://www.w3.org/ns/activitystreams#Public", + "object" => $notice->getUri () + ); + $this->client->setBody (json_encode ($data)); + foreach ($this->to_inbox () as $inbox) { + $this->client->post ($inbox, $this->headers); + } + } + + /** + * Send a Create notification to remote instances + * + * @param Notice $notice + */ + public function create ($notice) + { + $data = array ("@context" => "https://www.w3.org/ns/activitystreams", + "id" => $notice->getUri (), + "type" => "Create", + "actor" => $this->actor->getUrl (), + "to" => "https://www.w3.org/ns/activitystreams#Public", + "object" => array ( + "type" => "Note", + "url" => $notice->getUrl (), + "content" => $notice->getContent () + ) + ); + if (isset ($notice->reply_to)) { + $data["object"]["reply_to"] = $notice->getParent ()->getUri (); + } + $this->client->setBody (json_encode ($data)); + foreach ($this->to_inbox () as $inbox) { + $this->client->post ($inbox, $this->headers); + } + } + /** * Send a Delete notification to remote instances holding the notice * @@ -142,7 +190,7 @@ class Activitypub_postman $data = array ("@context" => "https://www.w3.org/ns/activitystreams", "type" => "Delete", "actor" => $this->actor->getUrl (), - "object" => $notice->getUrl () + "object" => $notice->getUri () ); $this->client->setBody (json_encode ($data)); foreach ($this->to_inbox () as $inbox) {