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

// Ensure proper timezone
date_default_timezone_set('GMT');

// Import required files by the plugin
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'httpsignature.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'discoveryhints.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'AcceptHeader.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'explorer.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'postman.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'inbox_handler.php';

// So that this isn't hardcoded everywhere
define('ACTIVITYPUB_BASE_ACTOR_URI', common_root_url().'index.php/user/');
const ACTIVITYPUB_PUBLIC_TO = ['https://www.w3.org/ns/activitystreams#Public',
                               'Public',
                               'as:Public'
                              ];

/**
 * @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 ActivityPubPlugin extends Plugin
{
    const PLUGIN_VERSION = '22.1.1dev';

    /**
     * Returns a Actor's URI from its local $profile
     * Works both for local and remote users.
     *
     * @param Profile $profile Actor's local profile
     * @return string Actor's URI
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function actor_uri($profile)
    {
        if ($profile->isLocal()) {
            return ACTIVITYPUB_BASE_ACTOR_URI.$profile->getID();
        } else {
            return $profile->getUri();
        }
    }

    /**
     * Returns a Actor's URL from its local $profile
     * Works both for local and remote users.
     *
     * @param Profile $profile Actor's local profile
     * @return string Actor's URL
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function actor_url($profile)
    {
        return ActivityPubPlugin::actor_uri($profile)."/";
    }

    /**
     * Returns a notice from its URL.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $url Notice's URL
     * @return Notice The Notice object
     * @throws Exception This function or provides a Notice or fails with exception
     */
    public static function grab_notice_from_url($url)
    {
        /* Offline Grabbing */
        try {
            // Look for a known remote notice
            return Notice::getByUri($url);
        } catch (Exception $e) {
            // Look for a local notice (unfortunately GNU social doesn't
            // provide this functionality natively)
            try {
                $candidate = Notice::getByID(intval(substr($url, (strlen(common_local_url('apNotice', ['id' => 0]))-1))));
                if (common_local_url('apNotice', ['id' => $candidate->getID()]) === $url) { // Sanity check
                    return $candidate;
                } else {
                    common_debug('ActivityPubPlugin Notice Grabber: '.$candidate->getUrl(). ' is different of '.$url);
                }
            } catch (Exception $e) {
                common_debug('ActivityPubPlugin Notice Grabber: failed to find: '.$url.' offline.');
            }
        }

        /* Online Grabbing */
        $client    = new HTTPClient();
        $headers   = [];
        $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
        $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social';
        $response  = $client->get($url, $headers);
        $object = json_decode($response->getBody(), true);
        Activitypub_notice::validate_note($object);
        return Activitypub_notice::create_notice($object);
    }

    /**
     * Route/Reroute urls
     *
     * @param URLMapper $m
     * @return void
     * @throws Exception
     */
    public function onRouterInitialized(URLMapper $m)
    {
        if (ActivityPubURLMapperOverwrite::should()) {
            ActivityPubURLMapperOverwrite::variable(
                $m,
                'user/:id',
                ['id'     => '[0-9]+'],
                'apActorProfile'
            );

            // Special route for webfinger purposes
            ActivityPubURLMapperOverwrite::variable(
                $m,
                ':nickname',
                ['nickname' => Nickname::DISPLAY_FMT],
                'apActorProfile'
            );
        }

        // No .json here for convenience purposes on Notice grabber
        $m->connect(
            'note/:id',
            ['action' => 'apNotice'],
            ['id'     => '[0-9]+']
        );

        $m->connect(
            'user/:id/liked.json',
            ['action' => 'apActorLiked'],
            ['id' => '[0-9]+']
        );

        $m->connect(
            'user/:id/followers.json',
            ['action' => 'apActorFollowers'],
            ['id' => '[0-9]+']
        );

        $m->connect(
            'user/:id/following.json',
            ['action' => 'apActorFollowing'],
            ['id' => '[0-9]+']
        );

        $m->connect(
            'user/:id/inbox.json',
            ['action' => 'apInbox'],
            ['id' => '[0-9]+']
        );

        $m->connect(
            'user/:id/outbox.json',
            ['action' => 'apActorOutbox'],
            ['id' => '[0-9]+']
        );

        $m->connect(
            'inbox.json',
            ['action' => 'apInbox']
        );
    }

    /**
     * Plugin version information
     *
     * @param array $versions
     * @return bool hook true
     */
    public function onPluginVersion(array &$versions)
    {
        $versions[] = [
            'name' => 'ActivityPub',
            'version' => self::PLUGIN_VERSION,
            'author' => 'Diogo Cordeiro, Daniel Supernault',
            'homepage' => 'https://notabug.org/diogo/gnu-social/src/activitypub/plugins/ActivityPub',
            // TRANS: Plugin description.
            'rawdescription' => _m('Follow people across social networks that implement '.
            'ActivityPub.')
        ];
        return true;
    }

    /**
     * Plugin Nodeinfo information
     *
     * @param array $protocols
     * @return bool hook true
     */
    public function onNodeInfoProtocols(array &$protocols)
    {
        $protocols[] = "activitypub";
        return true;
    }

    /**
     * Adds an indicator on Remote ActivityPub profiles.
     *
     * @param HTMLOutputter $out
     * @param Profile $profile
     * @return boolean hook return value
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile)
    {
        if ($profile->isLocal()) {
            return true;
        }
        try {
            Activitypub_profile::from_profile($profile);
        } catch (Exception $e) {
            // Not a remote ActivityPub_profile! Maybe some other network
            // that has imported a non-local user (e.g.: OStatus)?
            return true;
        }

        $out->elementStart('dl', 'entity_tags activitypub_profile');
        $out->element('dt', null, 'ActivityPub');
        $out->element('dd', null, _m('Remote Profile'));
        $out->elementEnd('dl');

        return true;
    }

    /**
     * Make sure necessary tables are filled out.
     *
     * @return boolean hook true
     */
    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('Activitypub_profile', Activitypub_profile::schemaDef());
        $schema->ensureTable('Activitypub_rsa', Activitypub_rsa::schemaDef());
        $schema->ensureTable('Activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef());
        return true;
    }

    /********************************************************
     *                   WebFinger Events                   *
     ********************************************************/

    /**
     * Get remote user's ActivityPub_profile via a identifier
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $arg A remote user identifier
     * @return Activitypub_profile|null Valid profile in success | null otherwise
     */
    public static function pull_remote_profile($arg)
    {
        if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
            // webfinger lookup
            try {
                return Activitypub_profile::ensure_web_finger($arg);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Webfinger lookup failed for ' .
                                                $arg . ': ' . $e->getMessage());
            }
        }

        // Look for profile URLs, with or without scheme:
        $urls = [];
        if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $urls[] = $arg;
        }
        if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $schemes = array('http', 'https');
            foreach ($schemes as $scheme) {
                $urls[] = "$scheme://$arg";
            }
        }

        foreach ($urls as $url) {
            try {
                return Activitypub_profile::fromUri($url);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Profile lookup failed for ' .
                                                $arg . ': ' . $e->getMessage());
            }
        }
        return null;
    }

    /**
    * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
    *
    * @author GNU social
    * @param   string  $text       The text from which to extract webfinger IDs
    * @param   string  $preMention Character(s) that signals a mention ('@', '!'...)
    * @return  array   The matching IDs (without $preMention) and each respective position in the given string.
    */
    public static function extractWebfingerIds($text, $preMention='@')
    {
        $wmatches = [];
        $result = preg_match_all(
            '/(?<!\S)'.preg_quote($preMention, '/').'('.Nickname::WEBFINGER_FMT.')/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
                );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error().').');
            return [];
        } elseif ($n_matches = count($wmatches)) {
            common_debug(sprintf('Found %d matches for WebFinger IDs: %s', $n_matches, _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Profile URL matches: @example.com/mublog/user
     *
     * @author GNU social
     * @param   string  $text       The text from which to extract URL mentions
     * @param   string  $preMention Character(s) that signals a mention ('@', '!'...)
     * @return  array   The matching URLs (without @ or acct:) and each respective position in the given string.
     */
    public static function extractUrlMentions($text, $preMention='@')
    {
        $wmatches = [];
        // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
        // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
        $result = preg_match_all(
            '/(?:^|\s+)'.preg_quote($preMention, '/').'('.URL_REGEX_DOMAIN_NAME.'(?:\/['.URL_REGEX_VALID_PATH_CHARS.']*)*)/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
                );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error().').');
        } elseif (count($wmatches)) {
            common_debug(sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Add activity+json mimetype on WebFinger
     *
     * @param XML_XRD $xrd
     * @param Managed_DataObject $object
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndWebFingerProfileLinks(XML_XRD &$xrd, Managed_DataObject $object)
    {
        if ($object->isPerson()) {
            $link = new XML_XRD_Element_Link(
                'self',
                ActivityPubPlugin::actor_uri($object->getProfile()),
                'application/activity+json'
            );
            $xrd->links[] = clone ($link);
        }
    }

    /**
     * Find any explicit remote mentions. Accepted forms:
     *   Webfinger: @user@example.com
     *   Profile link:
     * @param Profile $sender
     * @param string $text input markup text
     * @param $mentions
     * @return boolean hook return value
     * @throws InvalidUrlException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @example.com/mublog/user
     *
     * @author GNU social
     */
    public function onEndFindMentions(Profile $sender, $text, &$mentions)
    {
        $matches = [];

        foreach (self::extractWebfingerIds($text, '@') as $wmatch) {
            list($target, $pos) = $wmatch;
            $this->log(LOG_INFO, "Checking webfinger person '$target'");
            $profile = null;
            try {
                $aprofile = Activitypub_profile::ensure_web_finger($target);
                $profile = $aprofile->local_profile();
            } catch (Exception $e) {
                $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
                continue;
            }
            assert($profile instanceof Profile);

            $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
                        ? $profile->getNickname()   // TODO: we could do getBestName() or getFullname() here
                        : $target;
            $url = $profile->getUri();
            if (!common_valid_http_url($url)) {
                $url = $profile->getUrl();
            }
            $matches[$pos] = array('mentioned' => array($profile),
                                               'type' => 'mention',
                                               'text' => $displayName,
                                               'position' => $pos,
                                               'length' => mb_strlen($target),
                                               'url' => $url);
        }

        foreach (self::extractUrlMentions($text) as $wmatch) {
            list($target, $pos) = $wmatch;
            $schemes = array('https', 'http');
            foreach ($schemes as $scheme) {
                $url = "$scheme://$target";
                $this->log(LOG_INFO, "Checking profile address '$url'");
                try {
                    $aprofile = Activitypub_profile::fromUri($url);
                    $profile = $aprofile->local_profile();
                    $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
                                        $profile->nickname : $target;
                    $matches[$pos] = array('mentioned' => array($profile),
                                                               'type' => 'mention',
                                                               'text' => $displayName,
                                                               'position' => $pos,
                                                               'length' => mb_strlen($target),
                                                               'url' => $profile->getUrl());
                    break;
                } catch (Exception $e) {
                    $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
                }
            }
        }

        foreach ($mentions as $i => $other) {
            // If we share a common prefix with a local user, override it!
            $pos = $other['position'];
            if (isset($matches[$pos])) {
                $mentions[$i] = $matches[$pos];
                unset($matches[$pos]);
            }
        }
        foreach ($matches as $mention) {
            $mentions[] = $mention;
        }

        return true;
    }

    /**
     * Allow remote profile references to be used in commands:
     *   sub update@status.net
     *   whois evan@identi.ca
     *   reply http://identi.ca/evan hey what's up
     *
     * @param Command $command
     * @param string $arg
     * @param Profile &$profile
     * @return boolean hook return code
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartCommandGetProfile($command, $arg, &$profile)
    {
        try {
            $aprofile = $this->pull_remote_profile($arg);
            $profile = $aprofile->local_profile();
        } catch (Exception $e) {
            // No remote ActivityPub profile found
            return true;
        }

        return false;
    }

    /********************************************************
     *                   Discovery Events                   *
     ********************************************************/

    /**
     * Profile URI for remote profiles.
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param Profile $profile
     * @param string $uri in/out
     * @return mixed hook return code
     */
    public function onStartGetProfileUri(Profile $profile, &$uri)
    {
        $aprofile = Activitypub_profile::getKV('profile_id', $profile->id);
        if ($aprofile instanceof Activitypub_profile) {
            $uri = $aprofile->getUri();
            return false;
        }
        return true;
    }

    /**
     * Profile from URI.
     *
     * @author GNU social
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $uri
     * @param Profile &$profile in/out param: Profile got from URI
     * @return mixed hook return code
     */
    public function onStartGetProfileFromURI($uri, &$profile)
    {
        try {
            $explorer = new Activitypub_explorer();
            $profile = $explorer->lookup($uri)[0];
            return false;
        } catch (Exception $e) {
            return true; // It's not an ActivityPub profile as far as we know, continue event handling
        }
    }

    /********************************************************
     *                    Delivery Events                   *
     ********************************************************/

    /**
     * Having established a remote subscription, send a notification to the
     * remote ActivityPub profile's endpoint.
     *
     * @param Profile $profile subscriber
     * @param Profile $other subscribee
     * @return bool return value
     * @throws HTTP_Request2_Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartSubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal() && $other->isLocal()) {
            return true;
        }

        try {
            $other = Activitypub_profile::from_profile($other);
        } catch (Exception $e) {
            return true; // Let other plugin handle this instead
        }

        $postman = new Activitypub_postman($profile, array($other));

        $postman->follow();

        return true;
    }

    /**
     * Notify remote server on unsubscribe.
     *
     * @param Profile $profile
     * @param Profile $other
     * @return bool return value
     * @throws HTTP_Request2_Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartUnsubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal() && $other->isLocal()) {
            return true;
        }

        try {
            $other = Activitypub_profile::from_profile($other);
        } catch (Exception $e) {
            return true; // Let other plugin handle this instead
        }

        $postman = new Activitypub_postman($profile, array($other));

        $postman->undo_follow();

        return true;
    }

    /**
     * Notify remote users when their notices get favourited.
     *
     * @param Profile $profile of local user doing the faving
     * @param Notice $notice Notice being favored
     * @return bool return value
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndFavorNotice(Profile $profile, Notice $notice)
    {
        // Only distribute local users' favor actions, remote users
        // will have already distributed theirs.
        if (!$profile->isLocal()) {
            return true;
        }

        $other = [];
        try {
            $other[] = Activitypub_profile::from_profile($notice->getProfile());
        } catch (Exception $e) {
            // Local user can be ignored
        }
        foreach ($notice->getAttentionProfiles() as $to_profile) {
            try {
                $other[] = Activitypub_profile::from_profile($to_profile);
            } catch (Exception $e) {
                // Local user can be ignored
            }
        }
        if ($notice->reply_to) {
            try {
                $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
            } catch (Exception $e) {
                // Local user can be ignored
            }
            try {
                $mentions = $notice->getParent()->getAttentionProfiles();
                foreach ($mentions as $to_profile) {
                    try {
                        $other[] = Activitypub_profile::from_profile($to_profile);
                    } 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());
            }
        }

        $postman = new Activitypub_postman($profile, $other);

        $postman->like($notice);

        return true;
    }

    /**
     * Notify remote users when their notices get de-favourited.
     *
     * @param Profile $profile of local user doing the de-faving
     * @param Notice $notice Notice being favored
     * @return bool return value
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onEndDisfavorNotice(Profile $profile, Notice $notice)
    {
        // Only distribute local users' favor actions, remote users
        // will have already distributed theirs.
        if (!$profile->isLocal()) {
            return true;
        }

        $other = [];
        try {
            $other[] = Activitypub_profile::from_profile($notice->getProfile());
        } catch (Exception $e) {
            // Local user can be ignored
        }
        foreach ($notice->getAttentionProfiles() as $to_profile) {
            try {
                $other[] = Activitypub_profile::from_profile($to_profile);
            } catch (Exception $e) {
                // Local user can be ignored
            }
        }
        if ($notice->reply_to) {
            try {
                $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
            } catch (Exception $e) {
                // Local user can be ignored
            }
            try {
                $mentions = $notice->getParent()->getAttentionProfiles();
                foreach ($mentions as $to_profile) {
                    try {
                        $other[] = Activitypub_profile::from_profile($to_profile);
                    } 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());
            }
        }

        $postman = new Activitypub_postman($profile, $other);

        $postman->undo_like($notice);

        return true;
    }

    /**
     * Notify remote users when their notices get deleted
     *
     * @param $user
     * @param $notice
     * @return boolean hook flag
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartDeleteOwnNotice($user, $notice)
    {
        $profile = $user->getProfile();

        // Only distribute local users' delete actions, remote users
        // will have already distributed theirs.
        if (!$profile->isLocal()) {
            return true;
        }

        $other = [];

        foreach ($notice->getAttentionProfiles() as $to_profile) {
            try {
                $other[] = Activitypub_profile::from_profile($to_profile);
            } catch (Exception $e) {
                // Local user can be ignored
            }
        }
        if ($notice->reply_to) {
            try {
                $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
            } catch (Exception $e) {
                // Local user can be ignored
            }
            try {
                $mentions = $notice->getParent()->getAttentionProfiles();
                foreach ($mentions as $to_profile) {
                    try {
                        $other[] = Activitypub_profile::from_profile($to_profile);
                    } 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());
            }
        }

        $postman = new Activitypub_postman($profile, $other);
        $postman->delete($notice);
        return true;
    }

    /**
     * Insert notifications for replies, mentions and repeats
     *
     * @param $notice
     * @return boolean hook flag
     * @throws EmptyPkeyValueException
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function onStartNoticeDistribute($notice)
    {
        assert($notice->id > 0);        // Ignore if not a valid notice

        $profile = $notice->getProfile();

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

        // Ignore for activity/non-post-verb notices
        if (method_exists('ActivityUtils', 'compareVerbs')) {
            $is_post_verb = ActivityUtils::compareVerbs(
                $notice->verb,
                [ActivityVerb::POST]
                        );
        } else {
            $is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false);
        }
        if ($notice->source == 'activity' || !$is_post_verb) {
            return true;
        }

        $other = [];
        foreach ($notice->getAttentionProfiles() as $mention) {
            try {
                $other[] = Activitypub_profile::from_profile($mention);
            } catch (Exception $e) {
                // Local user can be ignored
            }
        }

        // Is a reply?
        if ($notice->reply_to) {
            try {
                $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile());
            } catch (Exception $e) {
                // Local user can be ignored
            }
            try {
                foreach ($notice->getParent()->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());
            }
        }

        // Is an Announce?
        if ($notice->isRepeat()) {
            $repeated_notice = Notice::getKV('id', $notice->repeat_of);
            if ($repeated_notice instanceof Notice) {
                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);
                return true;
            }
        }

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

    /**
     * Override the "from ActivityPub" bit in notice lists to link to the
     * original post and show the domain it came from.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param $notice
     * @param $name
     * @param $url
     * @param $title
     * @return mixed hook return code
     * @throws Exception
     */
    public function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
    {
        // If we don't handle this, keep the event handler going
        if (!in_array($notice->source, array('ActivityPub', 'share'))) {
            return true;
        }

        try {
            $url = $notice->getUrl();
            // If getUrl() throws exception, $url is never set

            $bits = parse_url($url);
            $domain = $bits['host'];
            if (substr($domain, 0, 4) == 'www.') {
                $name = substr($domain, 4);
            } else {
                $name = $domain;
            }

            // TRANS: Title. %s is a domain name.
            $title = sprintf(_m('Sent from %s via ActivityPub'), $domain);

            // Abort event handler, we have a name and URL!
            return false;
        } catch (InvalidUrlException $e) {
            // This just means we don't have the notice source data
            return true;
        }
    }
}

/**
 * Plugin return handler
 */
class ActivityPubReturn
{
    /**
     * Return a valid answer
     *
     * @param string $res
     * @param int $code Status Code
     * @return void
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function answer($res = '', $code = 202)
    {
        http_response_code($code);
        header('Content-Type: application/activity+json');
        echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET["pretty"]) ? JSON_PRETTY_PRINT : null));
        exit;
    }

    /**
     * Return an error
     *
     * @param string $m
     * @param int $code Status Code
     * @return void
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function error($m, $code = 400)
    {
        http_response_code($code);
        header('Content-Type: application/activity+json');
        $res[] = Activitypub_error::error_message_to_array($m);
        echo json_encode($res, JSON_UNESCAPED_SLASHES);
        exit;
    }
}

/**
 * Overwrites variables in URL-mapping
 */
class ActivityPubURLMapperOverwrite extends URLMapper
{
    /**
     * Overwrites a route.
     *
     * @author Hannes Mannerheim <h@nnesmannerhe.im>
     * @param URLMapper $m
     * @param string $path
     * @param string $paramPatterns
     * @param string $newaction
     * @return void
     * @throws Exception
     */
    public static function variable($m, $path, $paramPatterns, $newaction)
    {
        $m->connect($path, array('action' => $newaction), $paramPatterns);
        $regex = self::makeRegex($path, $paramPatterns);
        foreach ($m->variables as $n => $v) {
            if ($v[1] == $regex) {
                $m->variables[$n][0]['action'] = $newaction;
            }
        }
    }

    /**
     * Determines whether the route should or not be overwrited.
     * If ACCEPT header isn't set false will be returned.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @return boolean true if it should, false otherwise
     */
    public static function should()
    {
        // Do not operate without Accept Header
        if (!isset($_SERVER['HTTP_ACCEPT'])) {
            return false;
        }

        $mimes = [
            'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0,
            'application/activity+json' => 1,
            'application/json' => 2,
            'application/ld+json' => 3
        ];

        $acceptheader = new AcceptHeader($_SERVER['HTTP_ACCEPT']);
        foreach ($acceptheader as $ah) {
            if (isset($mimes[$ah['raw']])) {
                return true;
            }
        }

        return false;
    }
}


code:
# Contributing

When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

Please note we have a code of conduct, please follow it in all your interactions with the project.

# Coding Style
- We follow every [PSR-2](https://www.php-fig.org/psr/psr-2/) ...
- ... except camelCase, that's too bad, we use snake_case

## Merge Request Process

1. Ensure you strip any trailing spaces off
2. Increase the version numbers in any examples files and the README.md to the new version that this
   Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
   do not have permission to do that, you may request the second reviewer to merge it for you.

## Code of Conduct

### Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

### Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

### Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

### Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

### Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at diogo@fc.up.pt. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

### Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

repo_name: andremichelle/gnu

license: mit


code:
# ActivityPub plugin for GNU social
(c) 2018-2019 Free Software Foundation, Inc

This is the README file for GNU social's ActivityPub plugin.
It includes general information about the plugin.

## About

This plugin adds [ActivityPub](https://www.w3.org/TR/activitypub/) support to GNU social.

## Credits

* **[Diogo Cordeiro](https://www.diogo.site/)**
* **[Daniel Supernault](https://github.com/dansup)**

## Special thanks

* **[Mikael Nordfeldth](https://mmn-o.se/)**

## License

This program 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.

This program 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 this program, in the file "COPYING".  If not, see
<http://www.gnu.org/licenses/>.

    IMPORTANT NOTE: The GNU Affero General Public License (AGPL) has
    *different requirements* from the "regular" GPL. In particular, if
    you make modifications to the plugin source code on your server,
    you *MUST MAKE AVAILABLE* the modified version of the source code
    to your users under the same license. This is a legal requirement
    of using the software, and if you do not wish to share your
    modifications, *YOU MAY NOT USE THIS PLUGIN*.


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

/**
 * Actor's Followers Collection
 *
 * @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 apActorFollowersAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Followers Collection request
     *
     * @return void
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        try {
            $profile = Profile::getByID($this->trimmed('id'));
            $profile_id = $profile->getID();
        } catch (Exception $e) {
            ActivityPubReturn::error('Invalid Actor URI.', 404);
        }

        if (!$profile->isLocal()) {
            ActivityPubReturn::error("This is not a local user.", 403);
        }

        if (!isset($_GET["page"])) {
            $page = 0;
        } else {
            $page = intval($this->trimmed('page'));
        }

        if ($page < 0) {
            ActivityPubReturn::error('Invalid page number.');
        }

        $since = ($page - 1) * PROFILES_PER_MINILIST;
        $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST;

        /* Calculate total items */
        $total_subs  = $profile->subscriberCount();
        $total_pages = ceil($total_subs / PROFILES_PER_MINILIST);

        $res = [
            '@context'     => [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v1",
            ],
            'id'           => common_local_url('apActorFollowers', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''),
            'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'),
            'totalItems'   => $total_subs
        ];

        if ($page == 0) {
            $res['first'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page=1';
        } else {
            $res['orderedItems'] = $this->generate_followers($profile, $since, $limit);
            $res['partOf'] = common_local_url('apActorFollowers', ['id' => $profile_id]);

            if ($page+1 < $total_pages) {
                $res['next'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1);
            }

            if ($page > 1) {
                $res['prev'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1);
            }
        }

        ActivityPubReturn::answer($res);
    }

    /**
     * Generates a list of stalkers for a given profile.
     *
     * @param Profile $profile
     * @param int $since
     * @param int $limit
     * @return array of URIs
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function generate_followers($profile, $since, $limit)
    {
        /* Fetch Followers */
        try {
            $sub = $profile->getSubscribers($since, $limit);
        } catch (NoResultException $e) {
            // Just let the exception go on its merry way
        }

        /* Get followers' URLs */
        $subs = [];
        while ($sub->fetch()) {
            $subs[] = ActivityPubPlugin::actor_uri($sub);
        }

        return $subs;
    }
}


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

/**
 * Actor's Following Collection
 *
 * @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 apActorFollowingAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Following Collection request
     *
     * @return void
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        try {
            $profile = Profile::getByID($this->trimmed('id'));
            $profile_id = $profile->getID();
        } catch (Exception $e) {
            ActivityPubReturn::error('Invalid Actor URI.', 404);
        }

        if (!$profile->isLocal()) {
            ActivityPubReturn::error("This is not a local user.", 403);
        }

        if (!isset($_GET["page"])) {
            $page = 0;
        } else {
            $page = intval($this->trimmed('page'));
        }

        if ($page < 0) {
            ActivityPubReturn::error('Invalid page number.');
        }

        $since = ($page - 1) * PROFILES_PER_MINILIST;
        $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST;

        /* Calculate total items */
        $total_subs  = $profile->subscriptionCount();
        $total_pages = ceil($total_subs / PROFILES_PER_MINILIST);

        $res = [
            '@context'     => [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v1",
            ],
            'id'           => common_local_url('apActorFollowing', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''),
            'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'),
            'totalItems'   => $total_subs
        ];

        if ($page == 0) {
            $res['first'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page=1';
        } else {
            $res['orderedItems'] = $this->generate_following($profile, $since, $limit);
            $res['partOf'] = common_local_url('apActorFollowing', ['id' => $profile_id]);

            if ($page+1 < $total_pages) {
                $res['next'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1);
            }

            if ($page > 1) {
                $res['prev'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1);
            }
        }

        ActivityPubReturn::answer($res);
    }

    /**
     * Generates the list of those a given profile is stalking.
     *
     * @param Profile $profile
     * @param int $since
     * @param int $limit
     * @return array of URIs
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function generate_following($profile, $since, $limit)
    {
        /* Fetch Following */
        try {
            $sub = $profile->getSubscribed($since, $limit);
        } catch (NoResultException $e) {
            // Just let the exception go on its merry way
        }

        /* Get followed' URLs */
        $subs = [];
        while ($sub->fetch()) {
            $subs[] = ActivityPubPlugin::actor_uri($sub);
        }
        return $subs;
    }
}


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

/**
 * Actor's Liked Collection
 *
 * @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 apActorLikedAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Liked Collection request
     *
     * @return void
     * @throws EmptyPkeyValueException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        try {
            $profile = Profile::getByID($this->trimmed('id'));
            $profile_id = $profile->getID();
        } catch (Exception $e) {
            ActivityPubReturn::error('Invalid Actor URI.', 404);
        }

        if (!$profile->isLocal()) {
            ActivityPubReturn::error("This is not a local user.", 403);
        }

        $limit    = intval($this->trimmed('limit'));
        $since_id = intval($this->trimmed('since_id'));
        $max_id   = intval($this->trimmed('max_id'));

        $limit    = empty($limit) ? 40 : $limit;       // Default is 40
        $since_id = empty($since_id) ? null : $since_id;
        $max_id   = empty($max_id) ? null : $max_id;

        // Max is 80
        if ($limit > 80) {
            $limit = 80;
        }

        $fave = $this->fetch_faves($profile_id, $limit, $since_id, $max_id);

        $faves = array();
        while ($fave->fetch()) {
            $faves[] = $this->pretty_fave(clone ($fave));
        }

        $res = [
            '@context'     => [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v1",
            ],
            'id'           => common_local_url('apActorLiked', ['id' => $profile_id]),
            'type'         => 'OrderedCollection',
            'totalItems'   => Fave::countByProfile($profile),
            'orderedItems' => $faves
        ];

        ActivityPubReturn::answer($res);
    }

    /**
     * Take a fave object and turns it in a pretty array to be used
     * as a plugin answer
     *
     * @param Fave $fave_object
     * @return array pretty array representating a Fave
     * @throws EmptyPkeyValueException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function pretty_fave($fave_object)
    {
        $res = [
            'created' => $fave_object->created,
            'object' => Activitypub_notice::notice_to_array(Notice::getByID($fave_object->notice_id))
        ];

        return $res;
    }

    /**
     * Fetch faves
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param int $user_id
     * @param int $limit
     * @param int $since_id
     * @param int $max_id
     * @return Fave fetchable fave collection
     */
    private static function fetch_faves(
        $user_id,
        $limit = 40,
        $since_id = null,
        $max_id = null
        ) {
        $fav = new Fave();

        $fav->user_id = $user_id;

        $fav->orderBy('modified DESC');

        if ($since_id != null) {
            $fav->whereAdd("notice_id  > {$since_id}");
        }

        if ($max_id != null) {
            $fav->whereAdd("notice_id  < {$max_id}");
        }

        $fav->limit($limit);

        $fav->find();

        return $fav;
    }
}


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

/**
 * Inbox Request 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 apActorOutboxAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Outbox request
     *
     * @author Daniel Supernault <danielsupernault@gmail.com>
     */
    protected function handle()
    {
        try {
            $profile = Profile::getByID($this->trimmed('id'));
            $profile_id = $profile->getID();
        } catch (Exception $e) {
            ActivityPubReturn::error('Invalid Actor URI.', 404);
        }

        if (!$profile->isLocal()) {
            ActivityPubReturn::error("This is not a local user.", 403);
        }

        if (!isset($_GET["page"])) {
            $page = 0;
        } else {
            $page = intval($this->trimmed('page'));
        }

        if ($page < 0) {
            ActivityPubReturn::error('Invalid page number.');
        }

        $since = ($page - 1) * PROFILES_PER_MINILIST;
        $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST;

        /* Calculate total items */
        $total_notes = $profile->noticeCount();
        $total_pages = ceil($total_notes / PROFILES_PER_MINILIST);

        $res = [
            '@context'     => [
              "https://www.w3.org/ns/activitystreams",
              "https://w3id.org/security/v1",
            ],
            'id'           => common_local_url('apActorOutbox', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''),
            'type'         => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'),
            'totalItems'   => $total_notes
        ];

        if ($page == 0) {
            $res['first'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page=1';
        } else {
            $res['orderedItems'] = $this->generate_outbox($profile);
            $res['partOf'] = common_local_url('apActorOutbox', ['id' => $profile_id]);

            if ($page+1 < $total_pages) {
                $res['next'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1);
            }

            if ($page > 1) {
                $res['prev'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1);
            }
        }

        ActivityPubReturn::answer($res);
    }

    /**
     * Generates a list of people following given profile.
     *
     * @param Profile $profile
     * @return array of Notices
     * @throws EmptyPkeyValueException
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Daniel Supernault <danielsupernault@gmail.com>
     */
    public function generate_outbox($profile)
    {
        /* Fetch Notices */
        $notices = [];
        $notice = $profile->getNotices();
        while ($notice->fetch()) {
            $note = $notice;

            // TODO: Handle other types
            if ($note->object_type == 'http://activitystrea.ms/schema/1.0/note') {
                $notices[] = Activitypub_create::create_to_array(
                    ActivityPubPlugin::actor_uri($note->getProfile()),
                    Activitypub_notice::notice_to_array($note)
                );
            }
        }

        return $notices;
    }
}


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

/**
 * Actor's profile (Local users only)
 *
 * @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 apActorProfileAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Actor Profile request
     *
     * @return void
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        if (!empty($id = $this->trimmed('id'))) {
            try {
                $profile = Profile::getByID($id);
            } catch (Exception $e) {
                ActivityPubReturn::error('Invalid Actor URI.', 404);
            }
            unset($id);
        } else {
            try {
                $profile = User::getByNickname($this->trimmed('nickname'))->getProfile();
            } catch (Exception $e) {
                ActivityPubReturn::error('Invalid username.', 404);
            }
        }

        if (!$profile->isLocal()) {
            ActivityPubReturn::error("This is not a local user.", 403);
        }

        $res = Activitypub_profile::profile_to_array($profile);

        ActivityPubReturn::answer($res, 200);
    }
}


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

/**
 * Inbox Request 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 apInboxAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Inbox request
     *
     * @return void
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        $path = !empty($this->trimmed('id')) ? common_local_url('apInbox', ['id' => $this->trimmed('id')]) : common_local_url('apInbox');
        $path = parse_url($path, PHP_URL_PATH);

        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            ActivityPubReturn::error('Only POST requests allowed.');
        }

        common_debug('ActivityPub Inbox: Received a POST request.');
        $body = $data = file_get_contents('php://input');
        common_debug('ActivityPub Inbox: Request contents: '.$data);
        $data = json_decode(file_get_contents('php://input'), true);

        if (!isset($data['actor'])) {
            ActivityPubReturn::error('Actor not found in the request.');
        }

        $actor = Activitypub_explorer::get_profile_from_url($data['actor']);
        $aprofile = Activitypub_profile::from_profile($actor);

        $actor_public_key = new Activitypub_rsa();
        $actor_public_key = $actor_public_key->ensure_public_key($actor);

        common_debug('ActivityPub Inbox: HTTP Signature: Validation will now start!');

        $headers = $this->get_all_headers();
        common_debug('ActivityPub Inbox: Request Headers: '.print_r($headers, true));

        if (!isset($headers['signature'])) {
            common_debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.');
            ActivityPubReturn::error('Missing Signature header.', 400);
        }

        // Extract the signature properties
        $signatureData = HTTPSignature::parseSignatureHeader($headers['signature']);
        common_debug('ActivityPub Inbox: HTTP Signature Data: '.print_r($signatureData, true));
        if (isset($signatureData['error'])) {
            common_debug('ActivityPub Inbox: HTTP Signature: '.json_encode($signatureData, true));
            ActivityPubReturn::error(json_encode($signatureData, true), 400);
        }

        list($verified, $headers) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);

        // If the signature fails verification the first time, update profile as it might have change public key
        if($verified !== 1) {
            $res = Activitypub_explorer::get_remote_user_activity($aprofile->getUri());
            $actor = Activitypub_profile::update_profile($aprofile, $res);
            $actor_public_key = new Activitypub_rsa();
            $actor_public_key = $actor_public_key->ensure_public_key($actor);
            list($verified, $headers) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
        }

        // If it still failed despite profile update
        if($verified !== 1) {
            common_debug('ActivityPub Inbox: HTTP Signature: Invalid signature.');
            ActivityPubReturn::error('Invalid signature.');
        }

        // HTTP signature checked out, make sure the "actor" of the activity matches that of the signature
        common_debug('ActivityPub Inbox: HTTP Signature: Authorized request. Will now start the inbox handler.');

        try {
            new Activitypub_inbox_handler($data, $actor);
            ActivityPubReturn::answer();
        } catch (Exception $e) {
            ActivityPubReturn::error($e->getMessage());
        }
    }

    /**
     * Get all HTTP header key/values as an associative array for the current request.
     *
     * @return array [string] The HTTP header key/value pairs.
     * @author PHP Manual Contributed Notes <joyview@gmail.com>
     */
    private function get_all_headers()
    {
        $headers = [];
        foreach ($_SERVER as $name => $value) {
            if (substr($name, 0, 5) == 'HTTP_') {
                $headers[strtolower(str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))))] = $value;
            }
        }
        return $headers;
    }
}


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

