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 default_CURRENT_TIMESTAMP
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

    /**
     * Return table definition for Schema setup and DB_DataObject usage.
     *
     * @return array array of column definitions
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function schemaDef()
    {
        return [
            'fields' => [
                'uri' => ['type' => 'text', 'not null' => true],
                'profile_id' => ['type' => 'int', 'not null' => true],
                'inboxuri' => ['type' => 'text', 'not null' => true],
                'sharedInboxuri' => ['type' => 'text'],
                'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
                'modified' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
            ],
            'primary 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
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function profile_to_array($profile)
    {
        $uri = $profile->getUri();
        $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
     *
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @access public
     */
    public function do_insert()
    {
        // Does any other protocol have this remote entity we're about to add ?
        if (!Event::handle('StartTFNLookup', [$this->uri, get_class($this), &$profile_id])) {
            // Yes! Avoid creating a new profile
            $this->profile_id = $profile_id;
            $this->created = $this->modified = common_sql_now();

            if ($this->insert() === false) {
                $this->query('ROLLBACK');
                throw new ServerException('Cannot save ActivityPub profile.');
            }

            // Update existing profile with received data
            $profile = Profile::getKV('id', $profile_id);
            self::update_local_profile($profile, $this);

            // Ask TFN to handle profile duplication
            Event::handle('EndTFNLookup', [get_class($this), $profile_id]);
        } else {
            // No, create both a new profile and remote profile
            $profile = new Profile();
            $profile->created = $this->created = $this->modified = common_sql_now();
            self::update_local_profile($profile, $this);

            $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 get_called_class
     * @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
     *
     * @param Profile $profile
     * @return Activitypub_profile
     * @throws Exception if no Activitypub_profile exists for given Profile
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function from_profile(Profile $profile): Activitypub_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.');
            }
        }

        // extend the ap_profile with some information we
        // don't store in the database
        $fields = [
            'nickname' => 'nickname',
            'fullname' => 'fullname',
            'bio' => 'bio'
        ];

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

        return $aprofile;
    }

    public static function from_profile_collection(array $profiles): array
    {
        $ap_profiles = [];

        foreach ($profiles as $profile) {
            try {
                $ap_profiles[] = self::from_profile($profile);
            } catch (Exception $e) {
                // Don't mind local profiles
            }
        }

        return $ap_profiles;
    }

    /**
     * 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
     * @throws Exception
     * @throws 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
     *
     * @return string Inbox URL
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function get_inbox()
    {
        if (is_null($this->sharedInboxuri)) {
            return $this->inboxuri;
        }

        return $this->sharedInboxuri;
    }

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

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

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

    /**
     * Ensures a valid Activitypub_profile when provided with a valid URI.
     *
     * @param string $url
     * @param bool $grab_online whether to try online grabbing, defaults to true
     * @return Activitypub_profile
     * @throws Exception if it isn't possible to return an Activitypub_profile
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function fromUri($url, $grab_online = true)
    {
        try {
            return self::from_profile(Activitypub_explorer::get_profile_from_url($url, $grab_online));
        } 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.
     *
     * @param string $addr WebFinger address
     * @return Activitypub_profile
     * @throws Exception on error conditions
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @author GNU social
     */
    public static function ensure_webfinger($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(
            ['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 local profile with info from some AP profile
     *
     * @param Profile $profile
     * @param Activitypub_profile $aprofile
     * @return void
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function update_local_profile(Profile $profile, Activitypub_profile $aprofile): void
    {
        $fields = [
            'profileurl' => 'profileurl',
            'nickname' => 'nickname',
            'fullname' => 'fullname',
            'bio' => 'bio'
        ];

        $orig = clone($profile);

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

        if ($profile->id) {
            common_debug('Updating local Profile:' . $profile->id . ' from remote ActivityPub profile');
            $profile->modified = common_sql_now();
            $profile->update($orig);
        }
    }

    /**
     * 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 NoProfileException
     * @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 = $res['name'] ?? null;
        $aprofile->bio = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null;
        $aprofile->inboxuri = $res['inbox'];
        $aprofile->sharedInboxuri = $res['endpoints']['sharedInbox'] ?? $res['inbox'];
        $aprofile->profileurl = $res['url'] ?? $aprofile->uri;
        $aprofile->modified = common_sql_now();

        $profile = $aprofile->local_profile();

        // Profile
        self::update_local_profile($profile, $aprofile);
        $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;
    }

    /**
     * Update remote user profile URI in local instance
     *
     * @param string $uri
     * @return void
     * @throws Exception (if the update fails)
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public function updateUri(string $uri)
    {
        $orig = clone($this);
        $this->uri = $uri;
        $this->updateWithKeys($orig);
    }

    /**
     * Getter for the number of subscribers of a
     * given local profile
     *
     * @param Profile $profile profile object
     * @return int number of subscribers
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function subscriberCount(Profile $profile): int
    {
        $cnt = self::cacheGet(sprintf('activitypub_profile:subscriberCount:%d', $profile->id));

        if ($cnt !== false && is_int($cnt)) {
            return $cnt;
        }

        $sub = new Subscription();
        $sub->subscribed = $profile->id;
        $sub->whereAdd('subscriber != subscribed');
        $sub->whereAdd('subscriber IN (SELECT id FROM user UNION SELECT profile_id FROM activitypub_profile)');
        $cnt = $sub->count('distinct subscriber');

        self::cacheSet(sprintf('activitypub_profile:subscriberCount:%d', $profile->id), $cnt);

        return $cnt;
    }

    /**
     * Getter for the number of subscriptions of a
     * given local profile
     *
     * @param Profile $profile profile object
     * @return int number of subscriptions
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function subscriptionCount(Profile $profile): int
    {
        $cnt = self::cacheGet(sprintf('activitypub_profile:subscriptionCount:%d', $profile->id));

        if ($cnt !== false && is_int($cnt)) {
            return $cnt;
        }

        $sub = new Subscription();
        $sub->subscriber = $profile->id;
        $sub->whereAdd('subscriber != subscribed');
        $sub->whereAdd('subscribed IN (SELECT id FROM user UNION SELECT profile_id FROM activitypub_profile)');
        $cnt = $sub->count('distinct subscribed');

        self::cacheSet(sprintf('activitypub_profile:subscriptionCount:%d', $profile->id), $cnt);

        return $cnt;
    }

    public static function updateSubscriberCount(Profile $profile, $adder)
    {
        $cnt = self::cacheGet(sprintf('activitypub_profile:subscriberCount:%d', $profile->id));

        if ($cnt !== false && is_int($cnt)) {
            self::cacheSet(sprintf('activitypub_profile:subscriberCount:%d', $profile->id), $cnt + $adder);
        }
    }

    public static function updateSubscriptionCount(Profile $profile, $adder)
    {
        $cnt = self::cacheGet(sprintf('activitypub_profile:subscriptionCount:%d', $profile->id));

        if ($cnt !== false && is_int($cnt)) {
            self::cacheSet(sprintf('activitypub_profile:subscriptionCount:%d', $profile->id), $cnt + $adder);
        }
    }

    /**
     * Getter for the subscriber profiles of a
     * given local profile
     *
     * @param Profile $profile profile object
     * @param int $offset index of the starting row to fetch from
     * @param int $limit maximum number of rows allowed for fetching
     * @return array subscriber profile objects
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function getSubscribers(Profile $profile, $offset = 0, $limit = null): array
    {
        $cache = false;
        if ($offset + $limit <= Subscription::CACHE_WINDOW) {
            $subs = self::cacheGet(sprintf('activitypub_profile:subscriberCollection:%d', $profile->id));
            if ($subs !== false && is_array($subs)) {
                return array_slice($subs, $offset, $limit);
            }

            $cache = true;
        }

        $subs = Subscription::getSubscriberIDs($profile->id, $offset, $limit);
        $profiles = [];

        $users = User::multiGet('id', $subs);
        foreach ($users->fetchAll() as $user) {
            $profiles[$user->id] = $user->getProfile();
        }

        $ap_profiles = Activitypub_profile::multiGet('profile_id', $subs);
        foreach ($ap_profiles->fetchAll() as $ap) {
            $profiles[$ap->getID()] = $ap->local_profile();
        }

        if ($cache) {
            self::cacheSet(sprintf('activitypub_profile:subscriberCollection:%d', $profile->id), $profiles);
        }

        return $profiles;
    }

    /**
     * Getter for the subscribed profiles of a
     * given local profile
     *
     * @param Profile $profile profile object
     * @param int $offset index of the starting row to fetch from
     * @param int $limit maximum number of rows allowed for fetching
     * @return array subscribed profile objects
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function getSubscribed(Profile $profile, $offset = 0, $limit = null): array
    {
        $cache = false;
        if ($offset + $limit <= Subscription::CACHE_WINDOW) {
            $subs = self::cacheGet(sprintf('activitypub_profile:subscribedCollection:%d', $profile->id));
            if (is_array($subs)) {
                return array_slice($subs, $offset, $limit);
            }

            $cache = true;
        }

        $subs = Subscription::getSubscribedIDs($profile->id, $offset, $limit);
        try {
            $profiles = [];

            $users = User::multiGet('id', $subs);
            foreach ($users->fetchAll() as $user) {
                $profiles[$user->id] = $user->getProfile();
            }

            $ap_profiles = Activitypub_profile::multiGet('profile_id', $subs);
            foreach ($ap_profiles->fetchAll() as $ap) {
                $profiles[$ap->getID()] = $ap->local_profile();
            }
        } catch (NoResultException $e) {
            return $e->obj;
        }

        if ($cache) {
            self::cacheSet(sprintf('activitypub_profile:subscribedCollection:%d', $profile->id), $profiles);
        }

        return $profiles;
    }

    /**
     * Update cached values that are relevant to
     * the users involved in a subscription
     *
     * @param Profile $actor subscriber profile object
     * @param Profile $other subscribed profile object
     * @return void
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function subscribeCacheUpdate(Profile $actor, Profile $other)
    {
        self::blow('activitypub_profile:subscribedCollection:%d', $actor->getID());
        self::blow('activitypub_profile:subscriberCollection:%d', $other->id);
        self::updateSubscriptionCount($actor, +1);
        self::updateSubscriberCount($other, +1);
    }

    /**
     * Update cached values that are relevant to
     * the users involved in an unsubscription
     *
     * @param Profile $actor subscriber profile object
     * @param Profile $other subscribed profile object
     * @return void
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function unsubscribeCacheUpdate(Profile $actor, Profile $other)
    {
        self::blow('activitypub_profile:subscribedCollection:%d', $actor->getID());
        self::blow('activitypub_profile:subscriberCollection:%d', $other->id);
        self::updateSubscriptionCount($actor, -1);
        self::updateSubscriberCount($other, -1);
    }
}


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';
    public $profile_id;                      // int(4)  primary_key not_null
    public $private_key;                     // text()   not_null
    public $public_key;                      // text()   not_null
    public $created;                         // datetime()   not_null default_CURRENT_TIMESTAMP
    public $modified;                        // datetime()   not_null default_CURRENT_TIMESTAMP

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

    /**
     * Private key getter
     *
     * @param Profile $profile
     * @return string The private key
     * @throws Exception Throws exception if tries to fetch a private key of an actor we don't own
     */
    public function get_private_key(Profile $profile): string
    {
        $this->profile_id = $profile->getID();
        $apRSA = self::getKV('profile_id', $this->profile_id);
        if (!$apRSA instanceof Activitypub_rsa) {
            // Nonexistent key pair for this profile
            if ($profile->isLocal()) {
                self::generate_keys($this->private_key, $this->public_key);
                $this->store_keys();
                $apRSA->private_key = $this->private_key;
            } 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=true Should attempt to fetch keys from a remote profile?
     * @return string The public key
     * @throws ServerException It should never occur, but if so, we break everything!
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function ensure_public_key(Profile $profile, bool $fetch = true): string
    {
        $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();
                $apRSA->public_key = $this->public_key;
            } else {
                // ASSERT: This should never happen, but try to recover!
                common_log(LOG_ERR, "Activitypub_rsa: An impossible thing has happened... Please let the devs know that it entered in line 116 at Activitypub_rsa.php");
                if ($fetch) {
                    $res = Activitypub_explorer::get_remote_user_activity($profile->getUri());
                    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.
     *
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @access public
     */
    public function store_keys(): void
    {
        $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.
     *
     * @param string $private_key out
     * @param string $public_key out
     * @author PHP Manual Contributed Notes <dirt@awoms.com>
     */
    public static function generate_keys(?string &$private_key, ?string &$public_key): void
    {
        $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|Activitypub_profile $profile
     * @param string $public_key
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function update_public_key($profile, string $public_key): void
    {
        // 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();
        }
    }
}

repo_name: spookie/gnu

license: mit


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
// 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 queue handler for notice distribution
 *
 * @package   GNUsocial
 * @author    Bruno Casteleiro <brunoccast@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/**
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class ActivityPubQueueHandler extends QueueHandler
{
    /**
     * Getter of the queue transport name.
     *
     * @return string transport name
     */
    public function transport(): string
    {
        return 'activitypub';
    }

    /**
     * Notice distribution handler.
     *
     * @param Notice $notice notice to be distributed.
     * @return bool true on success, false otherwise
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function handle($notice): bool
    {
        if (!($notice instanceof Notice)) {
            common_log(LOG_ERR, "Got a bogus notice, not distributing");
            return true;
        }

        $profile = $notice->getProfile();

        if (!$profile->isLocal()) {
            return true;
        }

        // Ignore activity/non-post/share-verb notices
        $is_valid_verb = ($notice->verb == ActivityVerb::POST ||
                          $notice->verb == ActivityVerb::SHARE);

        if ($notice->source == 'activity' || !$is_valid_verb) {
            common_log(LOG_ERR, "Ignoring distribution of notice:{$notice->id}: activity source or invalid Verb");
            return true;
        }

        $other = Activitypub_profile::from_profile_collection(
            $notice->getAttentionProfiles()
        );

        // Handling a reply?
        if ($notice->reply_to) {
            try {
                $parent_notice = $notice->getParent();

                try {
                    $other[] = Activitypub_profile::from_profile($parent_notice->getProfile());
                } catch (Exception $e) {
                    // Local user can be ignored
                }

                foreach ($parent_notice->getAttentionProfiles() as $mention) {
                    try {
                        $other[] = Activitypub_profile::from_profile($mention);
                    } catch (Exception $e) {
                        // Local user can be ignored
                    }
                }
            } catch (NoParentNoticeException $e) {
                // This is not a reply to something (has no parent)
            } catch (NoResultException $e) {
                // Parent author's profile not found! Complain louder?
                common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage());
            }
        }

        // Handling an Announce?
        if ($notice->isRepeat()) {
            $repeated_notice = Notice::getKV('id', $notice->repeat_of);
            if ($repeated_notice instanceof Notice) {
                $other = array_merge($other,
                                     Activitypub_profile::from_profile_collection(
                                         $repeated_notice->getAttentionProfiles()
                                     ));

                try {
                    $other[] = Activitypub_profile::from_profile($repeated_notice->getProfile());
                } catch (Exception $e) {
                    // Local user can be ignored
                }

                // That was it
                $postman = new Activitypub_postman($profile, $other);
                $postman->announce($repeated_notice);
            }

            // either made the announce or found nothing to repeat
            return true;
        }

        // That was it
        $postman = new Activitypub_postman($profile, $other);
        $postman->create_note($notice);
        return true;
    }
}


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.
     *
     * @param string $url
     * @param bool $grab_online whether to try online grabbing, defaults to true
     * @return Profile
     * @throws HTTP_Request2_Exception Network issues
     * @throws NoProfileException This won't happen
     * @throws Exception Invalid request
     * @throws ServerException Error storing remote actor
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_profile_from_url($url, $grab_online = true)
    {
        $discovery = new Activitypub_explorer();
        // Get valid Actor object
        $actor_profile = $discovery->lookup($url, $grab_online);
        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
     * @param bool $grab_online whether to try online grabbing, defaults to true
     * @return array of Profile objects
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws Exception
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function lookup(string $url, bool $grab_online = true)
    {
        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, $grab_online);
    }

    /**
     * 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
     * @param bool $grab_online whether to try online grabbing, defaults to true
     * @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(string $url, bool $grab_online = true)
    {
        $grab_local = $this->grab_local_user($url);

        // First check if we already have it locally and, if so, return it.
        // If the local fetch fails and remote grab is required: store locally and return.
        if (!$grab_local && (!$grab_online || !$this->grab_remote_user($url))) {
            throw new Exception('User not found.');
        }

        return $this->discovered_actor_profiles;
    }

    /**
     * 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 bool 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.');
            $all_ids = LRDDPlugin::grab_profile_aliases($uri);
        } else {
            common_debug('ActivityPub Explorer: Searching locally for ' . $uri . ' offline.');
            $all_ids = [$uri];
        }

        if (is_null($all_ids)) {
            common_debug('AcvitityPub Explorer: Unable to find a local profile for ' . $uri);
            return false;
        }

        foreach ($all_ids as $alias) {
            // Try standard ActivityPub route
            // Is this a known filthy little mudblood?
            $aprofile = self::get_aprofile_by_url($alias);
            if ($aprofile instanceof Activitypub_profile) {
                common_debug('ActivityPub Explorer: Found a local Aprofile for ' . $alias);

                // double check to confirm this alias as a legitimate one
                if ($online) {
                    common_debug('ActivityPub Explorer: Double-checking ' . $alias . ' to confirm it as a legitimate alias');

                    $disco = new Discovery();
                    $xrd = $disco->lookup($aprofile->getUri());
                    $doublecheck_aliases = array_merge(array($xrd->subject), $xrd->aliases);

                    if (in_array($uri, $doublecheck_aliases)) {
                        // the original URI is present, we're sure now!
                        // update aprofile's URI and proceed
                        common_debug('ActivityPub Explorer: ' . $alias . ' is a legitimate alias');
                        $aprofile->updateUri($uri);
                    } else {
                        common_debug('ActivityPub Explorer: ' . $alias . ' is not an alias we can trust');
                        continue;
                    }
                }

                // Assert: This AProfile has a Profile, no try catch.
                $profile = $aprofile->local_profile();
                // We found something!
                $this->discovered_actor_profiles[] = $profile;
                return true;
            } else {
                common_debug('ActivityPub Explorer: Unable to find a local Aprofile for ' . $alias . ' - looking for a Profile instead.');
                // Well, maybe it is a pure blood?
                // Iff, we are in the same instance:
                $ACTIVITYPUB_BASE_ACTOR_URI = common_local_url('userbyid', ['id' => null], null, null, false, true); // @FIXME: Could this be too hardcoded?
                $ACTIVITYPUB_BASE_ACTOR_URI_length = strlen($ACTIVITYPUB_BASE_ACTOR_URI);
                if (substr($alias, 0, $ACTIVITYPUB_BASE_ACTOR_URI_length) === $ACTIVITYPUB_BASE_ACTOR_URI) {
                    try {
                        $profile = Profile::getByID((int)substr($alias, $ACTIVITYPUB_BASE_ACTOR_URI_length));
                        common_debug('ActivityPub Explorer: Found a Profile for ' . $alias);
                        // We found something!
                        $this->discovered_actor_profiles[] = $profile;
                        return true;
                    } catch (Exception $e) {
                        // Let the exception go on its merry way.
                        common_debug('ActivityPub Explorer: Unable to find a Profile for ' . $alias);
                    }
                }
            }
        }

        // 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 bool success state
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @throws Exception
     * @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);
        $client = new HTTPClient();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
        $res = json_decode($response->getBody(), true);

        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));
        }

        return false;
    }

    /**
     * Save remote user profile in local instance
     *
     * @param array $res remote response
     * @return Profile remote Profile object
     * @throws NoProfileException
     * @throws ServerException
     * @throws Exception
     * @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 = $res['name'] ?? null;
        $aprofile->bio = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null;
        $aprofile->inboxuri = $res['inbox'];
        $aprofile->sharedInboxuri = $res['endpoints']['sharedInbox'] ?? $res['inbox'];
        $aprofile->profileurl = $res['url'] ?? $aprofile->uri;

        $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
     *
     * @param Profile $profile
     * @param string $url
     * @return Avatar    The Avatar we have on disk.
     * @throws Exception in various failure cases
     * @author GNU social
     */
    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
     *
     * @param array $res remote response
     * @return bool success state
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    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)
     *
     * @param string $v URL
     * @return bool|Activitypub_profile false if fails | Aprofile object if successful
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    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 bool|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();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_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 bool
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function travel_collection($url)
    {
        $client = new HTTPClient();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_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->travel_collection($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 array
     * @throws Exception Either network issues or unsupported Activity format
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_remote_user_activity($url)
    {
        $client = new HTTPClient();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_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
{
    /**
     * Sign a message with an Actor
     *
     * @param Profile $user Actor signing
     * @param string $url Inbox url
     * @param string|bool $body Data to sign (optional)
     * @param array $addlHeaders Additional headers (optional)
     * @return array Headers to be used in curl
     * @throws Exception Attempted to sign something that belongs to an Actor we don't own
     */
    public static function sign(Profile $user, string $url, $body = false, array $addlHeaders = []): array
    {
        $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();
        // Intentionally unhandled exception, we want this to explode if that happens as it would be a bug
        $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="' . $user->getUri() . '#public-key' . '",headers="' . $signedHeaders . '",algorithm="rsa-sha256",signature="' . $signature . '"';
        unset($headers['(request-target)']);
        $headers['Signature'] = $signatureHeader;

        return self::_headersToCurlArray($headers);
    }

    /**
     * @param mixed $body
     * @return string
     */
    private static function _digest($body): string
    {
        if (is_array($body)) {
            $body = json_encode($body);
        }
        return base64_encode(hash('sha256', $body, true));
    }

    /**
     * @param string $url
     * @param mixed $digest
     * @return array
     * @throws Exception
     */
    protected static function _headersToSign(string $url, $digest = false): array
    {
        $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 - '.GNUSOCIAL_ENGINE_URL,
            'Content-Type' => 'application/activity+json'
        ];

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

        return $headers;
    }

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

    /**
     * @param array $headers
     * @return array
     */
    private static function _headersToCurlArray(array $headers): array
    {
        return array_map(function ($k, $v) {
            return "$k: $v";
        }, array_keys($headers), $headers);
    }

    /**
     * @param string $signature
     * @return array
     */
    public static function parseSignatureHeader(string $signature): array
    {
        $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;
    }

    /**
     * @param $publicKey
     * @param $signatureData
     * @param $inputHeaders
     * @param $path
     * @param $body
     * @return array
     */
    public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body): array
    {
        $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
        if (!$this->validate_activity()) {
            return; // Just ignore
        }

        // 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.
     *
     * @throws Exception if invalid
     * @return bool true if valid and acceptable, false if unsupported
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function validate_activity(): bool
    {
        // 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
        $valid = true;
        switch ($this->activity['type']) {
            case 'Accept':
                $valid = Activitypub_accept::validate_object($this->object);
                break;
            case 'Create':
                $valid = Activitypub_create::validate_object($this->object);
                break;
            case 'Delete':
                $valid = Activitypub_delete::validate_object($this->object);
                break;
            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':
                $valid = Activitypub_undo::validate_object($this->object);
                break;
            default:
                throw new Exception('Unknown Activity Type.');
        }

        return $valid;
    }

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

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

    /**
     * Handles an Accept Follow Activity received by our inbox.
     *
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_accept_follow()
    {
        // Get valid Object profile
        // Note that, since this an accept_follow, the $object
        // profile is actually the actor that followed someone
        $object_profile = new Activitypub_explorer;
        $object_profile = $object_profile->lookup($this->object['object'])[0];

        Activitypub_profile::subscribeCacheUpdate($object_profile, $this->actor);

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

    /**
     * Handles a Create Activity received by our inbox.
     *
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_create()
    {
        switch ($this->object['type']) {
            case 'Note':
                $this->handle_create_note();
                break;
        }
    }

    /**
     * Handle a Create Note Activity received by our inbox.
     *
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    private function handle_create_note()
    {
        if (Activitypub_create::isPrivateNote($this->activity)) {
            Activitypub_message::create_message($this->object, $this->actor);
        } else {
            Activitypub_notice::create_notice($this->object, $this->actor);
        }
    }

    /**
     * Handles a Delete Activity received by our inbox.
     *
     * @throws NoProfileException
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    private function handle_delete()
    {
        $object = $this->object;
        if (is_array($object)) {
            $object = $object['id'];
        }

        // profile deletion ?
        if ($this->activity['actor'] == $object) {
            $aprofile = Activitypub_profile::from_profile($this->actor);
            $this->handle_delete_profile($aprofile);
            return;
        }

        // note deletion ?
        try {
            $notice = ActivityPubPlugin::grab_notice_from_url($object, false);
            if ($notice instanceof Notice) {
                $this->handle_delete_note($notice);
            }
            return;
        } catch (Exception $e) {
            // either already deleted or not an object at all
            // nothing to do..
        }

        common_log(LOG_INFO, "Ignoring Delete activity, nothing that we can/need to handle.");
    }

    /**
     * Handles a Delete-Profile Activity.
     *
     * Note that the actual ap_profile is deleted during the ProfileDeleteRelated event,
     * subscribed by ActivityPubPlugin.
     *
     * @param Activitypub_profile $aprofile remote user being deleted
     * @return void
     * @throws NoProfileException
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    private function handle_delete_profile(Activitypub_profile $aprofile): void
    {
        $profile = $aprofile->local_profile();
        $profile->delete();
    }

    /**
     * Handles a Delete-Note Activity.
     *
     * @param Notice $note remote note being deleted
     * @return void
     * @throws AuthorizationException
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    private function handle_delete_note(Notice $note): void
    {
        $note->deleteAs($this->actor);
    }

    /**
     * Handles a Follow Activity received by our inbox.
     *
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_follow()
    {
        Activitypub_follow::follow($this->actor, $this->object, $this->activity['id']);
    }

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

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

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

        if (Subscription::exists($this->actor, $object_profile)) {
            Subscription::cancel($this->actor, $object_profile);
            // You are no longer following this person.
            Activitypub_profile::unsubscribeCacheUpdate($this->actor, $object_profile);
        } /*else {
            // 409: You already aren't following this person.
        }*/
    }

    /**
     * Handles a Undo Like Activity received by our inbox.
     *
     * @throws AlreadyFulfilledException
     * @throws ServerException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_undo_like()
    {
        $notice = ActivityPubPlugin::grab_notice_from_url($this->object['object']);
        Fave::removeEntry($this->actor, $notice);
    }

    /**
     * Handles a Announce Activity received by our inbox.
     *
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_announce()
    {
        $object_notice = ActivityPubPlugin::grab_notice_from_url($this->object);
        $object_notice->repeat($this->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 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_accept
{
    /**
     * Generates an ActivityPub representation of a Accept
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function accept_to_array($object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => common_root_url().'accept_follow_from_'.urlencode($object['actor']).'_to_'.urlencode($object['object']),
            'type'   => 'Accept',
            'actor'  => $object['object'],
            'object' => $object
        ];
        return $res;
    }

    /**
     * Verifies if a given object is acceptable for an Accept 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 Accept Activity.');
        }
        if (!isset($object['type'])) {
            throw new Exception('Object type was not specified for Accept Activity.');
        }
        switch ($object['type']) {
            case 'Follow':
                // 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 Accept Activity.');
        }
        return true;
    }
}


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_announce
{
    /**
     * Generates an ActivityPub representation of a Announce
     *
     * @param Profile $actor
     * @param Notice $notice
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function announce_to_array(Profile $actor, Notice $notice): array
    {
        $actor_uri = $actor->getUri();
        $notice_url = Activitypub_notice::getUrl($notice);

        $to = [common_local_url('apActorFollowers', ['id' => $actor->getID()])];
        foreach ($notice->getAttentionProfiles() as $to_profile) {
            $to[] = $to_profile->getUri();
        }

        $cc[]= 'https://www.w3.org/ns/activitystreams#Public';

        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => common_root_url().'share_from_'.urlencode($actor_uri).'_to_'.urlencode($notice_url),
            "type"   => "Announce",
            "actor"  => $actor_uri,
            "object" => $notice_url,
            "to"     => $to,
            "cc"     => $cc
        ];
        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 Attachment 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_attachment
{
    /**
     * Generates a pretty array from an Attachment object
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param Attachment $attachment
     * @return array pretty array to be used in a response
     */
    public static function attachment_to_array($attachment)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'type'      => 'Document',
            'mediaType' => $attachment->mimetype,
            'url'       => $attachment->getUrl(),
            'size'      => $attachment->getSize(),
            'name'      => $attachment->getTitle(),
        ];

        // Image
        if (substr($res["mediaType"], 0, 5) == "image") {
            $res["meta"]= [
                'width'  => $attachment->width,
                'height' => $attachment->height
            ];
        }

        return $res;
    }
}