code:
<?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 Pending follow requests
 *
 * @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_pending_follow_requests extends Managed_DataObject
{
    public $__table = 'Activitypub_pending_follow_requests';
    public $local_profile_id;
    public $remote_profile_id;
    private $_reldb = null;

    /**
     * Return table definition for Schema setup and DB_DataObject usage.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return array array of column definitions
     */
    public static function schemaDef()
    {
        return [
            'fields' => [
                'local_profile_id'  => ['type' => 'integer', 'not null' => true],
                'remote_profile_id' => ['type' => 'integer', 'not null' => true],
                'relation_id'       => ['type' => 'serial',  'not null' => true],
            ],
            'primary key' => ['relation_id'],
            'unique keys' => [
                'Activitypub_pending_follow_requests_relation_id_key' => ['relation_id'],
            ],
            'foreign keys' => [
                'Activitypub_pending_follow_requests_local_profile_id_fkey'  => ['profile', ['local_profile_id' => 'id']],
                'Activitypub_pending_follow_requests_remote_profile_id_fkey' => ['profile', ['remote_profile_id' => 'id']],
            ],
        ];
    }

    public function __construct($actor, $remote_actor)
    {
        $this->local_profile_id  = $actor;
        $this->remote_profile_id = $remote_actor;
    }

    /**
     * Add Follow request to table.
     *
     * @return boolean true if added, false otherwise
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function add()
    {
        return !$this->exists() && $this->insert();
    }

    /**
     * Check if a Follow request is pending.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return boolean true if is pending, false otherwise
     */
    public function exists()
    {
        $this->_reldb = clone ($this);
        if ($this->_reldb->find() > 0) {
            $this->_reldb->fetch();
            return true;
        }
        return false;
    }

    /**
     * Remove a request from the pending table.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return boolean true if removed, false otherwise
     */
    public function remove()
    {
        return $this->exists() && $this->_reldb->delete();
    }
}


