diff --git a/Controllers/api/v1/newsfeed.php b/Controllers/api/v1/newsfeed.php index ac0cd3e8e..c1d1ab87e 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\Interfaces; use Minds\Interfaces\Flaggable; use Minds\Core\Di\Di; +use Minds\Core\Newsfeed\ActivityPubClient; +use Minds\Interfaces\ActivityPubClient as iActivityPubClient; class newsfeed implements Interfaces\Api { + /** @var iActivityPubClient */ + protected $pubSubClient; + + public function __construct(iActivityPubClient $pubSubClient = null) + { + $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'); + } + /** * Returns the newsfeed * @param array $pages @@ -420,6 +433,14 @@ 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://www.minds.com/{$user->username}"); + + $this->pubSubClient->postArticle($embeded); + // Follow activity (new Core\Notification\PostSubscriptions\Manager()) ->setEntityGuid($activity->guid) @@ -746,7 +767,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/Core/Newsfeed/ActivityPubClient.php b/Core/Newsfeed/ActivityPubClient.php new file mode 100644 index 000000000..faa8bb7f9 --- /dev/null +++ b/Core/Newsfeed/ActivityPubClient.php @@ -0,0 +1,142 @@ +config = Di::_()->get('Config'); + $this->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(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-US' + ], + 'id' => $this->config->site_url . "newsfeed/{$article->guid}", + 'type' => 'Article', + 'name' => $article->getTitle(), + 'content' => $article->getBody(), + '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, ?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-US' + ], + // 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", + '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 000000000..82cb24a5e --- /dev/null +++ b/Interfaces/ActivityPubClient.php @@ -0,0 +1,27 @@ +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\Core\Newsfeed\ActivityPubClient'); + } + + 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); + } +}