123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- <?php
- // This file is part of GNU social - https://www.gnu.org/software/social
- //
- // GNU social is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // GNU social is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
- /**
- * ActivityPub implementation for GNU social
- *
- * @package GNUsocial
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
- * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
- * @link http://www.gnu.org/software/social/
- */
- defined('GNUSOCIAL') || die();
- /**
- * ActivityPub's own Postman
- *
- * Standard workflow expects that we send an Explorer to find out destinataries'
- * inbox address. Then we send our postman to deliver whatever we want to send them.
- *
- * @category Plugin
- * @package GNUsocial
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
- */
- class Activitypub_postman
- {
- private $actor;
- private $actor_uri;
- private $to = [];
- private $failed_to = [];
- private $client;
- private $headers;
- /**
- * Create a postman to deliver something to someone
- *
- * @param Profile $from sender Profile
- * @param array $to receiver AProfiles
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function __construct(Profile $from, array $to = [])
- {
- $this->actor = $from;
- $this->to = $to;
- $this->actor_uri = $this->actor->getUri();
- $this->client = new HTTPClient();
- }
- /**
- * When dear postman dies, resurrect him until he finishes what he couldn't in life
- *
- * @throws ServerException
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function __destruct()
- {
- $qm = QueueManager::get();
- foreach($this->failed_to as $to => $activity) {
- $qm->enqueue([$to, $activity], 'activitypub_failed');
- }
- }
- /**
- * Send something to remote instance
- *
- * @param string $data request body
- * @param string $inbox url of remote inbox
- * @param string $method request method
- * @return GNUsocial_HTTPResponse
- * @throws HTTP_Request2_Exception
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function send($data, $inbox, $method = 'POST')
- {
- common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
- $headers = HttpSignature::sign($this->actor, $inbox, $data);
- common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));
- $this->client->setBody($data);
- switch ($method) {
- case 'POST':
- $response = $this->client->post($inbox, $headers);
- break;
- case 'GET':
- $response = $this->client->get($inbox, $headers);
- break;
- default:
- throw new Exception("Unsupported request method for postman.");
- }
- common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
- return $response;
- }
- /**
- * Send a follow notification to remote instance
- *
- * @return bool
- * @throws HTTP_Request2_Exception
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function follow()
- {
- $data = Activitypub_follow::follow_to_array($this->actor_uri, $this->to[0]->getUrl());
- $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
- $res_body = json_decode($res->getBody(), true);
- if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
- $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
- $pending_list->add();
- return true;
- } elseif (isset($res_body['error'])) {
- throw new Exception($res_body['error']);
- }
- throw new Exception("An unknown error occurred.");
- }
- /**
- * Send a Undo Follow notification to remote instance
- *
- * @return bool
- * @throws HTTP_Request2_Exception
- * @throws Exception
- * @throws Exception
- * @throws Exception
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function undo_follow()
- {
- $data = Activitypub_undo::undo_to_array(
- Activitypub_follow::follow_to_array(
- $this->actor_uri,
- $this->to[0]->getUrl()
- )
- );
- $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
- $res_body = json_decode($res->getBody(), true);
- if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
- Activitypub_profile::unsubscribeCacheUpdate($this->actor, $this->to[0]->local_profile());
- $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
- $pending_list->remove();
- return true;
- }
- if (isset($res_body['error'])) {
- throw new Exception($res_body['error']);
- }
- throw new Exception("An unknown error occurred.");
- }
- /**
- * Send a Accept Follow notification to remote instance
- *
- * @param string $id Follow activity id
- * @return bool
- * @throws HTTP_Request2_Exception
- * @throws Exception Description of HTTP Response error or generic error message.
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function accept_follow(string $id): bool
- {
- $data = Activitypub_accept::accept_to_array(
- Activitypub_follow::follow_to_array(
- $this->to[0]->getUrl(),
- $this->actor_uri,
- $id
- )
- );
- $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
- $res_body = json_decode($res->getBody(), true);
- if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
- $pending_list = new Activitypub_pending_follow_requests($this->to[0]->getID(), $this->actor->getID());
- $pending_list->remove();
- return true;
- }
- if (isset($res_body['error'])) {
- throw new Exception($res_body['error']);
- }
- throw new Exception("An unknown error occurred.");
- }
- /**
- * Send a Like notification to remote instances holding the notice
- *
- * @param Notice $notice
- * @throws HTTP_Request2_Exception
- * @throws InvalidUrlException
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function like(Notice $notice): void
- {
- $data = Activitypub_like::like_to_array(
- $this->actor_uri,
- $notice
- );
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- // accumulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $notice;
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
- }
- }
- /**
- * Send a Undo Like notification to remote instances holding the notice
- *
- * @param Notice $notice
- * @throws HTTP_Request2_Exception
- * @throws InvalidUrlException
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function undo_like($notice)
- {
- $data = Activitypub_undo::undo_to_array(
- Activitypub_like::like_to_array(
- $this->actor_uri,
- $notice
- )
- );
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- // accummulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $notice;
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
- }
- }
- /**
- * Send a Create notification to remote instances
- *
- * @param Notice $notice
- * @throws EmptyPkeyValueException
- * @throws HTTP_Request2_Exception
- * @throws InvalidUrlException
- * @throws ServerException
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function create_note($notice)
- {
- $data = Activitypub_create::create_to_array(
- $this->actor_uri,
- common_local_url('apNotice', ['id' => $notice->getID()]),
- Activitypub_notice::notice_to_array($notice)
- );
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- // accummulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $notice;
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
- }
- }
- /**
- * Send a Create direct-notification to remote instances
- *
- * @param Notice $message
- * @author Bruno Casteleiro <brunoccast@fc.up.pt>
- */
- public function create_direct_note(Notice $message)
- {
- $data = Activitypub_create::create_to_array(
- $this->actor_uri,
- common_local_url('apNotice', ['id' => $message->getID()]),
- Activitypub_message::message_to_array($message),
- true
- );
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- foreach ($this->to_inbox(false) as $inbox) {
- $res = $this->send($data, $inbox);
- // accummulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $message;
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
- }
- }
- /**
- * Send a Announce notification to remote instances
- *
- * @param Notice $notice
- * @param Notice $repeat_of
- * @throws HTTP_Request2_Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function announce(Notice $notice, Notice $repeat_of): void
- {
- $data = json_encode(
- Activitypub_announce::announce_to_array($this->actor, $notice, $repeat_of),
- JSON_UNESCAPED_SLASHES
- );
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- // accummulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $notice;
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
- }
- }
- /**
- * Send a Delete notification to remote instances holding the notice
- *
- * @param Notice $notice
- * @throws HTTP_Request2_Exception
- * @throws InvalidUrlException
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- public function delete_note($notice)
- {
- $data = Activitypub_delete::delete_to_array($notice);
- $errors = [];
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- $to_failed[$inbox] = $notice;
- }
- }
- if (!empty($errors)) {
- throw new Exception(json_encode($errors));
- }
- }
- /**
- * Send a Delete notification to remote followers of some deleted profile
- *
- * @param Profile $deleted_profile
- * @throws HTTP_Request2_Exception
- * @author Bruno Casteleiro <brunoccast@fc.up.pt>
- */
- public function delete_profile(Profile $deleted_profile)
- {
- $data = Activitypub_delete::delete_to_array($deleted_profile);
- $data = json_encode($data, JSON_UNESCAPED_SLASHES);
- $errors = [];
- foreach ($this->to_inbox() as $inbox) {
- $res = $this->send($data, $inbox);
- // accumulate errors for later use, if needed
- if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
- $res_body = json_decode($res->getBody(), true);
- $errors[] = isset($res_body['error']) ?
- $res_body['error'] : "An unknown error occurred.";
- }
- }
- if (!empty($errors)) {
- common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
- }
- }
- /**
- * Clean list of inboxes to deliver messages
- *
- * @param bool $actorFollowers whether to include the actor's follower collection
- * @return array To Inbox URLs
- * @throws Exception
- * @author Diogo Cordeiro <diogo@fc.up.pt>
- */
- private function to_inbox(bool $actorFollowers = true): array
- {
- if ($actorFollowers) {
- $discovery = new Activitypub_explorer();
- $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
- foreach ($followers as $sub) {
- try {
- $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
- } catch (Exception $e) {
- // Not an ActivityPub Remote Follower, let it go
- }
- }
- unset($discovery);
- }
- $to_inboxes = [];
- foreach ($this->to as $to_profile) {
- $i = $to_profile->get_inbox();
- // Prevent delivering to self
- if ($i == common_local_url('apInbox')) {
- continue;
- }
- $to_inboxes[] = $i;
- }
- return array_unique($to_inboxes);
- }
- }
|