code:
<?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 Profile
 *
 * @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_profile extends Managed_DataObject
{
    public $__table = 'Activitypub_profile';
    public $uri;                             // text()   not_null
    public $profile_id;                      // int(4)  primary_key not_null
    public $inboxuri;                        // text()   not_null
    public $sharedInboxuri;                  // text()
    public $nickname;                        // varchar(64)  multiple_key not_null
    public $fullname;                        // text()
    public $profileurl;                      // text()
    public $homepage;                        // text()
    public $bio;                             // text()  multiple_key
    public $location;                        // text()
    public $created;                         // datetime()   not_null
    public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP

    /**
     * Return table definition for Schema setup and DB_DataObject usage.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return array array of column definitions
     */
    public static function schemaDef()
    {
        return [
            'fields' => [
                'uri' => ['type' => 'text', 'not null' => true],
                'profile_id' => ['type' => 'integer'],
                'inboxuri' => ['type' => 'text', 'not null' => true],
                'sharedInboxuri' => ['type' => 'text'],
                'created' => ['type' => 'datetime', 'not null' => true],
                'modified' => ['type' => 'datetime', 'not null' => true],
            ],
            'primary key' => ['profile_id'],
            'unique keys' => [
                'Activitypub_profile_profile_id_key' => ['profile_id'],
            ],
            'foreign keys' => [
                'Activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']],
            ],
        ];
    }

    /**
     * Generates a pretty profile from a Profile object
     *
     * @param Profile $profile
     * @return array array to be used in a response
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function profile_to_array($profile)
    {
        $uri = ActivityPubPlugin::actor_uri($profile);
        $id = $profile->getID();
        $rsa = new Activitypub_rsa();
        $public_key = $rsa->ensure_public_key($profile);
        unset($rsa);
        $res = [
            '@context'          => [
                'https://www.w3.org/ns/activitystreams',
                'https://w3id.org/security/v1',
                [
                    'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers'
                ]
            ],
            'id'                => $uri,
            'type'              => 'Person',
            'following'         => common_local_url('apActorFollowing', ['id' => $id]),
            'followers'         => common_local_url('apActorFollowers', ['id' => $id]),
            'liked'             => common_local_url('apActorLiked', ['id' => $id]),
            'inbox'             => common_local_url('apInbox', ['id' => $id]),
            'outbox'            => common_local_url('apActorOutbox', ['id' => $id]),
            'preferredUsername' => $profile->getNickname(),
            'name'              => $profile->getBestName(),
            'summary'           => ($desc = $profile->getDescription()) == null ? "" : $desc,
            'url'               => $profile->getUrl(),
            'manuallyApprovesFollowers' => false,
            'publicKey' => [
                'id'    => $uri."#public-key",
                'owner' => $uri,
                'publicKeyPem' => $public_key
            ],
            'tag' => [],
            'attachment' => [],
            'icon' => [
                'type'      => 'Image',
                'mediaType' => 'image/png',
                'height'    => AVATAR_PROFILE_SIZE,
                'width'     => AVATAR_PROFILE_SIZE,
                'url'       => $profile->avatarUrl(AVATAR_PROFILE_SIZE)
            ]
        ];

        if ($profile->isLocal()) {
            $res['endpoints']['sharedInbox'] = common_local_url('apInbox');
        } else {
            $aprofile = new Activitypub_profile();
            $aprofile = $aprofile->from_profile($profile);
            $res['endpoints']['sharedInbox'] = $aprofile->sharedInboxuri;
        }

        return $res;
    }

    /**
     * Insert the current object variables into the database
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @access public
     * @throws ServerException
     */
    public function do_insert()
    {
        $profile = new Profile();

        $profile->created = $this->created = $this->modified = common_sql_now();

        $fields = [
                    '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');
            $this->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
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function local_profile()
    {
        $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
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param Profile $profile
     * @return Activitypub_profile
     * @throws Exception if no Activitypub_profile exists for given Profile
     */
    public static function from_profile(Profile $profile)
    {
        $profile_id = $profile->getID();

        $aprofile = self::getKV('profile_id', $profile_id);
        if (!$aprofile instanceof Activitypub_profile) {
            // No Activitypub_profile for this profile_id,
            if (!$profile->isLocal()) {
                // create one!
                $aprofile = self::create_from_local_profile($profile);
            } else {
                throw new Exception('No Activitypub_profile for Profile ID: '.$profile_id. ', this is a local user.');
            }
        }

        $fields = [
                    'uri'      => 'profileurl',
                    'nickname' => 'nickname',
                    'fullname' => 'fullname',
                    'bio'      => 'bio'
                    ];

        foreach ($fields as $af => $pf) {
            $aprofile->$af = $profile->$pf;
        }

        return $aprofile;
    }

    /**
     * Given an existent local profile creates an ActivityPub profile.
     * One must be careful not to give a user profile to this function
     * as only remote users have ActivityPub_profiles on local instance
     *
     * @param Profile $profile
     * @return Activitypub_profile
     * @throws HTTP_Request2_Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private static function create_from_local_profile(Profile $profile)
    {
        $aprofile = new Activitypub_profile();

        $url = $profile->getUri();
        $inboxes = Activitypub_explorer::get_actor_inboxes_uri($url);

        if ($inboxes == null) {
            throw new Exception('This is not an ActivityPub user thus AProfile is politely refusing to proceed.');
        }

        $aprofile->created = $aprofile->modified = common_sql_now();

        $aprofile                 = new Activitypub_profile;
        $aprofile->profile_id     = $profile->getID();
        $aprofile->uri            = $url;
        $aprofile->nickname       = $profile->getNickname();
        $aprofile->fullname       = $profile->getFullname();
        $aprofile->bio            = substr($profile->getDescription(), 0, 1000);
        $aprofile->inboxuri       = $inboxes["inbox"];
        $aprofile->sharedInboxuri = $inboxes["sharedInbox"];

        $aprofile->insert();

        return $aprofile;
    }

    /**
     * Returns sharedInbox if possible, inbox otherwise
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return string Inbox URL
     */
    public function get_inbox()
    {
        if (is_null($this->sharedInboxuri)) {
            return $this->inboxuri;
        }

        return $this->sharedInboxuri;
    }

    /**
     * Getter for uri property
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return string URI
     */
    public function getUri()
    {
        return $this->uri;
    }

    /**
     * Getter for url property
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return string URL
     */
    public function getUrl()
    {
        return $this->getUri();
    }

    /**
     * Getter for id property
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return int
     */
    public function getID()
    {
        return $this->profile_id;
    }

    /**
     * Ensures a valid Activitypub_profile when provided with a valid URI.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $url
     * @return Activitypub_profile
     * @throws Exception if it isn't possible to return an Activitypub_profile
     */
    public static function fromUri($url)
    {
        try {
            return self::from_profile(Activitypub_explorer::get_profile_from_url($url));
        } catch (Exception $e) {
            throw new Exception('No valid ActivityPub profile found for given URI.');
        }
    }

    /**
     * Look up, and if necessary create, an Activitypub_profile for the remote
     * entity with the given webfinger address.
     * This should never return null -- you will either get an object or
     * an exception will be thrown.
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $addr webfinger address
     * @return Activitypub_profile
     * @throws Exception on error conditions
     */
    public static function ensure_web_finger($addr)
    {
        // Normalize $addr, i.e. add 'acct:' if missing
        $addr = Discovery::normalize($addr);

        // Try the cache
        $uri = self::cacheGet(sprintf('activitypub_profile:webfinger:%s', $addr));

        if ($uri !== false) {
            if (is_null($uri)) {
                // Negative cache entry
                // TRANS: Exception.
                throw new Exception(_m('Not a valid webfinger address (via cache).'));
            }
            try {
                return self::fromUri($uri);
            } catch (Exception $e) {
                common_log(LOG_ERR, sprintf(__METHOD__ . ': Webfinger address cache inconsistent with database, did not find Activitypub_profile uri==%s', $uri));
                self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), false);
            }
        }

        // Now, try some discovery

        $disco = new Discovery();

        try {
            $xrd = $disco->lookup($addr);
        } catch (Exception $e) {
            // Save negative cache entry so we don't waste time looking it up again.
            // @todo FIXME: Distinguish temporary failures?
            self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), null);
            // TRANS: Exception.
            throw new Exception(_m('Not a valid webfinger address.'));
        }

        $hints = array_merge(
            array('webfinger' => $addr),
            DiscoveryHints::fromXRD($xrd)
                );

        // If there's an Hcard, let's grab its info
        if (array_key_exists('hcard', $hints)) {
            if (!array_key_exists('profileurl', $hints) ||
                        $hints['hcard'] != $hints['profileurl']) {
                $hcardHints = DiscoveryHints::fromHcardUrl($hints['hcard']);
                $hints = array_merge($hcardHints, $hints);
            }
        }

        // If we got a profile page, try that!
        $profileUrl = null;
        if (array_key_exists('profileurl', $hints)) {
            $profileUrl = $hints['profileurl'];
            try {
                common_log(LOG_INFO, "Discovery on acct:$addr with profile URL $profileUrl");
                $aprofile = self::fromUri($hints['profileurl']);
                self::cacheSet(sprintf('activitypub_profile:webfinger:%s', $addr), $aprofile->getUri());
                return $aprofile;
            } catch (Exception $e) {
                common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage());
                // keep looking
                                //
                                // @todo FIXME: This means an error discovering from profile page
                                // may give us a corrupt entry using the webfinger URI, which
                                // will obscure the correct page-keyed profile later on.
            }
        }

        // XXX: try hcard
        // XXX: try FOAF

        // TRANS: Exception. %s is a webfinger address.
        throw new Exception(sprintf(_m('Could not find a valid profile for "%s".'), $addr));
    }

    /**
     * Update remote user profile in local instance
     * Depends on do_update
     *
     * @param Activitypub_profile $aprofile
     * @param array $res remote response
     * @return Profile remote Profile object
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function update_profile($aprofile, $res)
    {
        // ActivityPub Profile
        $aprofile->uri            = $res['id'];
        $aprofile->nickname       = $res['preferredUsername'];
        $aprofile->fullname       = isset($res['name']) ? $res['name'] : null;
        $aprofile->bio            = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null;
        $aprofile->inboxuri       = $res['inbox'];
        $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'];

        $profile = $aprofile->local_profile();

        $profile->modified = $aprofile->modified = common_sql_now();

        $fields = [
                    'uri'      => 'profileurl',
                    'nickname' => 'nickname',
                    'fullname' => 'fullname',
                    'bio'      => 'bio'
                    ];

        foreach ($fields as $af => $pf) {
            $profile->$pf = $aprofile->$af;
        }

        // Profile
        $profile->update();
        $aprofile->update();

        // Public Key
        Activitypub_rsa::update_public_key($profile, $res['publicKey']['publicKeyPem']);

        // Avatar
        if (isset($res['icon']['url'])) {
            try {
                Activitypub_explorer::update_avatar($profile, $res['icon']['url']);
            } catch (Exception $e) {
                // Let the exception go, it isn't a serious issue
                common_debug('An error ocurred while grabbing remote avatar'.$e->getMessage());
            }
        }

        return $profile;
    }
}

repo_name: andremichelle/gnu

license: mit


code:
<?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 error representation
 *
 * @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_reject extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Reject
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function reject_to_array($object)
    {
        $res = [
                '@context' => 'https://www.w3.org/ns/activitystreams',
                "type"   => "Reject",
                "object" => $object
        ];
        return $res;
    }
}


code:
<?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 Keys System
 *
 * @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_rsa extends Managed_DataObject
{
    public $__table = 'Activitypub_rsa';

    /**
     * Return table definition for Schema setup and DB_DataObject usage.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return array array of column definitions
     */
    public static function schemaDef()
    {
        return [
                'fields' => [
                    'profile_id'  => ['type' => 'integer'],
                    'private_key' => ['type' => 'text'],
                    'public_key'  => ['type' => 'text', 'not null' => true],
                    'created'     => ['type' => 'datetime', 'not null' => true],
                    'modified'    => ['type' => 'datetime', 'not null' => true],
                ],
                'primary key' => ['profile_id'],
                'unique keys' => [
                    'Activitypub_rsa_profile_id_key'  => ['profile_id'],
                ],
                'foreign keys' => [
                    'Activitypub_profile_profile_id_fkey' => ['profile', ['profile_id' => 'id']],
                ],
        ];
    }

    public function get_private_key($profile)
    {
        $this->profile_id = $profile->getID();
        $apRSA = self::getKV('profile_id', $this->profile_id);
        if (!$apRSA instanceof Activitypub_rsa) {
            // No existing key pair for this profile
            if ($profile->isLocal()) {
                self::generate_keys($this->private_key, $this->public_key);
                $this->store_keys();
            } else {
                throw new Exception('This is a remote Profile, there is no Private Key for this Profile.');
            }
        }
        return $apRSA->private_key;
    }

    /**
     * Guarantees a Public Key for a given profile.
     *
     * @param Profile $profile
     * @param bool $fetch
     * @return string The public key
     * @throws ServerException It should never occur, but if so, we break everything!
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function ensure_public_key($profile, $fetch = true)
    {
        $this->profile_id = $profile->getID();
        $apRSA = self::getKV('profile_id', $this->profile_id);
        if (!$apRSA instanceof Activitypub_rsa) {
            // No existing key pair for this profile
            if ($profile->isLocal()) {
                self::generate_keys($this->private_key, $this->public_key);
                $this->store_keys();
            } else {
                // This should never happen, but try to recover!
                if ($fetch) {
                    $res = Activitypub_explorer::get_remote_user_activity(ActivityPubPlugin::actor_uri($profile));
                    Activitypub_rsa::update_public_key($profile, $res['publicKey']['publicKeyPem']);
                    return self::ensure_public_key($profile, false);
                } else {
                    throw new ServerException('Activitypub_rsa: Failed to find keys for given profile. That should have not happened!');
                }
            }
        }
        return $apRSA->public_key;
    }

    /**
     * Insert the current object variables into the database.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @access public
     * @throws ServerException
     */
    public function store_keys()
    {
        $this->created = $this->modified = common_sql_now();
        $ok = $this->insert();
        if ($ok === false) {
            throw new ServerException('Cannot save ActivityPub RSA.');
        }
    }

    /**
     * Generates a pair of RSA keys.
     *
     * @author PHP Manual Contributed Notes <dirt@awoms.com>
     * @param string $private_key in/out
     * @param string $public_key in/out
     */
    public static function generate_keys(&$private_key, &$public_key)
    {
        $config = [
            'digest_alg'       => 'sha512',
            'private_key_bits' => 2048,
            'private_key_type' => OPENSSL_KEYTYPE_RSA,
        ];

        // Create the private and public key
        $res = openssl_pkey_new($config);

        // Extract the private key from $res to $private_key
        openssl_pkey_export($res, $private_key);

        // Extract the public key from $res to $pubKey
        $pubKey = openssl_pkey_get_details($res);
        $public_key = $pubKey["key"];
        unset($pubKey);
    }

    /**
     * Update public key.
     *
     * @param Profile $profile
     * @param string $public_key
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function update_public_key($profile, $public_key)
    {
        // Public Key
        $apRSA = new Activitypub_rsa();
        $apRSA->profile_id = $profile->getID();
        $apRSA->public_key = $public_key;
        $apRSA->modified = common_sql_now();
        if (!$apRSA->update()) {
            $apRSA->insert();
        }
    }
}


code:
<?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 representation of a Tag
 *
 * @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_tag extends Managed_DataObject
{
    /**
     * Generates a pretty tag from a Tag object
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array Tag $tag
     * @return array pretty array to be used in a response
     */
    public static function tag_to_array($tag)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'name' => $tag,
            'url'  => common_local_url('tag', ['tag' => $tag])
        ];
        return $res;
    }
}


