From 4cb1ed2c9da673ffc0a0d752906ebdee292c5ea4 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Fri, 6 Jul 2018 11:47:28 +0100 Subject: [PATCH 1/6] Add discovery module --- utils/discovery.php | 65 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 utils/discovery.php diff --git a/utils/discovery.php b/utils/discovery.php new file mode 100644 index 0000000..f1c45da --- /dev/null +++ b/utils/discovery.php @@ -0,0 +1,65 @@ +. + * + * @category Feed + * @package GNUsocial + * @author Daniel Supernault + * @author Diogo Cordeiro + * @copyright 2018 Free Software Foundaction, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link https://gnu.io/social + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class Activitypub_Discovery { + public function lookup ($url) + { + // First check if we already have it locally + if (($actor_profile = Profile::getKV("profileurl", $url)) != false) { + return $actor_profile; + } + + // If that's not the case, grab it + $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); + $this->response = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); + if (!$response->isOk ()) + ActivityPubReturn::error("Invalid Actor URL", 404); + return $this->storeProfile (); + } + + public function storeProfile () + { + $res = $this->response; + $profile = new Profile; + $profile->profileurl = $res["url"]; + $profile->nickname = $res["nickname"]; + $profile->fulname = $res["display_name"]; + $profile->bio = str_limit($res["summary"], 1000); + $profile->insert (); + + return $profile; + } +} From 4d3321ac21faa1ce82eca54338043dcebae29be9 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Fri, 6 Jul 2018 21:31:21 +0100 Subject: [PATCH 2/6] Minor bug fix and workaround a potential GNU Social issue --- utils/discovery.php | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/utils/discovery.php b/utils/discovery.php index f1c45da..fe5a2cc 100644 --- a/utils/discovery.php +++ b/utils/discovery.php @@ -33,12 +33,25 @@ if (!defined('GNUSOCIAL')) { exit(1); } class Activitypub_Discovery { public function lookup ($url) { - // First check if we already have it locally + // First check if we already have it locally and, if so, return it if (($actor_profile = Profile::getKV("profileurl", $url)) != false) { return $actor_profile; + } else { + // Sometimes it is not true that the user is not locally available, + // mostly when it is a local user and URLs slightly changed + // (not sure if this is necessary, but anyway) + + // Iff we really are in the same instance + $root_url_len = strlen (common_root_url ()); + if (substr ($url, 0, $root_url_len) == common_root_url ()) { + // Grab the nickname and try to get the user + if (($actor_profile = Profile::getKV("nickname", substr ($url, $root_url_len))) != false) { + return $actor_profile; + } + } } - // If that's not the case, grab it + // If the local fetch fails: grab it remotely, store locally and return $client = new HTTPClient (); $headers = array(); $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; @@ -46,7 +59,7 @@ class Activitypub_Discovery { $response = $client->get ($url, $headers); $this->response = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); if (!$response->isOk ()) - ActivityPubReturn::error("Invalid Actor URL", 404); + ActivityPubReturn::error ("Invalid Actor URL", 404); return $this->storeProfile (); } @@ -56,7 +69,7 @@ class Activitypub_Discovery { $profile = new Profile; $profile->profileurl = $res["url"]; $profile->nickname = $res["nickname"]; - $profile->fulname = $res["display_name"]; + $profile->fullname = $res["display_name"]; $profile->bio = str_limit($res["summary"], 1000); $profile->insert (); From 167f88a60273874913f3ea36e0cac297aa3925c1 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Fri, 6 Jul 2018 21:52:03 +0100 Subject: [PATCH 3/6] We are not using laravel helpers on GNU Social --- utils/discovery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/discovery.php b/utils/discovery.php index fe5a2cc..d1fed4d 100644 --- a/utils/discovery.php +++ b/utils/discovery.php @@ -70,7 +70,7 @@ class Activitypub_Discovery { $profile->profileurl = $res["url"]; $profile->nickname = $res["nickname"]; $profile->fullname = $res["display_name"]; - $profile->bio = str_limit($res["summary"], 1000); + $profile->bio = substr ($res["summary"], 0, 1000); $profile->insert (); return $profile; From f533ef2d32824a93d5cac9436b0b661fdbef4282 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Mon, 9 Jul 2018 00:07:14 +0100 Subject: [PATCH 4/6] Minor corrections --- utils/discovery.php | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/utils/discovery.php b/utils/discovery.php index d1fed4d..4bf93d4 100644 --- a/utils/discovery.php +++ b/utils/discovery.php @@ -1,4 +1,7 @@ get ($url, $headers); $this->response = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); - if (!$response->isOk ()) - ActivityPubReturn::error ("Invalid Actor URL", 404); + if (!$response->isOk()) { + throw new NoResultException ("Invalid Actor URL."); + } return $this->storeProfile (); } - public function storeProfile () + private function storeProfile () { - $res = $this->response; + $res = $this->response; + // Validate response + if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"])) { + throw new NoProfileException("Invalid Actor URL."); + } + $profile = new Profile; $profile->profileurl = $res["url"]; $profile->nickname = $res["nickname"]; From cc69c331138b5820a80fc6109f45e8ef5733b017 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Mon, 9 Jul 2018 14:41:53 +0100 Subject: [PATCH 5/6] Now discovery module is able to go through collections --- utils/discovery.php | 74 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/utils/discovery.php b/utils/discovery.php index 4bf93d4..0edc062 100644 --- a/utils/discovery.php +++ b/utils/discovery.php @@ -34,12 +34,30 @@ use Profile; if (!defined('GNUSOCIAL')) { exit(1); } class Activitypub_Discovery { + private $discovered_actor_profiles = array (); + public function lookup ($url) + { + $this->discovered_actor_profiles = array (); + + return $this->_lookup ($url);; + } + + private function _lookup ($url) { // First check if we already have it locally and, if so, return it + // If the local fetch fails: grab it remotely, store locally and return + $this->grab_local_user ($url) || $this->grab_remote_user($url); + + return $this->discovered_actor_profiles; + } + + private function grab_local_user ($url) + { if (($actor_profile = Profile::getKV ("profileurl", $url)) != false) { - return $actor_profile; - } else { + $this->discovered_actor_profiles[]= $actor_profile; + return true; + } else { // XXX: // Sometimes it is not true that the user is not locally available, // mostly when it is a local user and URLs slightly changed // e.g.: GS instance owner changed from standard urls to pretty urls @@ -50,39 +68,61 @@ class Activitypub_Discovery { if (substr ($url, 0, $root_url_len) == common_root_url ()) { // Grab the nickname and try to get the user if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) { - return $actor_profile; + $this->discovered_actor_profiles[]= $actor_profile; + return true; } } } - - // If the local fetch fails: grab it remotely, store locally and return + return false; + } + + private function grab_remote_user ($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); - $this->response = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); if (!$response->isOk()) { throw new NoResultException ("Invalid Actor URL."); } - return $this->storeProfile (); - } - - private function storeProfile () - { - $res = $this->response; - // Validate response - if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"])) { - throw new NoProfileException("Invalid Actor URL."); + $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); + if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!! + foreach ($res["orderedItems"] as $profile) { + if ($this->_lookup ($profile) == false) { + // XXX: Invalid actor found, not sure how we handle those + } + } + // Go through entire collection + if (!is_null ($res["next"])) { + $this->_lookup ($res["next"]); + } + return true; + } else if ($this->validate_remote_response ($res)) { + $this->discovered_actor_profiles[]= $this->store_profile ($res); + return true; } - + + return false; + } + + private function store_profile ($res) { $profile = new Profile; $profile->profileurl = $res["url"]; $profile->nickname = $res["nickname"]; $profile->fullname = $res["display_name"]; $profile->bio = substr ($res["summary"], 0, 1000); $profile->insert (); - + return $profile; } + + private function validate_remote_response ($res) + { + if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"])) { + return false; + } + + return true; + } } From cb70a8f6e8702e88ee10a13d7dbb26cb704c66c3 Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Tue, 10 Jul 2018 00:17:18 +0100 Subject: [PATCH 6/6] Add and fix some code documentation --- utils/discovery.php | 238 +++++++++++++++++++++++++++----------------- 1 file changed, 145 insertions(+), 93 deletions(-) diff --git a/utils/discovery.php b/utils/discovery.php index 0edc062..1cd8635 100644 --- a/utils/discovery.php +++ b/utils/discovery.php @@ -5,9 +5,7 @@ use Profile; /** * GNU social - a federating social network * - * An activity - * - * PHP version 5 + * ActivityPubPlugin implementation for GNU Social * * LICENCE: This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -22,107 +20,161 @@ use Profile; * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * - * @category Feed + * @category Plugin * @package GNUsocial * @author Daniel Supernault * @author Diogo Cordeiro - * @copyright 2018 Free Software Foundaction, Inc. + * @copyright 2018 Free Software Foundation http://fsf.org * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link https://gnu.io/social + * @link https://www.gnu.org/software/social/ + */ +if (!defined ('GNUSOCIAL')) { + exit(1); +} + +/** + * @category Plugin + * @package GNUsocial + * @author Daniel Supernault + * @author Diogo Cordeiro + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ */ -if (!defined('GNUSOCIAL')) { exit(1); } +class Activitypub_Discovery +{ + private $discovered_actor_profiles = array (); -class Activitypub_Discovery { - private $discovered_actor_profiles = array (); - - public function lookup ($url) - { - $this->discovered_actor_profiles = array (); - - return $this->_lookup ($url);; - } - - private function _lookup ($url) - { - // First check if we already have it locally and, if so, return it - // If the local fetch fails: grab it remotely, store locally and return - $this->grab_local_user ($url) || $this->grab_remote_user($url); + /** + * Get every profile from the given URL + * This function cleans the $this->discovered_actor_profiles array + * so that there is no erroneous data + * + * @param string $url User's url + * @return array of \Profile objects + */ + public function lookup ($url) + { + $this->discovered_actor_profiles = array (); - return $this->discovered_actor_profiles; - } - - private function grab_local_user ($url) - { - if (($actor_profile = Profile::getKV ("profileurl", $url)) != false) { - $this->discovered_actor_profiles[]= $actor_profile; - return true; - } else { // XXX: - // Sometimes it is not true that the user is not locally available, - // mostly when it is a local user and URLs slightly changed - // e.g.: GS instance owner changed from standard urls to pretty urls - // (not sure if this is necessary, but anyway) + return $this->_lookup ($url); + } - // Iff we really are in the same instance - $root_url_len = strlen (common_root_url ()); - if (substr ($url, 0, $root_url_len) == common_root_url ()) { - // Grab the nickname and try to get the user - if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) { - $this->discovered_actor_profiles[]= $actor_profile; - return true; + /** + * Get every profile from the given URL + * This is a recursive function that will accumulate the results on + * $discovered_actor_profiles array + * + * @param string $url User's url + * @return array of \Profile objects + */ + private function _lookup ($url) + { + // First check if we already have it locally and, if so, return it + // If the local fetch fails: grab it remotely, store locally and return + $this->grab_local_user ($url) || $this->grab_remote_user($url); + + return $this->discovered_actor_profiles; + } + + /** + * Get a local user profiles from its URL and joins it on + * $this->discovered_actor_profiles + * + * @param string $url User's url + * @return boolean success state + */ + private function grab_local_user ($url) + { + if (($actor_profile = Profile::getKV ("profileurl", $url)) != false) { + $this->discovered_actor_profiles[]= $actor_profile; + return true; + } else { + /******************************** XXX: ******************************** + * Sometimes it is not true that the user is not locally available, * + * mostly when it is a local user and URLs slightly changed * + * e.g.: GS instance owner changed from standard urls to pretty urls * + * (not sure if this is necessary, but anyway) * + **********************************************************************/ + + // Iff we really are in the same instance + $root_url_len = strlen (common_root_url ()); + if (substr ($url, 0, $root_url_len) == common_root_url ()) { + // Grab the nickname and try to get the user + if (($actor_profile = Profile::getKV ("nickname", substr ($url, $root_url_len))) != false) { + $this->discovered_actor_profiles[]= $actor_profile; + return true; + } + } } - } + return false; } - return false; - } - - private function grab_remote_user ($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 NoResultException ("Invalid Actor URL."); - } - $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); - if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!! - foreach ($res["orderedItems"] as $profile) { - if ($this->_lookup ($profile) == false) { - // XXX: Invalid actor found, not sure how we handle those - } - } - // Go through entire collection - if (!is_null ($res["next"])) { - $this->_lookup ($res["next"]); - } - return true; - } else if ($this->validate_remote_response ($res)) { - $this->discovered_actor_profiles[]= $this->store_profile ($res); - return true; - } - - return false; - } - - private function store_profile ($res) { - $profile = new Profile; - $profile->profileurl = $res["url"]; - $profile->nickname = $res["nickname"]; - $profile->fullname = $res["display_name"]; - $profile->bio = substr ($res["summary"], 0, 1000); - $profile->insert (); - - return $profile; - } - private function validate_remote_response ($res) - { - if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"])) { - return false; + /** + * Get a remote user(s) profile(s) from its URL and joins it on + * $this->discovered_actor_profiles + * + * @param string $url User's url + * @return boolean success state + */ + private function grab_remote_user ($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 NoResultException ("Invalid Actor URL."); + } + $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); + if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!! + foreach ($res["orderedItems"] as $profile) { + if ($this->_lookup ($profile) == false) { + // XXX: Invalid actor found, not sure how we handle those + } + } + // Go through entire collection + if (!is_null ($res["next"])) { + $this->_lookup ($res["next"]); + } + return true; + } else if ($this->validate_remote_response ($res)) { + $this->discovered_actor_profiles[]= $this->store_profile ($res); + return true; + } + + return false; + } + + /** + * Save remote user profile in local instance + * + * @param array $res remote response + * @return \Profile remote Profile object + */ + private function store_profile ($res) { + $profile = new Profile; + $profile->profileurl = $res["url"]; + $profile->nickname = $res["nickname"]; + $profile->fullname = $res["display_name"]; + $profile->bio = substr ($res["summary"], 0, 1000); + $profile->insert (); + + return $profile; + } + + /** + * Validates a remote response in order to determine whether this + * response is a valid profile or not + * + * @param array $res remote response + * @return boolean success state + */ + private function validate_remote_response ($res) { + if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"])) { + return false; + } + + return true; } - - return true; - } }