From bfdab74775055f316e6790fed74950c43a7887ff Mon Sep 17 00:00:00 2001 From: "Theodore R. Smith" Date: Mon, 12 Nov 2018 20:42:57 -0600 Subject: [PATCH 1/5] Added support for publishing newsfeed posts via the ActivityPub spec. --- Controllers/api/v1/newsfeed.php | 27 +++- Helpers/NewsfeedActivityActivityPubClient.php | 126 ++++++++++++++++++ Interfaces/ActivityPubClient.php | 26 ++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 Helpers/NewsfeedActivityActivityPubClient.php create mode 100644 Interfaces/ActivityPubClient.php diff --git a/Controllers/api/v1/newsfeed.php b/Controllers/api/v1/newsfeed.php index ac0cd3e8ee..8af39c33cb 100644 --- a/Controllers/api/v1/newsfeed.php +++ b/Controllers/api/v1/newsfeed.php @@ -13,15 +13,28 @@ use Minds\Core\Security; use Minds\Entities; use Minds\Entities\Activity; +use Minds\Entities\User; use Minds\Helpers; use Minds\Entities\Factory as EntitiesFactory; use Minds\Helpers\Counters; +use Minds\Helpers\NewsfeedActivityActivityPubClient; use Minds\Interfaces; use Minds\Interfaces\Flaggable; use Minds\Core\Di\Di; +use Minds\Interfaces\ActivityPubClient; class newsfeed implements Interfaces\Api { + /** @var ActivityPubClient */ + protected $pubSubClient; + + public function __construct(ActivityPubClient $pubSubClient = null) + { + $this->pubSubClient = $pubSubClient ?? new NewsfeedActivityActivityPubClient(); + // See https://project.hubzilla.org for how to set your own ActivityPub server. + $this->pubSubClient->setActivityPubServer('https://project.hubzilla.org'); + } + /** * Returns the newsfeed * @param array $pages @@ -420,6 +433,18 @@ public function post($pages) Helpers\Wallet::createTransaction($embeded->owner_guid, 5, $activity->guid, 'Remind'); } + // Post via ActivityPub: + /** @var User $user */ + $user = Core\Session::getLoggedinUser(); + + $this->pubSubClient->setActor($user->name, "https://pub.minds.com/{$user->username}"); + + $this->pubSubClient->postArticle( + $embeded->getTitle(), + $embeded->description, + "https://pub.minds.com/{$user->username}/friends" + ); + // Follow activity (new Core\Notification\PostSubscriptions\Manager()) ->setEntityGuid($activity->guid) @@ -746,7 +771,7 @@ public function delete($pages) if (!$activity->canEdit()) { return Factory::response(array('status' => 'error', 'message' => 'you don\'t have permission')); } - /** @var Entities\User $owner */ + /** @var User $owner */ $owner = $activity->getOwnerEntity(); if ( diff --git a/Helpers/NewsfeedActivityActivityPubClient.php b/Helpers/NewsfeedActivityActivityPubClient.php new file mode 100644 index 0000000000..75a8082b3a --- /dev/null +++ b/Helpers/NewsfeedActivityActivityPubClient.php @@ -0,0 +1,126 @@ +client = $client ?? new Guzzle_Client(); + } + + public function setActivityPubServer(string $serverURL) + { + $this->activityPubURI = $serverURL; + } + + public function setActor(string $actorName, string $actorURI) + { + $this->actorName = $actorName; + $this->actorURI = $actorURI; + } + + private function assertPubSubURI() + { + if (!$this->activityPubURI) { + throw new LogicException('The PubSub URI has not been specified.'); + } + } + + private function assertActor() + { + if (!$this->actorURI) { + throw new LogicException('The PubSub actor has not been specified.'); + } + } + + protected function validate() + { + $this->assertPubSubURI(); + $this->assertActor(); + } + + /** + * See: https://w3c.github.io/activitypub/#create-activity-outbox + * + * @param string $title + * @param string $body + * @param string[] $to + * @param string[]|null $cc + * @return int The HTTP Status code of the request. + */ + public function postArticle(string $title, string $body, array $to, ?array $cc = null) + { + $this->validate(); + + $params = [ + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + '@language' => 'en-GB' + ], + 'id' => 'https://rhiaro.co.uk/2016/05/minimal-activitypub', + 'type' => 'Article', + 'name' => $title, + 'content' => $body, + 'attributedTo' => $this->actorURI, + 'to' => $to, + 'cc' => $cc, + ]; + + $this->response = $this->client->post($this->activityPubURI, [ + 'Content-Type' => 'application/json', + 'json' => $params, + ]); + + return $this->response->getStatusCode(); + } + + /** + * See: https://w3c.github.io/activitypub/#create-activity-outbox + */ + public function like(string $refObjectURI, array $to, ?string $summary = null, ?array $cc = null) + { + $this->validate(); + + $params = [ + '@context' => [ + 'https://www.w3.org/ns/activitystreams', + '@language' => 'en-GB' + ], + 'id' => 'https://rhiaro.co.uk/2016/05/minimal-activitypub', + 'type' => 'Like', + 'actor' => $this->actorURI, + 'summary' => $summary ?? "{$this->actorName} liked the post", + 'object' => $refObjectURI, + 'to' => $to, + 'cc' => $cc, + ]; + + $this->response = $this->client->post($this->activityPubURI, [ + 'Content-Type' => 'application/json', + 'json' => $params, + ]); + + return $this->response->getStatusCode(); + } +} diff --git a/Interfaces/ActivityPubClient.php b/Interfaces/ActivityPubClient.php new file mode 100644 index 0000000000..7691590c86 --- /dev/null +++ b/Interfaces/ActivityPubClient.php @@ -0,0 +1,26 @@ + Date: Wed, 14 Nov 2018 21:49:42 -0600 Subject: [PATCH 2/5] Refactored the ActivityPub client along code review suggestions. --- Controllers/api/v1/newsfeed.php | 8 ++--- Helpers/NewsfeedActivityActivityPubClient.php | 34 ++++++++++++++----- Interfaces/ActivityPubClient.php | 7 ++-- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Controllers/api/v1/newsfeed.php b/Controllers/api/v1/newsfeed.php index 8af39c33cb..28ae774151 100644 --- a/Controllers/api/v1/newsfeed.php +++ b/Controllers/api/v1/newsfeed.php @@ -437,13 +437,9 @@ public function post($pages) /** @var User $user */ $user = Core\Session::getLoggedinUser(); - $this->pubSubClient->setActor($user->name, "https://pub.minds.com/{$user->username}"); + $this->pubSubClient->setActor($user->name, "https://www.minds.com/{$user->username}"); - $this->pubSubClient->postArticle( - $embeded->getTitle(), - $embeded->description, - "https://pub.minds.com/{$user->username}/friends" - ); + $this->pubSubClient->postArticle($embeded); // Follow activity (new Core\Notification\PostSubscriptions\Manager()) diff --git a/Helpers/NewsfeedActivityActivityPubClient.php b/Helpers/NewsfeedActivityActivityPubClient.php index 75a8082b3a..3f8fe5fb9a 100644 --- a/Helpers/NewsfeedActivityActivityPubClient.php +++ b/Helpers/NewsfeedActivityActivityPubClient.php @@ -5,10 +5,16 @@ use GuzzleHttp\Client as Guzzle_Client; use GuzzleHttp\Psr7\Response; use LogicException; +use Minds\Core\Blogs\Blog; +use Minds\Core\Config; +use Minds\Core\Di\Di; use Minds\Interfaces\ActivityPubClient; class NewsfeedActivityActivityPubClient implements ActivityPubClient { + /** @var Config */ + protected $config; + /** @var Guzzle_Client */ protected $client; @@ -26,6 +32,7 @@ class NewsfeedActivityActivityPubClient implements ActivityPubClient public function __construct(Guzzle_Client $client = null) { + $this->config = Di::_()->get('Config'); $this->client = $client ?? new Guzzle_Client(); } @@ -65,23 +72,27 @@ protected function validate() * * @param string $title * @param string $body - * @param string[] $to + * @param string $to * @param string[]|null $cc * @return int The HTTP Status code of the request. */ - public function postArticle(string $title, string $body, array $to, ?array $cc = null) + public function postArticle(Blog $article, ?string $to, ?array $cc = null) { $this->validate(); + // Default the "To" to the user's subscribers, although it could be any other user, + // even a user on another ActivityPub site. + $to = $to ?? $this->actorURI . '/subscribers'; + $params = [ '@context' => [ 'https://www.w3.org/ns/activitystreams', - '@language' => 'en-GB' + '@language' => 'en-US' ], - 'id' => 'https://rhiaro.co.uk/2016/05/minimal-activitypub', + 'id' => $this->config->site_url . "newsfeed/{$article->guid}", 'type' => 'Article', - 'name' => $title, - 'content' => $body, + 'name' => $article->getTitle(), + 'content' => $article->getBody(), 'attributedTo' => $this->actorURI, 'to' => $to, 'cc' => $cc, @@ -98,16 +109,21 @@ public function postArticle(string $title, string $body, array $to, ?array $cc = /** * See: https://w3c.github.io/activitypub/#create-activity-outbox */ - public function like(string $refObjectURI, array $to, ?string $summary = null, ?array $cc = null) + public function like(string $refObjectURI, ?string $to, ?string $summary = null, ?array $cc = null) { $this->validate(); + // Default the "To" to the user's subscribers, although it could be any other user, + // even a user on another ActivityPub site. + $to = $to ?? $this->actorURI . '/subscribers'; + $params = [ '@context' => [ 'https://www.w3.org/ns/activitystreams', - '@language' => 'en-GB' + '@language' => 'en-US' ], - 'id' => 'https://rhiaro.co.uk/2016/05/minimal-activitypub', + // Use the item's GUID as the basis for the unique ActivityPub ID. + 'id' => $this->config->site_url . $this->actorURI . '/activitypub/' . $refObjectURI, 'type' => 'Like', 'actor' => $this->actorURI, 'summary' => $summary ?? "{$this->actorName} liked the post", diff --git a/Interfaces/ActivityPubClient.php b/Interfaces/ActivityPubClient.php index 7691590c86..82cb24a5e9 100644 --- a/Interfaces/ActivityPubClient.php +++ b/Interfaces/ActivityPubClient.php @@ -1,6 +1,7 @@ Date: Tue, 27 Nov 2018 20:36:30 -0600 Subject: [PATCH 3/5] Fixed a typo in a file name. --- Controllers/api/v1/newsfeed.php | 4 ++-- ...ityActivityPubClient.php => NewsfeedActivityPubClient.php} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename Helpers/{NewsfeedActivityActivityPubClient.php => NewsfeedActivityPubClient.php} (98%) diff --git a/Controllers/api/v1/newsfeed.php b/Controllers/api/v1/newsfeed.php index 28ae774151..b6d19e727a 100644 --- a/Controllers/api/v1/newsfeed.php +++ b/Controllers/api/v1/newsfeed.php @@ -17,7 +17,7 @@ use Minds\Helpers; use Minds\Entities\Factory as EntitiesFactory; use Minds\Helpers\Counters; -use Minds\Helpers\NewsfeedActivityActivityPubClient; +use Minds\Helpers\NewsfeedActivityPubClient; use Minds\Interfaces; use Minds\Interfaces\Flaggable; use Minds\Core\Di\Di; @@ -30,7 +30,7 @@ class newsfeed implements Interfaces\Api public function __construct(ActivityPubClient $pubSubClient = null) { - $this->pubSubClient = $pubSubClient ?? new NewsfeedActivityActivityPubClient(); + $this->pubSubClient = $pubSubClient ?? new NewsfeedActivityPubClient(); // See https://project.hubzilla.org for how to set your own ActivityPub server. $this->pubSubClient->setActivityPubServer('https://project.hubzilla.org'); } diff --git a/Helpers/NewsfeedActivityActivityPubClient.php b/Helpers/NewsfeedActivityPubClient.php similarity index 98% rename from Helpers/NewsfeedActivityActivityPubClient.php rename to Helpers/NewsfeedActivityPubClient.php index 3f8fe5fb9a..900cab8c1a 100644 --- a/Helpers/NewsfeedActivityActivityPubClient.php +++ b/Helpers/NewsfeedActivityPubClient.php @@ -10,7 +10,7 @@ use Minds\Core\Di\Di; use Minds\Interfaces\ActivityPubClient; -class NewsfeedActivityActivityPubClient implements ActivityPubClient +class NewsfeedActivityPubClient implements ActivityPubClient { /** @var Config */ protected $config; From 2a8a33e7c7344f200f123c8b08021b1169f51504 Mon Sep 17 00:00:00 2001 From: "Theodore R. Smith" Date: Tue, 27 Nov 2018 20:36:41 -0600 Subject: [PATCH 4/5] Added spec tests. --- .../Helpers/NewsfeedActivityPubClientSpec.php | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Spec/Helpers/NewsfeedActivityPubClientSpec.php diff --git a/Spec/Helpers/NewsfeedActivityPubClientSpec.php b/Spec/Helpers/NewsfeedActivityPubClientSpec.php new file mode 100644 index 0000000000..b764b26aa8 --- /dev/null +++ b/Spec/Helpers/NewsfeedActivityPubClientSpec.php @@ -0,0 +1,95 @@ +setTitle('Test Blog'); + $blog->setBody('Test body.'); + + return $blog; + } + + protected function buildGuzzleMock(): Guzzle_Client + { + $guzzleMock = new class extends Guzzle_Client { + public function post($uri, $params): Response + { + return new class extends Response { + public function getStatusCode() + { + return 200; + } + }; + } + }; + + return $guzzleMock; + } + + public function it_is_initializable() + { + $this->shouldHaveType('Minds\Helpers\NewsfeedActivityPubClient'); + } + + public function it_should_not_post_blog_without_a_pub_server() + { + $this->shouldThrow(new \LogicException('The PubSub URI has not been specified.')) + ->duringPostArticle($this->buildTestBlog(), null); + } + + public function it_should_not_post_blog_without_an_actor() + { + $this->setActivityPubServer('https://test'); + $this->shouldThrow(new \LogicException('The PubSub actor has not been specified.')) + ->duringPostArticle($this->buildTestBlog(), null); + } + + public function it_should_post_blog_to_pub_server() + { + $this->beConstructedWith($this->buildGuzzleMock()); + + $this->setActivityPubServer('http://localhost'); + $this->setActor('testuser', 'https://minds.com/testuser'); + $this->postArticle($this->buildTestBlog(), null) + ->shouldBe(200); + } + + public function it_should_not_like_an_entity_without_a_pub_server() + { + $this->shouldThrow(new \LogicException('The PubSub URI has not been specified.')) + ->duringLike('https://minds.com/user/1', null); + } + + public function it_should_not_like_an_entity_without_an_actor() + { + $this->setActivityPubServer('https://test'); + $this->shouldThrow(new \LogicException('The PubSub actor has not been specified.')) + ->duringLike('https://minds.com/user/1', null); + } + + public function it_should_send_like_to_pub_server() + { + $this->beConstructedWith($this->buildGuzzleMock()); + + $this->setActivityPubServer('http://localhost'); + $this->setActor('testuser', 'https://minds.com/testuser'); + $this->like('https://minds.com/user/1', null) + ->shouldBe(200); + } +} From 384d46c7928a8c80740c3d28681368b8e3a1d6a6 Mon Sep 17 00:00:00 2001 From: "Theodore R. Smith" Date: Wed, 5 Dec 2018 12:50:40 -0600 Subject: [PATCH 5/5] Moved the ActivityPub to the Newsfeed directory. --- Controllers/api/v1/newsfeed.php | 10 +++++----- .../Newsfeed/ActivityPubClient.php | 6 +++--- .../Newsfeed/ActivityPubClientSpec.php} | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) rename Helpers/NewsfeedActivityPubClient.php => Core/Newsfeed/ActivityPubClient.php (96%) rename Spec/{Helpers/NewsfeedActivityPubClientSpec.php => Core/Newsfeed/ActivityPubClientSpec.php} (91%) diff --git a/Controllers/api/v1/newsfeed.php b/Controllers/api/v1/newsfeed.php index b6d19e727a..c1d1ab87e7 100644 --- a/Controllers/api/v1/newsfeed.php +++ b/Controllers/api/v1/newsfeed.php @@ -17,20 +17,20 @@ use Minds\Helpers; use Minds\Entities\Factory as EntitiesFactory; use Minds\Helpers\Counters; -use Minds\Helpers\NewsfeedActivityPubClient; use Minds\Interfaces; use Minds\Interfaces\Flaggable; use Minds\Core\Di\Di; -use Minds\Interfaces\ActivityPubClient; +use Minds\Core\Newsfeed\ActivityPubClient; +use Minds\Interfaces\ActivityPubClient as iActivityPubClient; class newsfeed implements Interfaces\Api { - /** @var ActivityPubClient */ + /** @var iActivityPubClient */ protected $pubSubClient; - public function __construct(ActivityPubClient $pubSubClient = null) + public function __construct(iActivityPubClient $pubSubClient = null) { - $this->pubSubClient = $pubSubClient ?? new NewsfeedActivityPubClient(); + $this->pubSubClient = $pubSubClient ?? new ActivityPubClient(); // See https://project.hubzilla.org for how to set your own ActivityPub server. $this->pubSubClient->setActivityPubServer('https://project.hubzilla.org'); } diff --git a/Helpers/NewsfeedActivityPubClient.php b/Core/Newsfeed/ActivityPubClient.php similarity index 96% rename from Helpers/NewsfeedActivityPubClient.php rename to Core/Newsfeed/ActivityPubClient.php index 900cab8c1a..faa8bb7f92 100644 --- a/Helpers/NewsfeedActivityPubClient.php +++ b/Core/Newsfeed/ActivityPubClient.php @@ -1,6 +1,6 @@ shouldHaveType('Minds\Helpers\NewsfeedActivityPubClient'); + $this->shouldHaveType('Minds\Core\Newsfeed\ActivityPubClient'); } public function it_should_not_post_blog_without_a_pub_server()