code:
<?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 error representation
 *
 * @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_undo extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Undo
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function undo_to_array($object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => $object['id'].'/undo',
            'type'   => 'Undo',
            'actor'  => $object['actor'],
            'object' => $object
        ];
        return $res;
    }

    /**
     * Verifies if a given object is acceptable for a Undo Activity.
     *
     * @param array $object
     * @return bool
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function validate_object($object)
    {
        if (!is_array($object)) {
            throw new Exception('Invalid Object Format for Undo Activity.');
        }
        if (!isset($object['type'])) {
            throw new Exception('Object type was not specified for Undo Activity.');
        }
        switch ($object['type']) {
            case 'Follow':
            case 'Like':
                // Validate data
                if (!filter_var($object['object'], FILTER_VALIDATE_URL)) {
                    throw new Exception('Object is not a valid Object URI for Activity.');
                }
                break;
            default:
                throw new Exception('This is not a supported Object Type for Undo Activity.');
        }
        return true;
    }
}


code:
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>ActivityPub plugin for GNU Social API Documentation</title>
    <!-- needed for adaptive design -->
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
    <link rel="icon" href="https://git.gnu.io/gnu/gnu-social/raw/b286e8798b7f89cd72ef19dc335624100f43ce94/theme/neo-gnu/favicon.ico">
    <!--
    ReDoc doesn't change outer page styles
    -->
    <style>
      body {
        margin: 0;
        padding: 0;
      }
    </style>
  </head>
  <body>
    <redoc spec-url='openapi.json'></redoc>
    <script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
  </body>
</html>


code:
ActivityPub Plugin for GNU Social Doc
=====================================

## Contents

- [Objects](#entities)
    - [Attachment](#attachment)
    - [Error](#error)
    - [Image](#image)
    - [Notice](#notice)
    - [Profile](#profile)
    - [Tag](#tag)
- [Activities](#activities)
    - [Create](#create)
    - [Announce](#announce)
    - [Delete](#delete)
    - [Undo](#undo)
    - [Accept](#accept)
    - [Reject](#reject)
    - [Like](#like)
    - [Follow](#follow)

## Objects

### Attachment

| Attribute                | Description                                     | Nullable | Type    |
| ------------------------ | ----------------------------------------------- | -------- | ------- |
| `id`                     | ID of the attachment                            | no       | int32   |
| `mimetype`               | Mimetype                                        | no       | string  |
| `url`                    | URL of the locally hosted version of the image  | no       | string  |
| `meta`                   | See **attachment metadata** below               | yes      | Array   |
| `title`                  | Attachment title                                | no       | string  |

**Attachment metadata:**

Images may contain `width`, `height`, `size`.

### Error

The most important part of an error response is the HTTP status code. Standard semantics are followed. The body of an error is a JSON object with this structure:

| Attribute                | Description                         | Nullable | Type    |
| ------------------------ | ----------------------------------- | -------- | ------- |
| `error`                  | A textual description of the error  | no       | string  |

### Image

| Attribute                | Description     | Nullable | Type    |
| ------------------------ | --------------- | -------- | ------- |
| `type`                   | "Image"         | no       | string  |
| `width`                  | Image's width   | no       | int32   |
| `height`                 | Image's height  | no       | int32   |
| `url`                    | Image URL       | no       | string  |

### Notice

| Attribute                | Description                                       | Nullable | Type                                 |
| ------------------------ | ------------------------------------------------- | -------- | ------------------------------------ |
| `id`                     | Notice's URL                                      | no       | string                               |
| `type`                   | Notice's Type                                     | no       | string                               |
| `actor`                  | URL of Notice owner profile page (can be remote)  | no       | string                               |
| `published`              | DateTime of notice creation                       | no       | datetime                             |
| `to`                     | To                                                | no       | string                               |
| `cc`                     | CC                                                | no       | string                               |
| `content`                | Notice's Content in plain text                    | no       | string                               |
| `url`                    | Notice's URL                                      | no       | string                               |
| `reply_to`               | ID of the notice this replies                     | yes      | int32                                |
| `is_local`               | Boolean, true if local, false otherwise           | no       | bool                                 |
| `conversation`           | Notice conversation id                            | no       | int32                                |
| `attachment`             | Array of [Attachments](#attachment)               | no       | Array of [Attachments](#attachment)  |
| `tag`                    | Array of [Tags](#tag)                             | no       | Array of [Tags](#tag)                |

### Profile

| Attribute                | Description                                      | Nullable | Type             |
| ------------------------ | ------------------------------------------------ | -------- | ---------------- |
| `@context`               | Standard compliance                              | no       | string           |
| `id`                     | Actor's id                                       | no       | int32            |
| `type`                   | "Person"                                         | no       | string           |
| `nickname`               | Actor's nickname                                 | no       | string           |
| `is_local`               | True if local, false otherwise                   | no       | bool             |
| `inbox`                  | URL to Actor's inbox endpoint                    | no       | string           |
| `outbox`                 | URL to Actor's outbox endpoint                   | no       | string           |
| `display_name`           | The Actor's display name                         | no       | string           |
| `followers`              | URL to Actor's followers endpoint                | no       | string           |
| `followers_count`        | Total number of followers                        | no       | int32            |
| `following`              | URL to Actor's following endpoint                | no       | string           |
| `following_count`        | Total number of following                        | no       | int32            |
| `liked`                  | URL to Actor's Liked collection endpoint         | no       | string           |
| `liked_count`            | Total number of favorites                        | no       | int32            |
| `summary`                | Actor's biography                                | no       | string           |
| `url`                    | URL of the Actor's profile page (can be remote)  | no       | string           |
| `avatar`                 | Actor's avatar                                   | no       | [Image](#image)  |

### Tag

| Attribute                | Description                                  | Nullable | Type       |
| ------------------------ | -------------------------------------------- | -------- | ---------- |
| `name`                   | The hashtag, not including the preceding `#` | no       | string     |
| `url`                    | The URL of the hashtag                       | no       | string     |


code:
<?php
/**
 * Note : Code is released under the GNU LGPL
 *
 * Please do not change the header of this file
 *
 * This library is free software; you can redistribute it and/or modify it under the terms of the GNU
 * Lesser General Public License as published by the Free Software Foundation; either version 2 of
 * the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 */

