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(string $url, bool $grab_online = true): Profile
    {
        $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): array
    {
        $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(string $uri, bool $online = false): bool
    {
        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(string $url): bool
    {
        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 ($response->getStatus() == 410) { // If it was deleted
            return true; // Nothing to add.
        } elseif (!$response->isOk()) { // If it is unavailable
            return false; // Try to add at another time.
        }
        if (is_null($res)) {
            common_debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getBody());
            return true; // Nothing to add.
        }

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

        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(array $res): Profile
    {
        // 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;
    }

    /**
     * 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(array $res): bool
    {
        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(string $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 to validate the answer | array with inbox and shared inbox if successful
     * @throws HTTP_Request2_Exception
     * @throws Exception If an irregular error happens (status code, body format or GONE)
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_actor_inboxes_uri(string $url)
    {
        $client = new HTTPClient();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
        if ($response->getStatus() == 410) { // If it was deleted
            throw new Exception('This actor is GONE.');
        } elseif (!$response->isOk()) { // If it is unavailable
            throw new Exception('Non Ok Status Code for given Actor URL.');
        }
        $res = json_decode($response->getBody(), true);
        if (is_null($res)) { // If it is in an unexpected format
            common_debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getBody());
            throw new Exception('Given Actor URL didn\'t return a valid JSON.');
        }
        if (self::validate_remote_response($res)) {
            return [
                'inbox' => $res['inbox'],
                'sharedInbox' => isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']
            ];
        }

        return false;
    }

    /**
     * Download and update given avatar image
     * TODO: Avoid updating an avatar if its URL didn't change. (this is something OStatus already does)
     * TODO: Should be in AProfile instead?
     *
     * @param Profile $profile
     * @param string $url
     * @return Avatar    The Avatar we have on disk. (seldom used)
     * @throws Exception in various failure cases
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function update_avatar(Profile $profile, string $url): Avatar
    {
        common_debug('ActivityPub Explorer: Started grabbing remote avatar from: ' . $url);
        // ImageFile throws exception if something goes wrong, which we'll let go on its merry way
        $imagefile = ImageFile::fromURL($url);

        $id = $profile->getID();

        $type = $imagefile->preferredType();
        $filename = Avatar::filename(
            $id,
            image_type_to_extension($type),
            null,
            'tmp' . common_timestamp()
        );

        $filepath = Avatar::path($filename);
        /*$imagefile = */$imagefile->copyTo($filepath);

        common_debug('ActivityPub Explorer: Stored avatar in: ' . $filepath);

        // XXX: Do we need this?
        chmod($filepath, 0644);

        $profile->setOriginal($filename);

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

    /**
     * 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(string $url): bool
    {
        $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|false If it is able to fetch, false if it's gone
     * @throws Exception Either network issues or unsupported Activity format
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function get_remote_user_activity(string $url)
    {
        $client = new HTTPClient();
        $response = $client->get($url, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
        // If it was deleted
        if ($response->getStatus() == 410) {
            return false;
        } elseif (!$response->isOk()) { // If it is unavailable
            throw new Exception('Non Ok Status Code for given Actor URL.');
        }
        $res = json_decode($response->getBody(), true);
        if (is_null($res)) {
            common_debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getBody());
            throw new Exception('Given Actor URL didn\'t return a valid JSON.');
        }
        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
    {
        // We need this because the used Request headers fields specified by Signature are in lower case.
        $headersContent = array_change_key_case($inputHeaders, CASE_LOWER);
        $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($headersContent[$h][0])) {
                $headersToSign[$h] = $headersContent[$h];
            }
        }
        $signingString = self::_headersToSigningString($headersToSign);

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

        return [$verified, $signingString];
    }
}

repo_name: ksuudilla/gnu

license: mit


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub 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>
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function handle_delete()
    {
        $object = $this->object;
        if (is_string($object)) {
            $client = new HTTPClient();
            $response = $client->get($object, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
            $gone = !$response->isOk();
            if (!$gone) { // It's not gone, we're updating it.
                $object = json_decode($response->getBody(), true);
                switch ($object['type']) {
                    case 'Person':
                        try {
                            // Update profile if we already have a copy of it
                            $aprofile = Activitypub_profile::fromUri($object['id'], false);
                            Activitypub_profile::update_profile($aprofile, $object);
                        } catch (Exception $e) {
                            // Import profile if we don't
                            Activitypub_explorer::get_profile_from_url($object['id']);
                        }
                        break;
                    case 'Tombstone':
                        try {
                            $notice = ActivityPubPlugin::grab_notice_from_url($object, false);
                            if ($notice instanceof Notice) {
                                $notice->delete();
                            }
                            return;
                        } catch (Exception $e) {
                            // either already deleted or not an object at all
                            // nothing to do..
                        }
                        break;
                    case 'Note':
                        // XXX: We do not support updating a note's contents so, we'll ignore it for now...
                    default:
                        common_log(LOG_INFO, "Ignoring Delete activity, we do not understand for {$object['type']}.");
                }
            }
        } else {
            // We don't know the type of the deleted object :(
            // Nor if it's gone or not.
            try {
                if (is_array($object)) {
                    $object = $object['id'];
                }
                $aprofile = Activitypub_profile::fromUri($object, false);
                $res = Activitypub_explorer::get_remote_user_activity($object);
                Activitypub_profile::update_profile($aprofile, $res);
                return;
            } catch (Exception $e) {
                // Means this wasn't a profile
            }

            try {
                $client = new HTTPClient();
                $response = $client->get($object, ACTIVITYPUB_HTTP_CLIENT_HEADERS);
                // If it was deleted
                if ($response->getStatus() == 410) {
                    $notice = ActivityPubPlugin::grab_notice_from_url($object, false);
                    if ($notice instanceof Notice) {
                        $notice->delete();
                    }
                } else {
                    // We can't update a note's contents so, we'll ignore it for now...
                }
                return;
            } catch (Exception $e) {
                // Means we didn't have this note already
            }

            return;
        }
    }

    /**
     * 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;
    }
}


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_create
{
    /**
     * Generates an ActivityPub representation of a Create
     *
     * @param string $actor
     * @param array $object
     * @param bool $directMessage whether it is a private Create activity or not
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function create_to_array(string $actor, array $object, bool $directMessage = false): array
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id' => $object['id'] . '/create',
            'type' => 'Create',
            'directMessage' => $directMessage,
            'to' => $object['to'],
            'cc' => $object['cc'],
            'actor' => $actor,
            'object' => $object
        ];
        return $res;
    }

    /**
     * Verifies if a given object is acceptable for a Create Activity.
     *
     * @param array $object
     * @return bool True if acceptable, false if valid but unsupported
     * @throws Exception if invalid
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function validate_object($object): bool
    {
        if (!is_array($object)) {
            common_debug('ActivityPub Create Validator: Rejected because of invalid Object format.');
            throw new Exception('Invalid Object Format for Create Activity.');
        }
        if (!isset($object['type'])) {
            common_debug('ActivityPub Create Validator: Rejected because of Type.');
            throw new Exception('Object type was not specified for Create Activity.');
        }
        if (isset($object['directMessage']) && !is_bool($object['directMessage'])) {
            common_debug('ActivityPub Create Validator: Rejected because Object directMessage is invalid.');
            throw new Exception('Invalid Object directMessage.');
        }
        switch ($object['type']) {
            case 'Note':
                // Validate data
                return Activitypub_notice::validate_note($object);
                break;
            default:
                throw new Exception('This is not a supported Object Type for Create Activity.');
        }
        return true;
    }

    /**
     * Verify if received note is private (direct).
     * Note that we're conformant with the (yet) non-standard directMessage attribute:
     * https://github.com/w3c/activitypub/issues/196#issuecomment-304958984
     *
     * @param array $activity received Create-Note activity
     * @return bool true if note is private, false otherwise
     * @author Bruno casteleiro <brunoccast@fc.up.pt>
     */
    public static function isPrivateNote(array $activity): bool
    {
        if (isset($activity['directMessage'])) {
            return $activity['directMessage'];
        }

        return empty($activity['cc']) && !in_array('https://www.w3.org/ns/activitystreams#Public', $activity['to']);
    }
}


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_delete
{
    /**
     * Generates an ActivityPub representation of a Delete
     *
     * @param string $actor actor URI
     * @param string $object object URI
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function delete_to_array(string $actor, string $object): array
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'       => $object.'/delete',
            'type'     => 'Delete',
            'to'       => ['https://www.w3.org/ns/activitystreams#Public'],
            'actor'    => $actor,
            'object'   => $object
        ];
        return $res;
    }

    /**
     * Verifies if a given object is acceptable for a Delete Activity.
     *
     * @param array|string $object
     * @return bool
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function validate_object($object): bool
    {
        if (!is_array($object)) {
            if (!filter_var($object, FILTER_VALIDATE_URL)) {
                throw new Exception('Object is not a valid Object URI for Activity.');
            }
        } else {
            if (!isset($object['type'])) {
                throw new Exception('Object type was not specified for Delete Activity.');
            }
            if ($object['type'] !== "Tombstone" && $object['type'] !== "Person") {
                throw new Exception('Invalid Object type for Delete Activity.');
            }
            if (!isset($object['id'])) {
                throw new Exception('Object id was not specified for Delete 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_error
{
    /**
     * Generates a pretty error from a string
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $m
     * @return array pretty array to be used in a response
     */
    public static function error_message_to_array($m)
    {
        $res = [
            'error'=> $m
        ];
        return $res;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub error representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_follow
{
    /**
     * Generates an ActivityPub representation of a subscription
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $actor
     * @param string $object
     * @param string|null $id Activity id, to be used when generating for an Accept Activity
     * @return array pretty array to be used in a response
     */
    public static function follow_to_array(string $actor, string $object, ?string $id = null): array
    {
        if ($id === null) {
            $id = common_root_url().'follow_from_'.urlencode($actor).'_to_'.urlencode($object);
        }

        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => $id,
            'type'   => 'Follow',
            'actor'  => $actor,
            'object' => $object
       ];
        return $res;
    }

    /**
     * Handles a Follow Activity received by our inbox.
     *
     * @param Profile $actor_profile Remote Actor
     * @param string $object Local Actor
     * @param string $id Activity id
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function follow(Profile $actor_profile, string $object, string $id)
    {
        // Get Actor's Aprofile
        $actor_aprofile = Activitypub_profile::from_profile($actor_profile);

        // Get Object profile
        $object_profile = new Activitypub_explorer;
        $object_profile = $object_profile->lookup($object)[0];

        if (!Subscription::exists($actor_profile, $object_profile)) {
            Subscription::start($actor_profile, $object_profile);
            Activitypub_profile::subscribeCacheUpdate($actor_profile, $object_profile);
            common_debug('ActivityPubPlugin: Accepted Follow request from '.$actor_profile->getUri().' to '.$object);
        } else {
            common_debug('ActivityPubPlugin: Received a repeated Follow request from '.$actor_profile->getUri().' to '.$object);
        }

        // Notify remote instance that we have accepted their request
        common_debug('ActivityPubPlugin: Notifying remote instance that we have accepted their Follow request request from '.$actor_profile->getUri().' to '.$object);
        $postman = new Activitypub_postman($object_profile, [$actor_aprofile]);
        $postman->accept_follow($id);
    }
}


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_like
{
    /**
     * Generates an ActivityPub representation of a Like
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $actor  Actor URI
     * @param string $object Notice URI
     * @return array pretty array to be used in a response
     */
    public static function like_to_array($actor, $object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => common_root_url().'like_from_'.urlencode($actor).'_to_'.urlencode($object),
            "type"   => "Like",
            "actor"  => $actor,
            "object" => $object
        ];
        return $res;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub Mention Tag 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_mention_tag
{
    /**
     * Generates an ActivityPub representation of a Mention Tag
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $href Actor Uri
     * @param string $name Mention name
     * @return array pretty array to be used in a response
     */
    public static function mention_tag_to_array_from_values(string $href, string $name): array
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            "type" => "Mention",
            "href" => $href,
            "name" => $name
         ];
        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 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();

/**
 * ActivityPub direct note representation
 *
 * @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
 */
class Activitypub_message
{
    /**
     * Generates a pretty message from a Notice object
     *
     * @param Notice $message
     * @return array array to be used in a response
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function message_to_array(Notice $message): array
    {
        $from = $message->getProfile();

        $tags = [];
        foreach ($message->getTags() as $tag) {
            if ($tag != "") { // Hacky workaround to avoid stupid outputs
                $tags[] = Activitypub_tag::tag_to_array($tag);
            }
        }

        $to = [];
        foreach ($message->getAttentionProfiles() as $to_profile) {
            $to[]   = $href = $to_profile->getUri();
            $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST));
        }

        $item = [
            '@context'      => 'https://www.w3.org/ns/activitystreams',
            'id'            => common_local_url('showmessage', ['message' => $message->getID()]),
            'type'          => 'Note',
            'published'     => str_replace(' ', 'T', $message->created).'Z',
            'attributedTo'  => $from->getUri(),
            'to'            => $to,
            'cc'            => [],
            'content'       => $message->getRendered(),
            'attachment'    => [],
            'tag'           => $tags
        ];

        return $item;
    }

    /**
     * Create a private Notice via ActivityPub Note Object.
     * Returns created Notice.
     *
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     * @param array $object
     * @param Profile $actor_profile
     * @return Notice
     * @throws Exception
     */
    public static function create_message(array $object, Profile $actor_profile = null): Notice
    {
        return Activitypub_notice::create_notice($object, $actor_profile, 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 notice 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_notice
{
    /**
     * Generates a pretty notice from a Notice object
     *
     * @param Notice $notice
     * @return array array to be used in a response
     * @throws EmptyPkeyValueException
     * @throws InvalidUrlException
     * @throws ServerException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function notice_to_array($notice)
    {
        $profile = $notice->getProfile();
        $attachments = [];
        foreach ($notice->attachments() as $attachment) {
            $attachments[] = Activitypub_attachment::attachment_to_array($attachment);
        }

        $tags = [];
        foreach ($notice->getTags() as $tag) {
            if ($tag != "") {       // Hacky workaround to avoid stupid outputs
                $tags[] = Activitypub_tag::tag_to_array($tag);
            }
        }

        if ($notice->isPublic()) {
            $to = ['https://www.w3.org/ns/activitystreams#Public'];
            $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])];
        } else {
            // Since we currently don't support sending unlisted/followers-only
            // notices, arriving here means we're instead answering to that type
            // of posts. Not having subscription policy working, its safer to
            // always send answers of type unlisted.
            $to = [];
            $cc = ['https://www.w3.org/ns/activitystreams#Public'];
        }

        foreach ($notice->getAttentionProfiles() as $to_profile) {
            $to[] = $href = $to_profile->getUri();
            $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname() . '@' . parse_url($href, PHP_URL_HOST));
        }

        $item = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id' => self::getUrl($notice),
            'type' => 'Note',
            'published' => str_replace(' ', 'T', $notice->getCreated()) . 'Z',
            'url' => self::getUrl($notice),
            'attributedTo' => $profile->getUri(),
            'to' => $to,
            'cc' => $cc,
            'conversation' => $notice->getConversationUrl(),
            'content' => $notice->getRendered(),
            'isLocal' => $notice->isLocal(),
            'attachment' => $attachments,
            'tag' => $tags
        ];

        // Is this a reply?
        if (!empty($notice->reply_to)) {
            $item['inReplyTo'] = self::getUrl(Notice::getById($notice->reply_to));
        }

        // Do we have a location for this notice?
        try {
            $location = Notice_location::locFromStored($notice);
            $item['latitude'] = $location->lat;
            $item['longitude'] = $location->lon;
        } catch (Exception $e) {
            // Apparently no.
        }

        return $item;
    }

    /**
     * Create a Notice via ActivityPub Note Object.
     * Returns created Notice.
     *
     * @param array $object
     * @param Profile $actor_profile
     * @param bool $directMessage
     * @return Notice
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function create_notice(array $object, Profile $actor_profile, bool $directMessage = false): Notice
    {
        $id = $object['id'];                                 // int
        $url = isset($object['url']) ? $object['url'] : $id; // string
        $content = $object['content'];                       // string

        // possible keys: ['inReplyTo', 'latitude', 'longitude']
        $settings = [];
        if (isset($object['inReplyTo'])) {
            $settings['inReplyTo'] = $object['inReplyTo'];
        }
        if (isset($object['latitude'])) {
            $settings['latitude'] = $object['latitude'];
        }
        if (isset($object['longitude'])) {
            $settings['longitude'] = $object['longitude'];
        }

        $act = new Activity();
        $act->verb = ActivityVerb::POST;
        $act->time = time();
        $act->actor = $actor_profile->asActivityObject();
        $act->context = new ActivityContext();
        $options = ['source' => 'ActivityPub',
            'uri' => $id,
            'url' => $url,
            'is_local' => self::getNotePolicyType($object, $actor_profile)];

        if ($directMessage) {
            $options['scope'] = Notice::MESSAGE_SCOPE;
        }

        // Is this a reply?
        if (isset($settings['inReplyTo'])) {
            try {
                $inReplyTo = ActivityPubPlugin::grab_notice_from_url($settings['inReplyTo']);
                $act->context->replyToID = $inReplyTo->getUri();
                $act->context->replyToUrl = $inReplyTo->getUrl();
            } catch (Exception $e) {
                // It failed to grab, maybe we got this note from another source
                // (e.g.: OStatus) that handles this differently or we really
                // failed to get it...
                // Welp, nothing that we can do about, let's
                // just fake we don't have such notice.
            }
        } else {
            $inReplyTo = null;
        }

        // Mentions
        $mentions = [];
        if (isset($object['tag']) && is_array($object['tag'])) {
            foreach ($object['tag'] as $tag) {
                if (array_key_exists('type', $tag) && $tag['type'] == 'Mention') {
                    $mentions[] = $tag['href'];
                }
            }
        }
        $mentions_profiles = [];
        $discovery = new Activitypub_explorer;
        foreach ($mentions as $mention) {
            try {
                $mentioned_profile = $discovery->lookup($mention);
                if (!empty($mentioned_profile)) {
                    $mentions_profiles[] = $mentioned_profile[0];
                }
            } catch (Exception $e) {
                // Invalid actor found, just let it go, it will eventually be handled by some other federation plugin like OStatus.
            }
        }
        unset($discovery);

        foreach ($mentions_profiles as $mp) {
            if (!$mp->hasBlocked($actor_profile)) {
                $act->context->attention[$mp->getUri()] = 'http://activitystrea.ms/schema/1.0/person';
            }
        }

        // Add location if that is set
        if (isset($settings['latitude'], $settings['longitude'])) {
            $act->context->location = Location::fromLatLon($settings['latitude'], $settings['longitude']);
        }

        /* Reject notice if it is too long (without the HTML)
           if (Notice::contentTooLong($content)) {
           throw new Exception('That\'s too long. Maximum notice size is %d character.');
           }*/

        // Attachments (first part)
        $attachments = [];
        if (isset($object['attachment']) && is_array($object['attachment'])) {
            foreach ($object['attachment'] as $attachment) {
                if (array_key_exists('type', $attachment) && $attachment['type'] == 'Document') {
                    try {
                        // throws exception on failure
                        $attachment = MediaFile::fromUrl($attachment['url'], $actor_profile, $attachment['name']);
                        $act->enclosures[] = $attachment->getEnclosure();
                        $attachments[] = $attachment;
                    } catch (Exception $e) {
                        // Whatever.
                    }
                }
            }
        }

        $actobj = new ActivityObject();
        $actobj->type = ActivityObject::NOTE;
        $actobj->content = strip_tags($content, '

');

        // Finally add the activity object to our activity
        $act->objects[] = $actobj;

        $note = Notice::saveActivity($act, $actor_profile, $options);

        // Attachments (last part)
        foreach($attachments as $attachment) {
            $attachment->attachToNotice($note);
        }

        return $note;
    }

    /**
     * Validates a note.
     *
     * @param array $object
     * @return bool false if unacceptable for GS but valid ActivityPub object
     * @throws Exception if invalid ActivityPub object
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function validate_note($object)
    {
        if (!isset($object['id'])) {
            common_debug('ActivityPub Notice Validator: Rejected because Object ID was not specified.');
            throw new Exception('Object ID not specified.');
        } elseif (!filter_var($object['id'], FILTER_VALIDATE_URL)) {
            common_debug('ActivityPub Notice Validator: Rejected because Object ID is invalid.');
            throw new Exception('Invalid Object ID.');
        }
        if (!isset($object['type']) || $object['type'] !== 'Note') {
            common_debug('ActivityPub Notice Validator: Rejected because of Type.');
            throw new Exception('Invalid Object type.');
        }
        if (isset($object['url']) && !filter_var($object['url'], FILTER_VALIDATE_URL)) {
            common_debug('ActivityPub Notice Validator: Rejected because Object URL is invalid.');
            throw new Exception('Invalid Object URL.');
        }
        if (!(isset($object['to']) && isset($object['cc']))) {
            common_debug('ActivityPub Notice Validator: Rejected because either Object CC or TO wasn\'t specified.');
            throw new Exception('Either Object CC or TO wasn\'t specified.');
        }
        if (!isset($object['content'])) {
            common_debug('ActivityPub Notice Validator: Rejected because Content was not specified (GNU social requires content in notes).');
            return false;
        }
        return true;
    }

    /**
     * Get the original representation URL of a given notice.
     *
     * @param Notice $notice notice from which to retrieve the URL
     * @return string URL
     * @throws InvalidUrlException
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function getUrl(Notice $notice): string
    {
        if ($notice->isLocal()) {
            return common_local_url('apNotice', ['id' => $notice->getID()]);
        } else {
            return $notice->getUrl();
        }
    }

    /**
     * Extract note policy type from note targets.
     *
     * @param array $note received Note
     * @param Profile $actor_profile Note author
     * @return int Notice policy type
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function getNotePolicyType(array $note, Profile $actor_profile): int
    {
        if (in_array('https://www.w3.org/ns/activitystreams#Public', $note['to'])) {
            return $actor_profile->isLocal() ? Notice::LOCAL_PUBLIC : Notice::REMOTE;
        } else {
            // either an unlisted or followers-only note, we'll handle
            // both as a GATEWAY notice since this type is not visible
            // from the public timelines, hence partially enough while
            // we don't have subscription_policy working.
            return Notice::GATEWAY;
        }
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub error representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_reject
{
    /**
     * Generates an ActivityPub representation of a Reject
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function reject_to_array($object)
    {
        $res = [
                '@context' => 'https://www.w3.org/ns/activitystreams',
                'type'     => 'Reject',
                'object'   => $object
        ];
        return $res;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub representation of a Tag
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_tag
{
    /**
     * Generates a pretty tag from a Tag object
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array Tag $tag
     * @return array pretty array to be used in a response
     */
    public static function tag_to_array($tag)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'name' => $tag,
            'url'  => common_local_url('tag', ['tag' => $tag])
        ];
        return $res;
    }
}


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

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

/**
 * ActivityPub error representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_undo
{
    /**
     * Generates an ActivityPub representation of a Undo
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function undo_to_array($object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => $object['id'].'/undo',
            'type'   => 'Undo',
            'actor'  => $object['actor'],
            'object' => $object
        ];
        return $res;
    }

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