diff --git a/ActivityPubPlugin.php b/ActivityPubPlugin.php index cc1e3d0..30d7884 100644 --- a/ActivityPubPlugin.php +++ b/ActivityPubPlugin.php @@ -1,4 +1,5 @@ 'showstream'], ['nickname' => Nickname::DISPLAY_FMT], - 'apactorprofile'); - + 'apActorProfile'); + $m->connect (':nickname/liked.json', ['action' => 'apActorLikedCollection'], ['nickname' => Nickname::DISPLAY_FMT]); @@ -77,7 +79,8 @@ class ActivityPubPlugin extends Plugin * @param array $versions * @return boolean true */ - public function onPluginVersion (array &$versions) { + public function onPluginVersion (array &$versions) + { $versions[] = [ 'name' => 'ActivityPub', 'version' => GNUSOCIAL_VERSION, 'author' => 'Daniel Supernault, Diogo Cordeiro', @@ -88,6 +91,74 @@ class ActivityPubPlugin extends Plugin return true; } + + /** + * Make sure necessary tables are filled out. + */ + function onCheckSchema () + { + $schema = Schema::get (); + $schema->ensureTable ('Activitypub_profile', Activitypub_profile::schemaDef()); + return true; + } + + /******************************************************** + * Delivery Events * + ********************************************************/ + + /** + * Having established a remote subscription, send a notification to the + * remote ActivityPub profile's endpoint. + * + * @param Profile $profile subscriber + * @param Profile $other subscribee + * @return hook return value + * @throws Exception + */ + function onEndSubscribe (Profile $profile, Profile $other) + { + if (!$profile->isLocal () || $other->isLocal ()) { + return true; + } + + try { + $other = Activitypub_profile::fromProfile ($other); + } catch (Exception $e) { + return true; + } + + $postman = new Activitypub_postman ($profile, array ($other)); + + $postman->follow (); + + return true; + } + + /** + * Notify remote server on unsubscribe. + * + * @param Profile $profile + * @param Profile $other + * @return hook return value + */ + function onEndUnsubscribe (Profile $profile, Profile $other) + { + if (!$profile->isLocal () || $other->isLocal ()) { + return true; + } + + try { + $other = Activitypub_profile::fromProfile ($other); + } catch (Exception $e) { + return true; + } + + $postman = new Activitypub_postman ($profile, array ($other)); + + $postman->undo_follow (); + + return true; + } } /** @@ -127,7 +198,8 @@ class ActivityPubReturn * @param array $res * @return void */ - static function answer ($res) { + static function answer ($res) + { header ('Content-Type: application/activity+json'); echo json_encode ($res, JSON_UNESCAPED_SLASHES | (isset ($_GET["pretty"]) ? JSON_PRETTY_PRINT : null)); exit; @@ -140,10 +212,11 @@ class ActivityPubReturn * @param int32 $code * @return void */ - static function error ($m, $code = 500) { + static function error ($m, $code = 500) + { http_response_code ($code); header ('Content-Type: application/activity+json'); - $res[] = Activitypub_error::errorMessageToObject ($m); + $res[] = Activitypub_error::error_message_to_array ($m); echo json_encode ($res, JSON_UNESCAPED_SLASHES); exit; } diff --git a/actions/apactorfollowers.php b/actions/apactorfollowers.php index ab1f8c8..8e32a10 100644 --- a/actions/apactorfollowers.php +++ b/actions/apactorfollowers.php @@ -46,7 +46,8 @@ class apActorFollowersAction extends ManagedAction * * @return void */ - protected function handle () { + protected function handle () + { $nickname = $this->trimmed ('nickname'); try { $user = User::getByNickname ($nickname); @@ -71,8 +72,7 @@ class apActorFollowersAction extends ManagedAction $since = ($page - 1) * PROFILES_PER_MINILIST; $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; $sub = $profile->getSubscribers ($since, $limit); - } - catch(NoResultException $e) { + } catch (NoResultException $e) { ActivityPubReturn::error ('This user has no followers.'); } diff --git a/actions/apactorfollowing.php b/actions/apactorfollowing.php index b0b3884..f5bc75f 100644 --- a/actions/apactorfollowing.php +++ b/actions/apactorfollowing.php @@ -46,7 +46,8 @@ class apActorFollowingAction extends ManagedAction * * @return void */ - protected function handle () { + protected function handle () + { $nickname = $this->trimmed ('nickname'); try { $user = User::getByNickname ($nickname); @@ -70,7 +71,7 @@ class apActorFollowingAction extends ManagedAction try { $since = ($page - 1) * PROFILES_PER_MINILIST; $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; - $sub = $profile->getSubscribed($since, $limit); + $sub = $profile->getSubscribed ($since, $limit); } catch (NoResultException $e) { ActivityPubReturn::error ('This user is not following anyone.'); } diff --git a/actions/apactorinbox.php b/actions/apactorinbox.php index d0a90e5..b662368 100644 --- a/actions/apactorinbox.php +++ b/actions/apactorinbox.php @@ -76,8 +76,8 @@ class apActorInboxAction extends ManagedAction // Get valid Actor object try { - require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "discovery.php"; - $actor_profile = new Activitypub_Discovery; + require_once dirname (__DIR__) . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php"; + $actor_profile = new Activitypub_explorer; $actor_profile = $actor_profile->lookup ($data->actor); $actor_profile = $actor_profile[0]; } catch (Exception $e) { diff --git a/actions/apactorlikedcollection.php b/actions/apactorlikedcollection.php index c3cf045..c6b128f 100644 --- a/actions/apactorlikedcollection.php +++ b/actions/apactorlikedcollection.php @@ -46,7 +46,8 @@ class apActorLikedCollectionAction extends ManagedAction * * @return void */ - protected function handle () { + protected function handle () + { $nickname = $this->trimmed ('nickname'); try { $user = User::getByNickname ($nickname); @@ -96,13 +97,14 @@ class apActorLikedCollectionAction extends ManagedAction * Take a fave object and turns it in a pretty array to be used * as a plugin answer * - * @param \Fave $fave_object + * @param Fave $fave_object * @return array pretty array representating a Fave */ - protected function pretty_fave ($fave_object) { + protected function pretty_fave ($fave_object) + { $res = array("uri" => $fave_object->uri, "created" => $fave_object->created, - "object" => Activitypub_notice::noticeToObject (Notice::getByID ($fave_object->notice_id))); + "object" => Activitypub_notice::notice_to_array (Notice::getByID ($fave_object->notice_id))); return $res; } @@ -114,10 +116,11 @@ class apActorLikedCollectionAction extends ManagedAction * @param int32 $limit * @param int32 $since_id * @param int32 $max_id - * @return \Fave fetchable fave collection + * @return Fave fetchable fave collection */ private static function fetch_faves ($user_id, $limit = 40, $since_id = null, - $max_id = null) { + $max_id = null) + { $fav = new Fave (); $fav->user_id = $user_id; diff --git a/actions/apactorprofile.php b/actions/apactorprofile.php index 4216909..f026728 100644 --- a/actions/apactorprofile.php +++ b/actions/apactorprofile.php @@ -47,7 +47,8 @@ class apActorProfileAction extends ManagedAction * * @return void */ - protected function handle() { + protected function handle() + { $nickname = $this->trimmed ('nickname'); try { $user = User::getByNickname ($nickname); @@ -57,7 +58,7 @@ class apActorProfileAction extends ManagedAction ActivityPubReturn::error ('Invalid username.', 404); } - $res = Activitypub_profile::profileToObject ($profile); + $res = Activitypub_profile::profile_to_array ($profile); ActivityPubReturn::answer ($res); } diff --git a/actions/apsharedinbox.php b/actions/apsharedinbox.php index b3684db..2f8321e 100644 --- a/actions/apsharedinbox.php +++ b/actions/apsharedinbox.php @@ -1,10 +1,5 @@ actor)) { ActivityPubReturn::error ("Actor was not specified."); } - if (!isset($data->to)) { - ActivityPubReturn::error ("To was not specified."); - } if (!isset($data->object)) { ActivityPubReturn::error ("Object was not specified."); } - $discovery = new Activitypub_Discovery; + $discovery = new Activitypub_explorer; // Get valid Actor object try { @@ -84,37 +76,42 @@ class apSharedInboxAction extends ManagedAction ActivityPubReturn::error ("Invalid Actor.", 404); } + unset ($discovery); + // Public To: $public_to = array ("https://www.w3.org/ns/activitystreams#Public", "Public", "as:Public"); - $to_profiles = array (); - // Generate To objects - if (is_array ($data->to)) { - // Remove duplicates from To actors set - array_unique ($data->to); - foreach ($data->to as $to_url) { - try { - $to_profiles = array_merge ($to_profiles, $discovery->lookup ($to_url)); - } catch (Exception $e) { - // XXX: Invalid actor found, not sure how we handle those - } - } - } else if (empty ($data->to) || in_array ($data->to, $public_to)) { - // No need to do anything else at this point, let's just break out the if - } else { - try { - $to_profiles[]= $discovery->lookup ($data->to); - } catch (Exception $e) { - ActivityPubReturn::error ("Invalid Actor.", 404); - } - } - unset ($discovery); - // Process request switch ($data->type) { case "Create": + if (!isset($data->to)) { + ActivityPubReturn::error ("To was not specified."); + } + $discovery = new Activitypub_Discovery; + $to_profiles = array (); + // Generate To objects + if (is_array ($data->to)) { + // Remove duplicates from To actors set + array_unique ($data->to); + foreach ($data->to as $to_url) { + try { + $to_profiles = array_merge ($to_profiles, $discovery->lookup ($to_url)); + } catch (Exception $e) { + // XXX: Invalid actor found, not sure how we handle those + } + } + } else if (empty ($data->to) || in_array ($data->to, $public_to)) { + // No need to do anything else at this point, let's just break out the if + } else { + try { + $to_profiles[]= $discovery->lookup ($data->to); + } catch (Exception $e) { + ActivityPubReturn::error ("Invalid Actor.", 404); + } + } + unset ($discovery); require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Create.php"; break; case "Follow": @@ -126,6 +123,9 @@ class apSharedInboxAction extends ManagedAction case "Announce": require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Announce.php"; break; + case "Undo": + require_once __DIR__ . DIRECTORY_SEPARATOR . "inbox" . DIRECTORY_SEPARATOR . "Undo.php"; + break; default: ActivityPubReturn::error ("Invalid type value."); } diff --git a/actions/inbox/Announce.php b/actions/inbox/Announce.php index 461b463..d1470c2 100644 --- a/actions/inbox/Announce.php +++ b/actions/inbox/Announce.php @@ -30,8 +30,8 @@ if (!defined ('GNUSOCIAL')) { } try { - Notice::getByUri ($data->object)->repeat ($actor_profile, "api"); + Notice::getByUri ($data->object)->repeat ($actor_profile, "ActivityPub"); ActivityPubReturn::answer ("Notice repeated successfully."); -} catch(Exception $ex) { - ActivityPubReturn::error ($ex->getMessage (), 403); +} catch (Exception $e) { + ActivityPubReturn::error ($e->getMessage (), 403); } diff --git a/actions/inbox/Create.php b/actions/inbox/Create.php index f121300..4bffcaf 100644 --- a/actions/inbox/Create.php +++ b/actions/inbox/Create.php @@ -32,7 +32,7 @@ if (!defined ('GNUSOCIAL')) { $valid_object_types = array ("Note"); // Validate data -if (! (isset($data->object->type) && in_array ($data->object->type, $valid_object_types))) { +if (!(isset ($data->object->type) && in_array ($data->object->type, $valid_object_types))) { ActivityPubReturn::error ("Invalid Object type."); } if (!isset ($data->object->content)) { @@ -51,17 +51,17 @@ $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->replyToID = $reply_to->getUri (); $act->context->replyToUrl = $data->object->reply_to; } else { $reply_to = null; } -$act->context->attention = common_get_attentions($content, $actor_profile, $reply_to); +$act->context->attention = common_get_attentions ($content, $actor_profile, $reply_to); foreach ($to_profiles as $to) { - $act->context->attention[$to->getUri()] = "http://activitystrea.ms/schema/1.0/person"; + $act->context->attention[$to->getUri ()] = "http://activitystrea.ms/schema/1.0/person"; } // Reject notice if it is too long (without the HTML) @@ -70,7 +70,7 @@ if (Notice::contentTooLong ($content)) { ActivityPubReturn::error ("That's too long. Maximum notice size is %d character."); } -$options = array ('source' => 'web', 'uri' => $data->id); +$options = array ('source' => 'ActivityPub', 'uri' => $data->id); // $options gets filled with possible scoping settings ToSelector::fillActivity ($this, $act, $options); @@ -86,7 +86,7 @@ try { "id" => $data->id, "type" => "Create", "actor" => $data->actor, - "object" => Activitypub_notice::noticeToObject (Notice::saveActivity ($act, $actor_profile, $options))); + "object" => Activitypub_notice::notice_to_array (Notice::saveActivity ($act, $actor_profile, $options))); ActivityPubReturn::answer ($res); } catch (Exception $e) { ActivityPubReturn::error ($e->getMessage ()); diff --git a/actions/inbox/Delete.php b/actions/inbox/Delete.php index 32f97c1..44a3936 100644 --- a/actions/inbox/Delete.php +++ b/actions/inbox/Delete.php @@ -31,7 +31,11 @@ if (!defined ('GNUSOCIAL')) { try { Activitypub_notice::getByUri ($data->object)->deleteAs ($actor_profile); - ActivityPubReturn::answer ("Notice deleted successfully."); -} catch(Exception $ex) { - ActivityPubReturn::error ($ex->getMessage (), 403); + $res = array ("@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Delete", + "actor" => $data->actor, + "object" => $data->object); + ActivityPubReturn::answer ($res); +} catch (Exception $e) { + ActivityPubReturn::error ($e->getMessage (), 403); } diff --git a/actions/inbox/Follow.php b/actions/inbox/Follow.php index 9fdd81f..1ff819f 100644 --- a/actions/inbox/Follow.php +++ b/actions/inbox/Follow.php @@ -29,10 +29,15 @@ if (!defined ('GNUSOCIAL')) { exit (1); } +// Validate Object +if (!is_string ($data->object)) { + ActivityPubReturn::error ("Invalid Object object, URL expected."); +} + // Get valid Object profile try { - $object_profile = new Activitypub_Discovery; - $object_profile = $object_profile->lookup ($data->object); + $object_profile = new Activitypub_explorer; + $object_profile = $object_profile->lookup ($data->object)[0]; } catch(Exception $e) { ActivityPubReturn::error ("Invalid Object Actor URL.", 404); } @@ -40,10 +45,14 @@ try { try { if (!Subscription::exists ($actor_profile, $object_profile)) { Subscription::start ($actor_profile, $object_profile); - ActivityPubReturn::answer ("You are now following this person."); + $res = array ("@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => $data->actor, + "object" => $data->object); + ActivityPubReturn::answer ($res); } else { ActivityPubReturn::error ("Already following.", 409); } -} catch (Exception $ex) { +} catch (Exception $e) { ActivityPubReturn::error ("Invalid Object Actor URL.", 404); } diff --git a/actions/inbox/Like.php b/actions/inbox/Like.php index 2ae552d..7c64e81 100644 --- a/actions/inbox/Like.php +++ b/actions/inbox/Like.php @@ -31,7 +31,11 @@ if (!defined ('GNUSOCIAL')) { try { Fave::addNew ($actor_profile, Notice::getByUri ($data->object)); - ActivityPubReturn::answer ("Notice favorited successfully."); -} catch (Exception $ex) { - ActivityPubReturn::error ($ex->getMessage (), 403); + $res = array ("@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Like", + "actor" => $data->actor, + "object" => $data->object); + ActivityPubReturn::answer ($res); +} catch (Exception $e) { + ActivityPubReturn::error ($e->getMessage (), 403); } diff --git a/actions/inbox/Undo.php b/actions/inbox/Undo.php index c976d19..86731e0 100644 --- a/actions/inbox/Undo.php +++ b/actions/inbox/Undo.php @@ -1,7 +1,4 @@ object->object)); + Fave::removeEntry ($actor_profile, Notice::getByUri ($data->object->object)); ActivityPubReturn::answer ("Notice disfavorited successfully."); } catch (Exception $e) { - ActivityPubReturn::error ($e->getMessage(), 403); + ActivityPubReturn::error ($e->getMessage (), 403); } break; case "Follow": @@ -58,8 +55,8 @@ case "Follow": } // Get valid Object profile try { - $object_profile = new Activitypub_Discovery; - $object_profile = $object_profile->lookup ($data->object->object); + $object_profile = new Activitypub_explorer; + $object_profile = $object_profile->lookup ($data->object->object)[0]; } catch (Exception $e) { ActivityPubReturn::error ("Invalid Object Actor URL.", 404); } @@ -71,7 +68,7 @@ case "Follow": } else { ActivityPubReturn::error ("You are not following this person already.", 409); } - } catch (Exception $ex) { + } catch (Exception $e) { ActivityPubReturn::error ("Invalid Object Actor URL.", 404); } break; diff --git a/classes/Activitypub_attachment.php b/classes/Activitypub_attachment.php index 61f617b..691277b 100644 --- a/classes/Activitypub_attachment.php +++ b/classes/Activitypub_attachment.php @@ -41,10 +41,11 @@ class Activitypub_attachment extends Managed_DataObject /** * Generates a pretty array from an Attachment object * - * @param \Attachment $attachment + * @param Attachment $attachment * @return pretty array to be used in a response */ - public static function attachmentToObject ($attachment) { + public static function attachment_to_array ($attachment) + { $res = [ '@context' => [ "https://www.w3.org/ns/activitystreams", diff --git a/classes/Activitypub_error.php b/classes/Activitypub_error.php index 69e74bc..b14299b 100644 --- a/classes/Activitypub_error.php +++ b/classes/Activitypub_error.php @@ -44,7 +44,8 @@ class Activitypub_error extends Managed_DataObject * @param string $m * @return pretty array to be used in a response */ - public static function errorMessageToObject ($m) { + public static function error_message_to_array ($m) + { $res = [ 'error'=> $m ]; diff --git a/classes/Activitypub_notice.php b/classes/Activitypub_notice.php index 1fc8daf..8868dd9 100644 --- a/classes/Activitypub_notice.php +++ b/classes/Activitypub_notice.php @@ -42,19 +42,20 @@ class Activitypub_notice extends Managed_DataObject /** * Generates a pretty notice from a Notice object * - * @param \Notice $notice + * @param Notice $notice * @return pretty array to be used in a response */ - public static function noticeToObject ($notice) { + public static function notice_to_array ($notice) + { $attachments = array (); foreach($notice->attachments () as $attachment) { - $attachments[] = Activitypub_attachment::attachmentToObject ($attachment); + $attachments[] = Activitypub_attachment::attachment_to_array ($attachment); } $tags = array (); foreach($notice->getTags()as $tag) { if ($tag != "") { // Hacky workaround to avoid stupid outputs - $tags[] = Activitypub_tag::tagNameToObject ($tag); + $tags[] = Activitypub_tag::tag_to_array ($tag); } } diff --git a/classes/Activitypub_profile.php b/classes/Activitypub_profile.php index 3ec1344..3c5f93e 100644 --- a/classes/Activitypub_profile.php +++ b/classes/Activitypub_profile.php @@ -37,15 +37,47 @@ if (!defined ('GNUSOCIAL')) { * @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/ */ -class Activitypub_profile extends Managed_DataObject +class Activitypub_profile extends Profile { + public $__table = 'Activitypub_profile'; + + protected $_profile = null; + + /** + * Return table definition for Schema setup and DB_DataObject usage. + * + * @return array array of column definitions + */ + static function schemaDef () + { + return array ( + 'fields' => array ( + 'uri' => array ('type' => 'varchar', 'length' => 191, 'not null' => true), + 'profile_id' => array ('type' => 'integer'), + 'inboxuri' => array ('type' => 'varchar', 'length' => 191), + 'sharedInboxuri' => array ('type' => 'varchar', 'length' => 191), + 'created' => array ('type' => 'datetime', 'not null' => true), + 'modified' => array ('type' => 'datetime', 'not null' => true), + ), + 'primary key' => array ('uri'), + 'unique keys' => array ( + 'Activitypub_profile_profile_id_key' => array ('profile_id'), + 'Activitypub_profile_inboxuri_key' => array ('inboxuri'), + ), + 'foreign keys' => array ( + 'Activitypub_profile_profile_id_fkey' => array ('profile', array ('profile_id' => 'id')), + ), + ); + } + /** * Generates a pretty profile from a Profile object * - * @param \Profile $profile + * @param Profile $profile * @return pretty array to be used in a response */ - public static function profileToObject($profile) { + public static function profile_to_array ($profile) + { $url = $profile->getURL (); $res = [ '@context' => [ @@ -68,7 +100,7 @@ class Activitypub_profile extends Managed_DataObject 'following_count' => $profile->subscriptionCount (), 'liked' => "{$url}/liked.json", 'liked_count' => Fave::countByProfile ($profile), - 'summary' => $profile->getDescription (), + 'summary' => ($desc = $profile->getDescription ()) == null ? "" : $desc, 'url' => $profile->getURL (), 'avatar' => [ 'type' => 'Image', @@ -79,4 +111,92 @@ class Activitypub_profile extends Managed_DataObject ]; return $res; } + + /** + * Insert the current objects variables into the database + * + * @access public + * @throws ServerException + */ + public function doInsert () + { + $profile = new Profile (); + + $profile->created = $this->created = $this->modified = common_sql_now (); + + $fields = array ( + 'uri' => 'profileurl', + 'nickname' => 'nickname', + 'fullname' => 'fullname', + 'bio' => 'bio' + ); + + foreach ($fields as $af => $pf) { + $profile->$pf = $this->$af; + } + + $this->profile_id = $profile->insert (); + if ($this->profile_id === false) { + $profile->query ('ROLLBACK'); + throw new ServerException ('Profile insertion failed.'); + } + + $ok = $this->insert (); + + if ($ok === false) { + $profile->query ('ROLLBACK'); + throw new ServerException ('Cannot save ActivityPub profile.'); + } + } + + /** + * Fetch the locally stored profile for this Activitypub_profile + * @return Profile + * @throws NoProfileException if it was not found + */ + public function localProfile () + { + $profile = Profile::getKV ('id', $this->profile_id); + if (!$profile instanceof Profile) { + throw new NoProfileException ($this->profile_id); + } + return $profile; + } + + /** + * Generates an Activitypub_profile from a Profile + * + * @param Profile $profile + * @return Activitypub_profile + * @throws Exception if no Activitypub_profile exists for given Profile + */ + static function fromProfile (Profile $profile) + { + $profile_id = $profile->getID (); + + $aprofile = Activitypub_profile::getKV ('profile_id', $profile_id); + if (!$aprofile instanceof Activitypub_profile) { + throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id); + } + + foreach ($profile as $key => $value) { + $aprofile->$key = $value; + } + + return $aprofile; + } + + /** + * Returns sharedInbox if possible, inbox otherwise + * + * @return string Inbox URL + */ + public function getInbox () + { + if (is_null ($this->sharedInboxuri)) { + return $this->inboxuri; + } + + return $this->sharedInboxuri; + } } diff --git a/classes/Activitypub_tag.php b/classes/Activitypub_tag.php index e72a73c..9f27a96 100644 --- a/classes/Activitypub_tag.php +++ b/classes/Activitypub_tag.php @@ -41,10 +41,11 @@ class Activitypub_tag extends Managed_DataObject /** * Generates a pretty tag from a Tag object * - * @param \Tag $tag + * @param Tag $tag * @return pretty array to be used in a response */ - public static function tagNameToObject ($tag) { + public static function tag_to_array ($tag) + { $res = [ '@context' => [ "https://www.w3.org/ns/activitystreams", diff --git a/utils/discovery.php b/utils/explorer.php similarity index 73% rename from utils/discovery.php rename to utils/explorer.php index 4fe5c25..cbf9d38 100644 --- a/utils/discovery.php +++ b/utils/explorer.php @@ -1,7 +1,4 @@ grab_local_user ($url) || $this->grab_remote_user($url); + if (! ($this->grab_local_user ($url) || $this->grab_remote_user ($url))) { + throw new Exception ("User not found"); + } + return $this->discovered_actor_profiles; } @@ -84,7 +84,7 @@ class Activitypub_Discovery */ private function grab_local_user ($url) { - if (($actor_profile = Profile::getKV ("profileurl", $url)) != false) { + if (($actor_profile = self::get_profile_by_url ($url)) != false) { $this->discovered_actor_profiles[]= $actor_profile; return true; } else { @@ -115,14 +115,15 @@ class Activitypub_Discovery * @param string $url User's url * @return boolean success state */ - private function grab_remote_user ($url) { + 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."); + if (!$response->isOk ()) { + throw new Exception ("Invalid Actor URL."); } $res = json_decode ($response->getBody (), JSON_UNESCAPED_SLASHES); if (isset ($res["orderedItems"])) { // It's a potential collection of actors!!! @@ -148,17 +149,21 @@ class Activitypub_Discovery * Save remote user profile in local instance * * @param array $res remote response - * @return \Profile remote Profile object + * @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 (); + private function store_profile ($res) + { + $aprofile = new Activitypub_profile; + $aprofile->uri = $res["url"]; + $aprofile->nickname = $res["nickname"]; + $aprofile->fullname = $res["display_name"]; + $aprofile->bio = substr ($res["summary"], 0, 1000); + $aprofile->inboxuri = $res["inbox"]; + $aprofile->sharedInboxuri = $res["sharedInbox"]; - return $profile; + $aprofile->doInsert (); + + return $aprofile->localProfile (); } /** @@ -168,11 +173,37 @@ class Activitypub_Discovery * @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"])) { + private function validate_remote_response ($res) + { + if (!isset ($res["url"], $res["nickname"], $res["display_name"], $res["summary"], $res["inbox"], $res["sharedInbox"])) { return false; } return true; } + + /** + * Get a profile from it's profileurl + * Unfortunately GNU Social cache is not truly reliable when handling + * potential ActivityPub remote profiles, as so it is important to use + * this hacky workaround (at least for now) + * + * @param string $v URL + * @return boolean|Profile false if fails | Profile object if successful + */ + static function get_profile_by_url ($v) + { + $i = Managed_DataObject::getcached(Profile, "profileurl", $v); + if (empty ($i)) { // false = cache miss + $i = new Profile; + $result = $i->get ("profileurl", $v); + if ($result) { + // Hit! + $i->encache(); + } else { + return false; + } + } + return $i; + } } diff --git a/utils/postman.php b/utils/postman.php new file mode 100644 index 0000000..4fa69fb --- /dev/null +++ b/utils/postman.php @@ -0,0 +1,91 @@ +. + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @author Daniel Supernault + * @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://www.gnu.org/software/social/ + */ +if (!defined ('GNUSOCIAL')) { + exit (1); +} + +/** + * @category Plugin + * @package GNUsocial + * @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/ + */ +class Activitypub_postman +{ + private $actor; + private $to = array (); + private $client; + private $headers; + + /** + * Create a postman to deliver something to someone + * + * @param Activitypub_profile $to array of destinataries + */ + public function __construct ($from, $to) + { + $this->actor = $from; + $this->to = $to; + $this->headers = array(); + $this->headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; + $this->headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; + } + + /** + * Send a follow notification to remote instance + */ + public function follow () + { + $this->client = new HTTPClient (); + $data = array ("@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Follow", + "actor" => $this->actor->getUrl (), + "object" => $this->to[0]->getUrl ()); + $this->client->setBody (json_encode ($data)); + $response = $this->client->post ($this->to[0]->getInbox (), $this->headers); + } + + /** + * Send a Undo Follow notification to remote instance + */ + public function undo_follow () + { + $this->client = new HTTPClient (); + $data = array ("@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Undo", + "actor" => $this->actor->getUrl (), + "object" => array ( + "type" => "Follow", + "object" => $this->to[0]->getUrl () + ) + ); + $this->client->setBody (json_encode ($data)); + $response = $this->client->post ($this->to[0]->getInbox (), $this->headers); + } +}