/**
 * The AcceptHeader page will parse and sort the different
 * allowed types for the content negociations
 *
 * @author Pierrick Charron <pierrick@webstart.fr>
 */
class AcceptHeader extends \ArrayObject
{
    /**
     * Constructor
     *
     * @param string $header Value of the Accept header
     * @return void
     */
    public function __construct($header)
    {
        $acceptedTypes = $this->_parse($header);
        usort($acceptedTypes, [$this, '_compare']);
        parent::__construct($acceptedTypes);
    }

    /**
     * Parse the accept header and return an array containing
     * all the informations about the Accepted types
     *
     * @param string $data Value of the Accept header
     * @return array
     */
    private function _parse($data)
    {
        $array = [];
        $items = explode(',', $data);
        foreach ($items as $item) {
            $elems = explode(';', $item);

            $acceptElement = [];
            $mime = current($elems);
            list($type, $subtype) = explode('/', $mime);
            $acceptElement['type'] = trim($type);
            $acceptElement['subtype'] = trim($subtype);
            $acceptElement['raw'] = $mime;

            $acceptElement['params'] = [];
            while (next($elems)) {
                list($name, $value) = explode('=', current($elems));
                $acceptElement['params'][trim($name)] = trim($value);
            }

            $array[] = $acceptElement;
        }
        return $array;
    }

    /**
     * Compare two Accepted types with their parameters to know
     * if one media type should be used instead of an other
     *
     * @param array $a The first media type and its parameters
     * @param array $b The second media type and its parameters
     * @return int
     */
    private function _compare($a, $b)
    {
        $a_q = isset($a['params']['q']) ? floatval($a['params']['q']) : 1.0;
        $b_q = isset($b['params']['q']) ? floatval($b['params']['q']) : 1.0;
        if ($a_q === $b_q) {
            $a_count = count($a['params']);
            $b_count = count($b['params']);
            if ($a_count === $b_count) {
                if ($r = $this->_compareSubType($a['subtype'], $b['subtype'])) {
                    return $r;
                } else {
                    return $this->_compareSubType($a['type'], $b['type']);
                }
            } else {
                return $a_count < $b_count;
            }
        } else {
            return $a_q < $b_q;
        }
    }

    /**
     * Compare two subtypes
     *
     * @param string $a First subtype to compare
     * @param string $b Second subtype to compare
     * @return int
     */
    private function _compareSubType($a, $b)
    {
        if ($a === '*' && $b !== '*') {
            return 1;
        } elseif ($b === '*' && $a !== '*') {
            return -1;
        } else {
            return 0;
        }
    }
}


code:
<?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();