/**
 * Notice (Local notices only)
 *
 * @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 apNoticeAction extends ManagedAction
{
    protected $needLogin = false;
    protected $canPost   = true;

    /**
     * Handle the Notice request
     *
     * @return void
     * @throws EmptyPkeyValueException
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    protected function handle()
    {
        try {
            $notice = Notice::getByID($this->trimmed('id'));
        } catch (Exception $e) {
            ActivityPubReturn::error('Invalid Notice URI.', 404);
        }

        if (!$notice->isLocal()) {
            ActivityPubReturn::error("This is not a local notice.", 403);
        }

        $res = Activitypub_notice::notice_to_array($notice);

        ActivityPubReturn::answer($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_accept extends Managed_DataObject
{
    /**
     * 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 extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Announce
     *
     * @param $actor
     * @param array $object
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function announce_to_array($actor, $object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            "type"   => "Announce",
            "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 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 extends Managed_DataObject
{
    /**
     * 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 extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Create
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $actor
     * @param array $object
     * @return array pretty array to be used in a response
     */
    public static function create_to_array($actor, $object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => $object['id'].'/create',
            'type'   => 'Create',
            'to'     => $object['to'],
            'cc'     => $object['cc'],
            'actor'  => $actor,
            'object' => $object
        ];
        return $res;
    }

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


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 extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Delete
     *
     * @param $actor
     * @param array $object
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function delete_to_array($actor, $object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => $object.'/delete',
            'type'   => 'Delete',
            '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 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 extends Managed_DataObject
{
    /**
     * 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 extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a subscription
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $actor
     * @param string $object
     * @return array pretty array to be used in a response
     */
    public static function follow_to_array($actor, $object)
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id'     => common_root_url().'follow_from_'.urlencode($actor).'_to_'.urlencode($object),
            '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
     * @throws AlreadyFulfilledException
     * @throws HTTP_Request2_Exception
     * @throws NoProfileException
     * @throws ServerException
     * @throws \GuzzleHttp\Exception\GuzzleException
     * @throws \HttpSignatures\Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function follow($actor_profile, $object)
    {
        // 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);
            common_debug('ActivityPubPlugin: Accepted Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object);
        } else {
            common_debug('ActivityPubPlugin: Received a repeated Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' 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 '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object);
        $postman = new Activitypub_postman($actor_profile, [$actor_aprofile]);
        $postman->accept_follow();
    }
}


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 extends Managed_DataObject
{
    /**
     * 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 extends Managed_DataObject
{
    /**
     * Generates an ActivityPub representation of a Mention Tag
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $href Actor Uri
     * @param array $name Mention name
     * @return array pretty array to be used in a response
     */
    public static function mention_tag_to_array_from_values($href, $name)
    {
        $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 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 extends Managed_DataObject
{
    /**
     * 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
     * @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);
            }
        }

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

        // In a world without walls and fences, we should make everything Public!
        $to[]= 'https://www.w3.org/ns/activitystreams#Public';

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

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

        // 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.
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param array $object
     * @param Profile|null $actor_profile
     * @return Notice
     * @throws Exception
     */
    public static function create_notice($object, $actor_profile = null)
    {
        $id      = $object['id'];         // int
        $url     = $object['url'];        // 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'];
        }

        // Ensure Actor Profile
        if (is_null($actor_profile)) {
            $actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']);
        }

        $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 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 ($tag['type'] == 'Mention') {
                    $mentions[] = $tag['href'];
                }
            }
        }
        $mentions_profiles = [];
        $discovery = new Activitypub_explorer;
        foreach ($mentions as $mention) {
            try {
                $mentions_profiles[] = $discovery->lookup($mention)[0];
            } catch (Exception $e) {
                // Invalid actor found, just let it go. // TODO: Fallback to OStatus
            }
        }
        unset($discovery);

        foreach ($mentions_profiles as $mp) {
            $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = '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.');
        }*/

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

        return $note;
    }

    /**
     * Validates a note.
     *
     * @param array $object
     * @return bool
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function validate_note($object)
    {
        if (!isset($object['attributedTo'])) {
            common_debug('ActivityPub Notice Validator: Rejected because attributedTo was not specified.');
            throw new Exception('No attributedTo specified.');
        }
        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['content'])) {
            common_debug('ActivityPub Notice Validator: Rejected because Content was not specified.');
            throw new Exception('Object content was not specified.');
        }
        if (!isset($object['url'])) {
            throw new Exception('Object URL was not specified.');
        } elseif (!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['cc'])) {
            common_debug('ActivityPub Notice Validator: Rejected because Object CC was not specified.');
            throw new Exception('Object CC was not specified.');
        }
        return true;
    }
}