/**
 * Utility class to hold a bunch of constant defining default verb types
 *
 * @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_activityverb2 extends Managed_DataObject
{
    const FULL_LIST =
    [
        'Accept'          => 'https://www.w3.org/ns/activitystreams#Accept',
        'TentativeAccept' => 'https://www.w3.org/ns/activitystreams#TentativeAccept',
        'Add'             => 'https://www.w3.org/ns/activitystreams#Add',
        'Arrive'          => 'https://www.w3.org/ns/activitystreams#Arrive',
        'Create'          => 'https://www.w3.org/ns/activitystreams#Create',
        'Delete'          => 'https://www.w3.org/ns/activitystreams#Delete',
        'Follow'          => 'https://www.w3.org/ns/activitystreams#Follow',
        'Ignore'          => 'https://www.w3.org/ns/activitystreams#Ignore',
        'Join'            => 'https://www.w3.org/ns/activitystreams#Join',
        'Leave'           => 'https://www.w3.org/ns/activitystreams#Leave',
        'Like'            => 'https://www.w3.org/ns/activitystreams#Like',
        'Offer'           => 'https://www.w3.org/ns/activitystreams#Offer',
        'Invite'          => 'https://www.w3.org/ns/activitystreams#Invite',
        'Reject'          => 'https://www.w3.org/ns/activitystreams#Reject',
        'TentativeReject' => 'https://www.w3.org/ns/activitystreams#TentativeReject',
        'Remove'          => 'https://www.w3.org/ns/activitystreams#Remove',
        'Undo'            => 'https://www.w3.org/ns/activitystreams#Undo',
        'Update'          => 'https://www.w3.org/ns/activitystreams#Update',
        'View'            => 'https://www.w3.org/ns/activitystreams#View',
        'Listen'          => 'https://www.w3.org/ns/activitystreams#Listen',
        'Read'            => 'https://www.w3.org/ns/activitystreams#Read',
        'Move'            => 'https://www.w3.org/ns/activitystreams#Move',
        'Travel'          => 'https://www.w3.org/ns/activitystreams#Travel',
        'Announce'        => 'https://www.w3.org/ns/activitystreams#Announce',
        'Block'           => 'https://www.w3.org/ns/activitystreams#Block',
        'Flag'            => 'https://www.w3.org/ns/activitystreams#Flag',
        'Dislike'         => 'https://www.w3.org/ns/activitystreams#Dislike',
        'Question'        => 'https://www.w3.org/ns/activitystreams#Question'
    ];

    const KNOWN =
    [
        'Accept',
        'Create',
        'Delete',
        'Follow',
        'Like',
        'Undo',
        'Announce'
    ];

    /**
     * Converts canonical into verb.
     *
     * @author GNU social
     * @param string $verb
     * @return string
     */
    public static function canonical($verb)
    {
        $ns = 'https://www.w3.org/ns/activitystreams#';
        if (substr($verb, 0, mb_strlen($ns)) == $ns) {
            return substr($verb, mb_strlen($ns));
        } else {
            return $verb;
        }
    }
}


code:
<?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    Evan Prodromou
 * @author    Brion Vibber
 * @author    James Walker
 * @author    Siebrand Mazeland
 * @author    Mikael Nordfeldth
 * @author    Diogo Cordeiro
 * @copyright 2010-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();

class DiscoveryHints
{
    public static function fromXRD(XML_XRD $xrd)
    {
        $hints = [];

        if (Event::handle('StartDiscoveryHintsFromXRD', [$xrd, &$hints])) {
            foreach ($xrd->links as $link) {
                switch ($link->rel) {
                case WebFingerResource_Profile::PROFILEPAGE:
                    $hints['profileurl'] = $link->href;
                    break;
                case Discovery::UPDATESFROM:
                    if (empty($link->type) || $link->type == 'application/atom+xml') {
                        $hints['feedurl'] = $link->href;
                    }
                    break;
                case Discovery::HCARD:
                case Discovery::MF2_HCARD:
                    $hints['hcard'] = $link->href;
                    break;
                default:
                    break;
                }
            }
            Event::handle('EndDiscoveryHintsFromXRD', [$xrd, &$hints]);
        }

        return $hints;
    }

    public static function fromHcardUrl($url)
    {
        $client = new HTTPClient();
        $client->setHeader('Accept', 'text/html,application/xhtml+xml');
        try {
            $response = $client->get($url);

            if (!$response->isOk()) {
                return null;
            }
        } catch (HTTP_Request2_Exception $e) {
            // Any HTTPClient error that might've been thrown
            common_log(LOG_ERR, __METHOD__ . ':'.$e->getMessage());
            return null;
        }

        return self::hcardHints(
            $response->getBody(),
            $response->getEffectiveUrl()
        );
    }

    public static function hcardHints($body, $url)
    {
        $hcard = self::_hcard($body, $url);

        if (empty($hcard)) {
            return [];
        }

        $hints = [];

        // XXX: don't copy stuff into an array and then copy it again

        if (array_key_exists('nickname', $hcard) && !empty($hcard['nickname'][0])) {
            $hints['nickname'] = $hcard['nickname'][0];
        }

        if (array_key_exists('name', $hcard) && !empty($hcard['name'][0])) {
            $hints['fullname'] = $hcard['name'][0];
        }

        if (array_key_exists('photo', $hcard) && count($hcard['photo'])) {
            $hints['avatar'] = $hcard['photo'][0];
        }

        if (array_key_exists('note', $hcard) && !empty($hcard['note'][0])) {
            $hints['bio'] = $hcard['note'][0];
        }

        if (array_key_exists('adr', $hcard) && !empty($hcard['adr'][0])) {
            $hints['location'] = $hcard['adr'][0]['value'];
        }

        if (array_key_exists('url', $hcard) && !empty($hcard['url'][0])) {
            $hints['homepage'] = $hcard['url'][0];
        }

        return $hints;
    }

    public static function _hcard($body, $url)
    {
        $mf2 = new Mf2\Parser($body, $url);
        $mf2 = $mf2->parse();

        if (empty($mf2['items'])) {
            return null;
        }

        $hcards = [];

        foreach ($mf2['items'] as $item) {
            if (!in_array('h-card', $item['type'])) {
                continue;
            }

            // We found a match, return it immediately
            if (isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) {
                return $item['properties'];
            }

            // Let's keep all the hcards for later, to return one of them at least
            $hcards[] = $item['properties'];
        }

        // No match immediately for the url we expected, but there were h-cards found
        if (count($hcards) > 0) {
            return $hcards[0];
        }

        return null;
    }
}


code:
<?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 Explorer
 *
 * Allows to discovery new (or the same) Profiles (both local or remote)
 *
 * @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_explorer
{
    private $discovered_actor_profiles = [];

    /**
     * Shortcut function to get a single profile from its URL.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $url
     * @return Profile
     * @throws Exception
     */
    public static function get_profile_from_url($url)
    {
        $discovery = new Activitypub_explorer;
        // Get valid Actor object
        $actor_profile = $discovery->lookup($url);
        if (!empty($actor_profile)) {
            return $actor_profile[0];
        }
        throw new Exception('Invalid Actor.');
    }

    /**
     * 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
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function lookup($url)
    {
        if (in_array($url, ACTIVITYPUB_PUBLIC_TO)) {
            return [];
        }

        common_debug('ActivityPub Explorer: Started now looking for '.$url);
        $this->discovered_actor_profiles = [];

        return $this->_lookup($url);
    }

    /**
     * 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
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    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
        if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) {
            throw new Exception('User not found.');
        }

        return $this->discovered_actor_profiles;
    }

    /**
     * This ensures that we are using a valid ActivityPub URI
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $url
     * @return boolean success state (related to the response)
     * @throws Exception (If the HTTP request fails)
     */
    private function ensure_proper_remote_uri($url)
    {
        $client    = new HTTPClient();
        $headers   = [];
        $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);
        $res = json_decode($response->getBody(), true);
        if (self::validate_remote_response($res)) {
            $this->temp_res = $res;
            return true;
        } else {
            common_debug('ActivityPub Explorer: Invalid potential remote actor while ensuring URI: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
        }

        return false;
    }

    /**
     * Get a local user profile from its URL and joins it on
     * $this->discovered_actor_profiles
     *
     * @param string $uri Actor's uri
     * @param bool $online
     * @return boolean success state
     * @throws NoProfileException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function grab_local_user($uri, $online = false)
    {
        if ($online) {
            common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' with online resources.');
        } else {
            common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' offline.');
        }
        // Ensure proper remote URI
        // If an exception occurs here it's better to just leave everything
        // break than to continue processing
        if ($online && $this->ensure_proper_remote_uri($uri)) {
            $uri = $this->temp_res["id"];
        }

        // Try standard ActivityPub route
        // Is this a known filthy little mudblood?
        $aprofile = self::get_aprofile_by_url($uri);
        if ($aprofile instanceof Activitypub_profile) {
            $profile = $aprofile->local_profile();
            common_debug('ActivityPub Explorer: Found a local Aprofile for '.$uri);
            // We found something!
            $this->discovered_actor_profiles[]= $profile;
            unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
            return true;
        } else {
            common_debug('ActivityPub Explorer: Unable to find a local Aprofile for '.$uri.' - looking for a Profile instead.');
            // Well, maybe it is a pure blood?
            // Iff, we are in the same instance:
            $ACTIVITYPUB_BASE_ACTOR_URI_length = strlen(ACTIVITYPUB_BASE_ACTOR_URI);
            if (substr($uri, 0, $ACTIVITYPUB_BASE_ACTOR_URI_length) == ACTIVITYPUB_BASE_ACTOR_URI) {
                try {
                    $profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_ACTOR_URI_length)));
                    common_debug('ActivityPub Explorer: Found a Profile for '.$uri);
                    // We found something!
                    $this->discovered_actor_profiles[]= $profile;
                    unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
                    return true;
                } catch (Exception $e) {
                    // Let the exception go on its merry way.
                    common_debug('ActivityPub Explorer: Unable to find a Profile for '.$uri);
                }
            }
        }

        // If offline grabbing failed, attempt again with online resources
        if (!$online) {
            common_debug('ActivityPub Explorer: Will try everything again with online resources against: '.$uri);
            return $this->grab_local_user($uri, true);
        }

        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
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function grab_remote_user($url)
    {
        common_debug('ActivityPub Explorer: Trying to grab a remote actor for '.$url);
        if (!isset($this->temp_res)) {
            $client    = new HTTPClient();
            $headers   = [];
            $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);
            $res = json_decode($response->getBody(), true);
        } else {
            $res = $this->temp_res;
            unset($this->temp_res);
        }
        if (isset($res['type']) && $res['type'] === 'OrderedCollection' && isset($res['first'])) { // It's a potential collection of actors!!!
            common_debug('ActivityPub Explorer: Found a collection of actors for '.$url);
            $this->travel_collection($res['first']);
            return true;
        } elseif (self::validate_remote_response($res)) {
            common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url);
            $this->discovered_actor_profiles[]= $this->store_profile($res);
            return true;
        } else {
            common_debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
        }

        // TODO: Fallback to OStatus
        return false;
    }

    /**
     * Save remote user profile in local instance
     *
     * @param array $res remote response
     * @return Profile remote Profile object
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function store_profile($res)
    {
        // ActivityPub Profile
        $aprofile                 = new Activitypub_profile;
        $aprofile->uri            = $res['id'];
        $aprofile->nickname       = $res['preferredUsername'];
        $aprofile->fullname       = isset($res['name']) ? $res['name'] : null;
        $aprofile->bio            = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null;
        $aprofile->inboxuri       = $res['inbox'];
        $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'];

        $aprofile->do_insert();
        $profile = $aprofile->local_profile();

        // Public Key
        $apRSA = new Activitypub_rsa();
        $apRSA->profile_id = $profile->getID();
        $apRSA->public_key = $res['publicKey']['publicKeyPem'];
        $apRSA->store_keys();

        // Avatar
        if (isset($res['icon']['url'])) {
            try {
                $this->update_avatar($profile, $res['icon']['url']);
            } catch (Exception $e) {
                // Let the exception go, it isn't a serious issue
                common_debug('ActivityPub Explorer: An error ocurred while grabbing remote avatar: '.$e->getMessage());
            }
        }

        return $profile;
    }

    /**
     * Download and update given avatar image
     *
     * @author GNU social
     * @param  Profile $profile
     * @param  string $url
     * @return Avatar    The Avatar we have on disk.
     * @throws Exception in various failure cases
     */
    public static function update_avatar(Profile $profile, $url)
    {
        common_debug('ActivityPub Explorer: Started grabbing remote avatar from: '.$url);
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
            // TRANS: Server exception. %s is a URL.
            common_debug('ActivityPub Explorer: Failed because it is an invalid url: '.$url);
            throw new ServerException(sprintf('Invalid avatar URL %s.', $url));
        }

        // @todo FIXME: This should be better encapsulated
        // ripped from oauthstore.php (for old OMB client)
        $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
        try {
            $imgData = HTTPClient::quickGet($url);
            // Make sure it's at least an image file. ImageFile can do the rest.
            if (false === getimagesizefromstring($imgData)) {
                common_debug('ActivityPub Explorer: Failed because the downloaded avatar: '.$url. 'is not a valid image.');
                throw new UnsupportedMediaException('Downloaded avatar was not an image.');
            }
            file_put_contents($temp_filename, $imgData);
            unset($imgData);    // No need to carry this in memory.
            common_debug('ActivityPub Explorer: Stored dowloaded avatar in: '.$temp_filename);

            $id = $profile->getID();

            $imagefile = new ImageFile(null, $temp_filename);
            $filename = Avatar::filename(
                $id,
                image_type_to_extension($imagefile->type),
                null,
                common_timestamp()
            );
            rename($temp_filename, Avatar::path($filename));
            common_debug('ActivityPub Explorer: Moved avatar from: '.$temp_filename.' to '.$filename);
        } catch (Exception $e) {
            common_debug('ActivityPub Explorer: Something went wrong while processing the avatar from: '.$url.' details: '.$e->getMessage());
            unlink($temp_filename);
            throw $e;
        }
        // @todo FIXME: Hardcoded chmod is lame, but seems to be necessary to
        // keep from accidentally saving images from command-line (queues)
        // that can't be read from web server, which causes hard-to-notice
        // problems later on:
        //
        // http://status.net/open-source/issues/2663
        chmod(Avatar::path($filename), 0644);

        $profile->setOriginal($filename);

        $orig = clone($profile);
        $profile->avatar = $url;
        $profile->update($orig);

        common_debug('ActivityPub Explorer: Seted Avatar from: '.$url.' to profile.');
        return Avatar::getUploaded($profile);
    }

    /**
     * Validates a remote response in order to determine whether this
     * response is a valid profile or not
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $res remote response
     * @return boolean success state
     */
    public static function validate_remote_response($res)
    {
        if (!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem'])) {
            return false;
        }

        return true;
    }

    /**
     * Get a ActivityPub Profile from it's uri
     * 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)
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $v URL
     * @return boolean|Activitypub_profile false if fails | Aprofile object if successful
     */
    public static function get_aprofile_by_url($v)
    {
        $i = Managed_DataObject::getcached("Activitypub_profile", "uri", $v);
        if (empty($i)) { // false = cache miss
            $i = new Activitypub_profile;
            $result = $i->get("uri", $v);
            if ($result) {
                // Hit!
                $i->encache();
            } else {
                return false;
            }
        }
        return $i;
    }

    /**
     * Given a valid actor profile url returns its inboxes
     *
     * @param string $url of Actor profile
     * @return boolean|array false if fails | array with inbox and shared inbox if successful
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_actor_inboxes_uri($url)
    {
        $client    = new HTTPClient();
        $headers   = [];
        $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 Exception('Invalid Actor URL.');
        }
        $res = json_decode($response->getBody(), true);
        if (self::validate_remote_response($res)) {
            return [
                'inbox' => $res['inbox'],
                'sharedInbox' => isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']
            ];
        }

        return false;
    }

    /**
     * Allows the Explorer to transverse a collection of persons.
     *
     * @param string $url
     * @return boolean
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function travel_collection($url)
    {
        $client    = new HTTPClient();
        $headers   = [];
        $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);
        $res = json_decode($response->getBody(), true);

        if (!isset($res['orderedItems'])) {
            return false;
        }

        foreach ($res["orderedItems"] as $profile) {
            if ($this->_lookup($profile) == false) {
                common_debug('ActivityPub Explorer: Found an invalid actor for '.$profile);
                // TODO: Invalid actor found, fallback to OStatus
            }
        }
        // Go through entire collection
        if (!is_null($res["next"])) {
            $this->_lookup($res["next"]);
        }

        return true;
    }

    /**
     * Get a remote user array from its URL (this function is only used for
     * profile updating and shall not be used for anything else)
     *
     * @param string $url User's url
     * @return mixed
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_remote_user_activity($url)
    {
        $client    = new HTTPClient();
        $headers   = [];
        $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);
        $res = json_decode($response->getBody(), true);
        if (Activitypub_explorer::validate_remote_response($res)) {
            common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url);
            return $res;
        }
        throw new Exception('ActivityPub Explorer: Failed to get activity.');
    }
}


code:
<?php
/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @category  Network
 * @package   Nautilus
 * @author    Aaron Parecki <aaron@parecki.com>
 * @license   http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
 * @link      https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
 */

class HttpSignature
{
    public static function sign($user, $url, $body = false, $addlHeaders = [])
    {
        $digest = false;
        if ($body) {
            $digest = self::_digest($body);
        }
        $headers = self::_headersToSign($url, $digest);
        $headers = array_merge($headers, $addlHeaders);
        $stringToSign = self::_headersToSigningString($headers);
        $signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
        $actor_private_key = new Activitypub_rsa();
        $actor_private_key = $actor_private_key->get_private_key($user);
        $key = openssl_pkey_get_private($actor_private_key);
        openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
        $signature = base64_encode($signature);
        $signatureHeader = 'keyId="' . ActivityPubPlugin::actor_uri($user).'#public-key' . '",headers="' . $signedHeaders . '",algorithm="rsa-sha256",signature="' . $signature . '"';
        unset($headers['(request-target)']);
        $headers['Signature'] = $signatureHeader;

        return self::_headersToCurlArray($headers);
    }

    private static function _digest($body)
    {
        if (is_array($body)) {
            $body = json_encode($body);
        }
        return base64_encode(hash('sha256', $body, true));
    }

    protected static function _headersToSign($url, $digest = false)
    {
        $date = new DateTime('UTC');

        $headers = [
            '(request-target)' => 'post ' . parse_url($url, PHP_URL_PATH),
            'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
            'Host' => parse_url($url, PHP_URL_HOST),
            'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json',
            'User-Agent'   => 'GNU social ActivityPub Plugin - https://gnu.io/social',
            'Content-Type' => 'application/activity+json'
        ];

        if ($digest) {
            $headers['Digest'] = 'SHA-256=' . $digest;
        }

        return $headers;
    }

    private static function _headersToSigningString($headers)
    {
        return implode("\n", array_map(function ($k, $v) {
            return strtolower($k) . ': ' . $v;
        }, array_keys($headers), $headers));
    }

    private static function _headersToCurlArray($headers)
    {
        return array_map(function ($k, $v) {
            return "$k: $v";
        }, array_keys($headers), $headers);
    }

    public static function parseSignatureHeader($signature)
    {
        $parts = explode(',', $signature);
        $signatureData = [];

        foreach ($parts as $part) {
            if (preg_match('/(.+)="(.+)"/', $part, $match)) {
                $signatureData[$match[1]] = $match[2];
            }
        }

        if (!isset($signatureData['keyId'])) {
            return [
                'error' => 'No keyId was found in the signature header. Found: ' . implode(', ', array_keys($signatureData))
            ];
        }

        if (!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
            return [
                'error' => 'keyId is not a URL: ' . $signatureData['keyId']
            ];
        }

        if (!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
            return [
                'error' => 'Signature is missing headers or signature parts'
            ];
        }

        return $signatureData;
    }

    public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
    {
        $digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true));
        $headersToSign = [];
        foreach (explode(' ', $signatureData['headers']) as $h) {
            if ($h == '(request-target)') {
                $headersToSign[$h] = 'post ' . $path;
            } elseif ($h == 'digest') {
                $headersToSign[$h] = $digest;
            } elseif (isset($inputHeaders[$h][0])) {
                $headersToSign[$h] = $inputHeaders[$h];
            }
        }
        $signingString = self::_headersToSigningString($headersToSign);

        $verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);

        return [$verified, $signingString];
    }
}


code:
<?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 Inbox Handler
 *
 * @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_inbox_handler
{
    private $activity;
    private $actor;
    private $object;

    /**
     * Create a Inbox Handler to receive something from someone.
     *
     * @param array $activity Activity we are receiving
     * @param Profile $actor_profile Actor originating the activity
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function __construct($activity, $actor_profile = null)
    {
        $this->activity = $activity;
        $this->object = $activity['object'];

        // Validate Activity
        $this->validate_activity();

        // Get Actor's Profile
        if (!is_null($actor_profile)) {
            $this->actor = $actor_profile;
        } else {
            $this->actor = ActivityPub_explorer::get_profile_from_url($this->activity['actor']);
        }

        // Handle the Activity
        $this->process();
    }

    /**
     * Validates if a given Activity is valid. Throws exception if not.
     *
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     * @throws Exception
     */
    private function validate_activity()
    {
        // Activity validation
        // Validate data
        if (!(isset($this->activity['type']))) {
            throw new Exception('Activity Validation Failed: Type was not specified.');
        }
        if (!isset($this->activity['actor'])) {
            throw new Exception('Activity Validation Failed: Actor was not specified.');
        }
        if (!isset($this->activity['object'])) {
            throw new Exception('Activity Validation Failed: Object was not specified.');
        }

        // Object validation
        switch ($this->activity['type']) {
            case 'Accept':
                Activitypub_accept::validate_object($this->object);
                break;
            case 'Create':
                Activitypub_create::validate_object($this->object);
                break;
            case 'Delete':
            case 'Follow':
            case 'Like':
            case 'Announce':
                if (!filter_var($this->object, FILTER_VALIDATE_URL)) {
                    throw new Exception('Object is not a valid Object URI for Activity.');
                }
                break;
            case 'Undo':
                Activitypub_undo::validate_object($this->object);
                break;
            default:
                throw new Exception('Unknown Activity Type.');
        }
    }

    /**
     * Sends the Activity to proper handler in order to be processed.
     *
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function process()
    {
        switch ($this->activity['type']) {
            case 'Accept':
                $this->handle_accept($this->actor, $this->object);
                break;
            case 'Create':
                $this->handle_create($this->actor, $this->object);
                break;
            case 'Delete':
                $this->handle_delete($this->actor, $this->object);
                break;
            case 'Follow':
                $this->handle_follow($this->actor, $this->object);
                break;
            case 'Like':
                $this->handle_like($this->actor, $this->object);
                break;
            case 'Undo':
                $this->handle_undo($this->actor, $this->object);
                break;
            case 'Announce':
                $this->handle_announce($this->actor, $this->object);
                break;
        }
    }

    /**
     * Handles an Accept Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_accept($actor, $object)
    {
        switch ($object['type']) {
            case 'Follow':
                $this->handle_accept_follow($actor, $object);
                break;
        }
    }

    /**
     * Handles an Accept Follow Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_accept_follow($actor, $object)
    {
        // Get valid Object profile
        $object_profile = new Activitypub_explorer;
        $object_profile = $object_profile->lookup($object['object'])[0];

        $pending_list = new Activitypub_pending_follow_requests($actor->getID(), $object_profile->getID());
        $pending_list->remove();
    }

    /**
     * Handles a Create Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_create($actor, $object)
    {
        switch ($object['type']) {
            case 'Note':
                Activitypub_notice::create_notice($object, $actor);
                break;
        }
    }

    /**
     * Handles a Delete Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws AuthorizationException
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_delete($actor, $object)
    {
        $notice = ActivityPubPlugin::grab_notice_from_url($object['object']);
        $notice->deleteAs($actor);
    }

    /**
     * Handles a Follow Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @throws \GuzzleHttp\Exception\GuzzleException
     * @throws \HttpSignatures\Exception
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_follow($actor, $object)
    {
        Activitypub_follow::follow($actor, $object);
    }

    /**
     * Handles a Like Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_like($actor, $object)
    {
        $notice = ActivityPubPlugin::grab_notice_from_url($object);
        Fave::addNew($actor, $notice);
    }

    /**
     * Handles a Undo Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_undo($actor, $object)
    {
        switch ($object['type']) {
            case 'Follow':
                $this->handle_undo_follow($actor, $object['object']);
                break;
            case 'Like':
                $this->handle_undo_like($actor, $object['object']);
                break;
        }
    }

    /**
     * Handles a Undo Like Activity received by our inbox.
     *
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws AlreadyFulfilledException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     */
    private function handle_undo_like($actor, $object)
    {
        $notice = ActivityPubPlugin::grab_notice_from_url($object);
        Fave::removeEntry($actor, $notice);
    }

    /**
     * Handles a Undo Follow Activity received by our inbox.
     *
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     */
    private function handle_undo_follow($actor, $object)
    {
        // Get Object profile
        $object_profile = new Activitypub_explorer;
        $object_profile = $object_profile->lookup($object)[0];

        if (Subscription::exists($actor, $object_profile)) {
            Subscription::cancel($actor, $object_profile);
        // You are no longer following this person.
        } else {
            // 409: You are not following this person already.
        }
    }

    /**
     * Handles a Announce Activity received by our inbox.
     *
     * @author Diogo Cordeiro <diogo@fc.upt.pt>
     * @param Profile $actor Actor
     * @param array $object Activity
     * @throws Exception
     */
    private function handle_announce($actor, $object)
    {
        $object_notice = ActivityPubPlugin::grab_notice_from_url($object);
        $object_notice->repeat($actor, 'ActivityPub');
    }
}


code:
<?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 $client;
    private $headers;

    /**
     * Create a postman to deliver something to someone
     *
     * @param Profile $from Profile of sender
     * @param $to
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function __construct($from, $to)
    {
        $this->actor = $from;
        $discovery = new Activitypub_explorer();
        $this->to = $to;
        $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
        foreach ($followers as $sub) {
            try {
                $to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
            } catch (Exception $e) {
                // Not an ActivityPub Remote Follower, let it go
            }
        }
        unset($discovery);

        $this->actor_uri = ActivityPubPlugin::actor_uri($this->actor);

        $this->client = new HTTPClient();
    }

    /**
     * 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(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl());
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody());

        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[0]->error)) {
            throw new Exception($res_body[0]->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(
                ActivityPubPlugin::actor_uri($this->actor),
                $this->to[0]->getUrl()
                    )
                );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody());

        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->remove();
            return true;
        }
        if (isset($res_body[0]->error)) {
            throw new Exception($res_body[0]->error);
        }
        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Accept 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 accept_follow()
    {
        $data = Activitypub_accept::accept_to_array(
            Activitypub_follow::follow_to_array(
                $this->to[0]->getUrl(),
                ActivityPubPlugin::actor_uri($this->actor)

                    )
               );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody());

        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->remove();
            return true;
        }
        if (isset($res_body[0]->error)) {
            throw new Exception($res_body[0]->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)
    {
        $data = Activitypub_like::like_to_array(
            ActivityPubPlugin::actor_uri($this->actor),
            $notice->getUrl()
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $this->send($data, $inbox);
        }
    }

    /**
     * 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(
                ActivityPubPlugin::actor_uri($this->actor),
                $notice->getUrl()
                         )
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $this->send($data, $inbox);
        }
    }

    /**
     * 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,
            Activitypub_notice::notice_to_array($notice)
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $this->send($data, $inbox);
        }
    }

    /**
     * Send a Announce notification to remote instances
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function announce($notice)
    {
        $data = Activitypub_announce::announce_to_array(
            ActivityPubPlugin::actor_uri($this->actor),
            $notice->getUri()
                        );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $this->send($data, $inbox);
        }
    }

    /**
     * Send a Delete notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function delete($notice)
    {
        $data = Activitypub_delete::delete_to_array(
            ActivityPubPlugin::actor_uri($notice->getProfile()),
            $notice->getUrl()
                );
        $errors = [];
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);
        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);
            if (!$res->getStatus() == 200) {
                $res_body = json_decode($res->getBody(), true);
                if (isset($res_body[0]['error'])) {
                    $errors[] = ($res_body[0]['error']);
                    continue;
                }
                $errors[] = ("An unknown error occurred.");
            }
        }
        if (!empty($errors)) {
            throw new Exception(json_encode($errors));
        }
    }

    /**
     * Clean list of inboxes to deliver messages
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return array To Inbox URLs
     */
    private function to_inbox()
    {
        $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);
    }
}