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

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

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

/**
 * ActivityPub error representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_create
{
    /**
     * Generates an ActivityPub representation of a Create
     *
     * @param string $actor
     * @param array $object
     * @param bool $directMessage whether it is a private Create activity or not
     * @return array pretty array to be used in a response
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function create_to_array(string $actor, array $object, bool $directMessage = false): array
    {
        $res = [
            '@context' => 'https://www.w3.org/ns/activitystreams',
            'id' => $object['id'] . '/create',
            'type' => 'Create',
            'directMessage' => $directMessage,
            'to' => $object['to'],
            'cc' => $object['cc'],
            'actor' => $actor,
            'object' => $object
        ];
        return $res;
    }

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

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

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


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

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

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

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

    /**
     * Verifies if a given object is acceptable for a Delete Activity.
     *
     * @param array|string $object
     * @return bool
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function validate_object($object): bool
    {
        if (!is_array($object)) {
            if (!filter_var($object, FILTER_VALIDATE_URL)) {
                throw new Exception('Object is not a valid Object URI for Activity.');
            }
        } else {
            if (!isset($object['type'])) {
                throw new Exception('Object type was not specified for Delete Activity.');
            }
            if ($object['type'] !== "Tombstone" && $object['type'] !== "Person") {
                throw new Exception('Invalid Object type for Delete Activity.');
            }
            if (!isset($object['id'])) {
                throw new Exception('Object id was not specified for Delete Activity.');
            }
        }

        return true;
    }
}

repo_name: spookie/gnu

license: mit


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

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

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

/**
 * ActivityPub error representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_error
{
    /**
     * Generates a pretty error from a string
     *
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     * @param string $m
     * @return array pretty array to be used in a response
     */
    public static function error_message_to_array($m)
    {
        $res = [
            'error'=> $m
        ];
        return $res;
    }
}


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

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

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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


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

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

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

/**
 * ActivityPub direct note representation
 *
 * @author Bruno Casteleiro <brunoccast@fc.up.pt>
 * @copyright 2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_message
{
    /**
     * Generates a pretty message from a Notice object
     *
     * @param Notice $message
     * @return array array to be used in a response
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public static function message_to_array(Notice $message): array
    {
        $from = $message->getProfile();

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

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

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

        return $item;
    }

    /**
     * Create a private Notice via ActivityPub Note Object.
     * Returns created Notice.
     *
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     * @param array $object
     * @param Profile $actor_profile
     * @return Notice
     * @throws Exception
     */
    public static function create_message(array $object, Profile $actor_profile = null): Notice
    {
        return Activitypub_notice::create_notice($object, $actor_profile, true);
    }
}


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

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

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

/**
 * ActivityPub notice representation
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_notice
{
    /**
     * Generates a pretty notice from a Notice object
     *
     * @param Notice $notice
     * @return array array to be used in a response
     * @throws EmptyPkeyValueException
     * @throws InvalidUrlException
     * @throws ServerException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public static function notice_to_array($notice)
    {
        $profile = $notice->getProfile();
        $attachments = [];
        foreach ($notice->attachments() as $attachment) {
            $attachments[] = Activitypub_attachment::attachment_to_array($attachment);
        }

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

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

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

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

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

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

        return $item;
    }

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

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

        // Ensure Actor Profile
        if (is_null($actor_profile)) {
            if (isset($object['attributedTo'])) {
                $actor_profile = ActivityPub_explorer::get_profile_from_url($object['attributedTo']);
            } elseif (isset($object['actor'])) {
                $actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']);
            } else {
                throw new Exception("A notice can't be created without an 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_local' => self::getNotePolicyType($object, $actor_profile)];

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

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

        // Mentions
        $mentions = [];
        if (isset($object['tag']) && is_array($object['tag'])) {
            foreach ($object['tag'] as $tag) {
                if (array_key_exists('type', $tag) && $tag['type'] == 'Mention') {
                    $mentions[] = $tag['href'];
                }
            }
        }
        $mentions_profiles = [];
        $discovery = new Activitypub_explorer;
        foreach ($mentions as $mention) {
            try {
                $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) {
            if (!$mp->hasBlocked($actor_profile)) {
                $act->context->attention[$mp->getUri()] = 'http://activitystrea.ms/schema/1.0/person';
            }
        }

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

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

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

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

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


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

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

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

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


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

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

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

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


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

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

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

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

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


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

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

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

/**
 * ActivityPub's own Postman
 *
 * Standard workflow expects that we send an Explorer to find out destinataries'
 * inbox address. Then we send our postman to deliver whatever we want to send them.
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_postman
{
    private $actor;
    private $actor_uri;
    private $to = [];
    private $client;
    private $headers;

    /**
     * Create a postman to deliver something to someone
     *
     * @param Profile $from sender Profile
     * @param array $to receiver Profiles
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function __construct(Profile $from, array $to = [])
    {
        $this->actor = $from;
        $this->to = $to;

        $this->actor_uri = $this->actor->getUri();

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

    /**
     * Send something to remote instance
     *
     * @param string $data request body
     * @param string $inbox url of remote inbox
     * @param string $method request method
     * @return GNUsocial_HTTPResponse
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function send($data, $inbox, $method = 'POST')
    {
        common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);

        $headers = HttpSignature::sign($this->actor, $inbox, $data);

        common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));

        $this->client->setBody($data);

        switch ($method) {
            case 'POST':
                $response = $this->client->post($inbox, $headers);
                break;
            case 'GET':
                $response = $this->client->get($inbox, $headers);
                break;
            default:
                throw new Exception("Unsupported request method for postman.");
        }

        common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
        return $response;
    }

    /**
     * Send a follow notification to remote instance
     *
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function follow()
    {
        $data = Activitypub_follow::follow_to_array($this->actor_uri, $this->to[0]->getUrl());
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
            $pending_list->add();
            return true;
        } elseif (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }

        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Undo Follow notification to remote instance
     *
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @throws Exception
     * @throws Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function undo_follow()
    {
        $data = Activitypub_undo::undo_to_array(
            Activitypub_follow::follow_to_array(
                $this->actor_uri,
                $this->to[0]->getUrl()
                    )
                );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            Activitypub_profile::unsubscribeCacheUpdate($this->actor, $this->to[0]->local_profile());
            $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
            $pending_list->remove();
            return true;
        }
        if (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }
        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Accept Follow notification to remote instance
     *
     * @param string $id Follow activity id
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception Description of HTTP Response error or generic error message.
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function accept_follow(string $id): bool
    {
        $data = Activitypub_accept::accept_to_array(
            Activitypub_follow::follow_to_array(
                $this->to[0]->getUrl(),
                $this->actor_uri,
                $id
                )
            );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            $pending_list = new Activitypub_pending_follow_requests($this->to[0]->getID(), $this->actor->getID());
            $pending_list->remove();
            return true;
        }
        if (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }
        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Like notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function like($notice)
    {
        $data = Activitypub_like::like_to_array(
            $this->actor_uri,
            Activitypub_notice::getUrl($notice)
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

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

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
        }
    }

    /**
     * Send a Undo Like notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function undo_like($notice)
    {
        $data = Activitypub_undo::undo_to_array(
            Activitypub_like::like_to_array(
                $this->actor_uri,
                Activitypub_notice::getUrl($notice)
                         )
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

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

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
        }
    }

    /**
     * Send a Create notification to remote instances
     *
     * @param Notice $notice
     * @throws EmptyPkeyValueException
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function create_note($notice)
    {
        $data = Activitypub_create::create_to_array(
            $this->actor_uri,
            Activitypub_notice::notice_to_array($notice)
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

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

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
        }
    }

    /**
     * Send a Create direct-notification to remote instances
     *
     * @param Notice $message
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public function create_direct_note(Notice $message)
    {
        $data = Activitypub_create::create_to_array(
            $this->actor_uri,
            Activitypub_message::message_to_array($message),
            true
        );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox(false) as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
        }
    }

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

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

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
        }
    }

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

    /**
     * Send a Delete notification to remote followers of some deleted profile
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public function delete_profile()
    {
        $data = Activitypub_delete::delete_to_array($this->actor_uri, $this->actor_uri);
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        $errors = [];
        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
        }
    }

    /**
     * Clean list of inboxes to deliver messages
     *
     * @param bool $actorFollowers whether to include the actor's follower collection
     * @return array To Inbox URLs
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function to_inbox(bool $actorFollowers = true): array
    {
        if ($actorFollowers) {
            $discovery = new Activitypub_explorer();
            $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
            foreach ($followers as $sub) {
                try {
                    $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
                } catch (Exception $e) {
                    // Not an ActivityPub Remote Follower, let it go
                }
            }
            unset($discovery);
        }

        $to_inboxes = [];
        foreach ($this->to as $to_profile) {
            $i = $to_profile->get_inbox();
            // Prevent delivering to self
            if ($i == common_local_url('apInbox')) {
                continue;
            }
            $to_inboxes[] = $i;
        }

        return array_unique($to_inboxes);
    }
}


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr ""

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr ""

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr ""

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr ""

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr ""


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 01:23+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: English (United Kingdom) (http://www.transifex.com/gnu-social/gnu-social/language/en_GB/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_GB\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Follow people across social networks that implement ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Remote Profile"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Sent from %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Not a valid WebFinger address (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Not a valid WebFinger address."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Could not find a valid profile for \"%s\"."


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 01:23+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: Portuguese (http://www.transifex.com/gnu-social/gnu-social/language/pt/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Segue pessoas em redes sociais que implementam ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Perfil Remoto"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Enviado por %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Endereço WebFinger inválido (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Endereço WebFinger inválido."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Não foi possível encontrar um perfil válido para \"%s\"."


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 02:24+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/gnu-social/gnu-social/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt_BR\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Segue pessoas em redes sociais que implementam ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Perfil Remoto"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Enviado por %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Endereço WebFinger inválido (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Endereço WebFinger inválido."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Não foi possível encontrar um perfil válido para \"%s\"."


code:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
</phpunit>


code:
#!/usr/bin/env php
<?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/
 */

define('INSTALLDIR', dirname(__DIR__, 3));
define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');

$shortoptions = 'u:af';
$longoptions = ['uri=', 'all', 'force'];

$helptext = <<<END_OF_HELP
update_activitypub_profiles.php [options]
Refetch / update ActivityPub RSA keys, profile info and avatars. Useful if you
do something like accidentally delete your avatars directory when
you have no backup.

    -u --uri ActivityPub profile URI to update
    -a --all update all

END_OF_HELP;

require_once INSTALLDIR.'/scripts/commandline.inc';

$quiet = have_option('q', 'quiet');

if (!$quiet) {
    echo "ActivityPub Profiles updater will now start!\n";
    echo "Summoning Diogo Cordeiro, Richard Stallman and Chuck Norris to help us with this task!\n";
}

if (have_option('u', 'uri')) {
    $uri = get_option_value('u', 'uri');
    $discovery = new Activitypub_explorer();
    $discovery = $discovery->lookup($uri);
    if (empty($discovery)) {
        echo "Bad URI\n";
        exit(1);
    }
    $user = $discovery->lookup($uri)[0];
    try {
        $res = Activitypub_explorer::get_remote_user_activity($uri);
    } catch (Exception $e) {
        echo $e->getMessage()."\n";
        exit(1);
    }
    if (!$quiet) {
        echo "Updated ".Activitypub_profile::update_profile($user, $res)->getBestName()."\n";
    }
} elseif (!have_option('a', 'all')) {
    show_help();
    exit(1);
}

$user = new Activitypub_profile();
$cnt = $user->find();
if (!empty($cnt)) {
    if (!$quiet) {
        echo "Found {$cnt} ActivityPub profiles:\n";
    }
} else {
    if (have_option('u', 'uri')) {
        if (!$quiet) {
            echo "Couldn't find an existing ActivityPub profile with that URI.\n";
        }
    } else {
        if (!$quiet) {
            echo "Couldn't find any existing ActivityPub profiles.\n";
        }
    }
    exit(0);
}
while ($user->fetch()) {
    try {
        $res = Activitypub_explorer::get_remote_user_activity($user->uri);
        $updated_profile = Activitypub_profile::update_profile($user, $res);
        if (!$quiet) {
            echo "Updated ".$updated_profile->getBestName()."\n";
        }
    } catch (Exception $e) {
        // let it go
    }
}


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

namespace Tests;

trait CreatesApplication
{
    /**
     * Creates the application.
     *
     * @return todo
     */
    public static function createApplication()
    {
        if (!defined('INSTALLDIR')) {
            define('INSTALLDIR', __DIR__ . '/../../../');
        }
        if (!defined('GNUSOCIAL')) {
            define('GNUSOCIAL', true);
        }
        if (!defined('STATUSNET')) {
            define('STATUSNET', true);  // compatibility
        }

        require INSTALLDIR . '/lib/common.php';

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

namespace Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    use CreatesApplication;

    protected function setUp()
    {
        $this->createApplication();
    }
}


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

require 'AcceptHeader.php';

class ContainerTest extends \PHPUnit_Framework_TestCase
{
    public function testHeader1()
    {
        $acceptHeader = new AcceptHeader('audio/*; q=0.2, audio/basic');
        $this->assertEquals('audio/basic', $this->_getMedia($acceptHeader[0]));
        $this->assertEquals('audio/*; q=0.2', $this->_getMedia($acceptHeader[1]));
    }

    public function testHeader2()
    {
        $acceptHeader = new AcceptHeader('text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5');
        $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0]));
        $this->assertEquals('text/html; q=0.7', $this->_getMedia($acceptHeader[1]));
        $this->assertEquals('*/*; q=0.5', $this->_getMedia($acceptHeader[2]));
        $this->assertEquals('text/html; level=2; q=0.4', $this->_getMedia($acceptHeader[3]));
        $this->assertEquals('text/*; q=0.3', $this->_getMedia($acceptHeader[4]));
    }

    public function testHeader3()
    {
        $acceptHeader = new AcceptHeader('text/*, text/html, text/html;level=1, */*');
        $this->assertEquals('text/html; level=1', $this->_getMedia($acceptHeader[0]));
        $this->assertEquals('text/html', $this->_getMedia($acceptHeader[1]));
        $this->assertEquals('text/*', $this->_getMedia($acceptHeader[2]));
        $this->assertEquals('*/*', $this->_getMedia($acceptHeader[3]));
    }

    private function _getMedia(array $mediaType)
    {
        $str = $mediaType['type'] . '/' . $mediaType['subtype'];
        if (!empty($mediaType['params'])) {
            foreach ($mediaType['params'] as $k => $v) {
                $str .= '; ' . $k . '=' . $v;
            }
        }
        return $str;
    }
}


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

namespace Tests\Unit;

use Tests\TestCase;

class ProfileObjectTest extends TestCase
{
    public function testLibraryInstalled()
    {
        $this->assertTrue(class_exists('\Activitypub_profile'));
    }

    public function testActivitypubProfile()
    {
        // Mimic proper ACCEPT header
        $_SERVER['HTTP_ACCEPT'] = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams';

        /* Test do_insert() */
        $aprofile = new \Activitypub_profile();
        $aprofile->uri = 'https://testinstance.net/index.php/user/1';
        $aprofile->nickname = 'test1';
        $aprofile->fullname = 'Test User 1';
        $aprofile->bio      = 'I am a nice test 1 guy';
        $aprofile->inboxuri = "https://testinstance.net/index.php/user/1/inbox.json";
        $aprofile->sharedInboxuri = "https://testinstance.net/inbox.json";
        $aprofile->do_insert();

        /* Test local_profile() */
        $profile = $aprofile->local_profile();

        /* Test from_profile() and create_from_local_profile() */
        $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::from_profile($profile)));

        /* Create Keys for Test User 1 */
        $apRSA = new \Activitypub_rsa();
        $apRSA->profile_id = $profile->getID();
        \Activitypub_rsa::generate_keys($apRSA->private_key, $apRSA->public_key);
        $apRSA->store_keys();

        /* Test profile_to_array() */
        // Fetch ActivityPub Actor Object representation
        $profile_array = \Activitypub_profile::profile_to_array($profile);
        // Check type
        $this->assertTrue(is_array($profile_array));
        // Test with Explorer's Profile Tester
        $this->assertTrue(\Activitypub_explorer::validate_remote_response($profile_array));

        /* Test get_inbox() */
        $this->assertTrue($aprofile->sharedInboxuri == $aprofile->get_inbox());

        /* Test getUri() */
        $this->assertTrue($aprofile->uri == $aprofile->getUri());

        /* Test getUrl() */
        $this->assertTrue($profile->getUrl() == $aprofile->getUrl());

        /* Test getID() */
        $this->assertTrue($profile->getID() == $aprofile->getID());

        /* Test fromUri() */
        $this->assertTrue($this->compare_aprofiles($aprofile, \Activitypub_profile::fromUri($aprofile->uri)));

        /* Remove Remote User Test 1 */
        $old_id = $profile->getID();
        $apRSA->delete();
        $aprofile->delete();
        $profile->delete();
        // Check if successfuly removed
        try {
            \Profile::getById($old_id);
            $this->assertTrue(false);
        } catch (\NoResultException $e) {
            $this->assertTrue(true);
        }

        /* Test ensure_web_finger() */
        // TODO: Maybe elaborate on this function's tests
        try {
            \Activitypub_profile::ensure_webfinger('test1@testinstance.net');
            $this->assertTrue(false);
        } catch (\Exception $e) {
            $this->assertTrue($e->getMessage() == 'Not a valid webfinger address.' ||
                              $e->getMessage() == 'Not a valid webfinger address (via cache).');
        }
    }

    // Helpers

    private function compare_profiles(\Profile $a, \Profile $b)
    {
        if (($av = $a->getID()) != ($bv = $b->getID())) {
            throw new Exception('Compare Profiles 1 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getNickname()) != ($bv = $b->getNickname())) {
            throw new Exception('Compare Profiles 2 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getFullname()) != ($bv = $b->getFullname())) {
            throw new Exception('Compare Profiles 3 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getUrl()) != ($bv = $b->getUrl())) {
            throw new Exception('Compare Profiles 4 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getDescription()) != ($bv = $b->getDescription())) {
            throw new Exception('Compare Profiles 5 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getLocation()) != ($bv = $b->getLocation())) {
            throw new Exception('Compare Profiles 6 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getNickname()) != ($bv = $b->getNickname())) {
            throw new Exception('Compare Profiles 7 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->lat) != ($bv = $b->lat)) {
            throw new Exception('Compare Profiles 8 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->lon) != ($bv = $b->lon)) {
            throw new Exception('Compare Profiles 9 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        return true;
    }

    private function compare_aprofiles(\Activitypub_profile $a, \Activitypub_profile $b)
    {
        if (($av = $a->getUri()) != ($bv = $b->getUri())) {
            throw new Exception('Compare AProfiles 1 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getUrl()) != ($bv = $b->getUrl())) {
            throw new Exception('Compare AProfiles 2 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->getID()) != ($bv = $b->getID())) {
            throw new Exception('Compare AProfiles 3 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->profile_id) != ($bv = $b->profile_id)) {
            throw new Exception('Compare AProfiles 4 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->inboxuri) != ($bv = $b->inboxuri)) {
            throw new Exception('Compare AProfiles 5 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

        if (($av = $a->sharedInboxuri) != ($bv = $b->sharedInboxuri)) {
            throw new Exception('Compare AProfiles 6 Fail: $a: '.$av.' is different from $b: '.$bv);
        }

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

namespace Tests\Unit;

use Tests\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $this->assertTrue(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/
 */

namespace Tests\Unit;

use Tests\TestCase;
use GuzzleHttp\Client;
use GuzzleHttp\Handler\CurlHandler;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
use HttpSignatures\Context;
use HttpSignatures\GuzzleHttpSignatures;

class HTTPSignatureTest extends TestCase
{
    /**
     * @var Context
     */
    private $context;
    /**
     * @var Client
     */
    private $client;
    /**
     * @var
     */
    private $history = [];

    public function testLibraryInstalled()
    {
        $this->assertTrue(class_exists('\GuzzleHttp\Client'));
        $this->assertTrue(class_exists('\HttpSignatures\Context'));
        $this->assertTrue(class_exists('\HttpSignatures\GuzzleHttpSignatures'));
    }

    public function setUp()
    {
        $this->context = new Context([
            'keys' => ['pda' => 'secret'],
            'algorithm' => 'hmac-sha256',
            'headers' => ['(request-target)', 'date'],
        ]);
        $stack = new HandlerStack();
        $stack->setHandler(new MockHandler([
            new Response(200, ['Content-Length' => 0]),
        ]));
        $stack->push(GuzzleHttpSignatures::middlewareFromContext($this->context));
        $stack->push(Middleware::history($this->history));
        $this->client = new Client(['handler' => $stack]);
    }
    /**
     * test signing a message
     */
    public function testGuzzleRequestHasExpectedHeaders()
    {
        $this->client->get('/path?query=123', [
            'headers' => ['date' => 'today', 'accept' => 'llamas']
        ]);
        // get last request
        $message = end($this->history);
        /** @var Request $request */
        $request = $message['request'];
        /** @var Response $response */
        $response = $message['request'];
        $expectedString = implode(
            ',',
            [
                'keyId="pda"',
                'algorithm="hmac-sha256"',
                'headers="(request-target) date"',
                'signature="SFlytCGpsqb/9qYaKCQklGDvwgmrwfIERFnwt+yqPJw="',
            ]
        );
        $this->assertEquals(
            [$expectedString],
            $request->getHeader('Signature')
        );
        $this->assertEquals(
            ['Signature ' . $expectedString],
            $request->getHeader('Authorization')
        );
    }
    /**
     * test signing a message with a URL that doesn't contain a ?query
     */
    public function testGuzzleRequestHasExpectedHeaders2()
    {
        $this->client->get('/path', [
            'headers' => ['date' => 'today', 'accept' => 'llamas']
        ]);
        // get last request
        $message = end($this->history);
        /** @var Request $request */
        $request = $message['request'];
        /** @var Response $response */
        $response = $message['request'];
        $expectedString = implode(
            ',',
            [
                'keyId="pda"',
                'algorithm="hmac-sha256"',
                'headers="(request-target) date"',
                'signature="DAtF133khP05pS5Gh8f+zF/UF7mVUojMj7iJZO3Xk4o="',
            ]
        );
        $this->assertEquals(
            [$expectedString],
            $request->getHeader('Signature')
        );
        $this->assertEquals(
            ['Signature ' . $expectedString],
            $request->getHeader('Authorization')
        );
    }
    public function getVerifyGuzzleRequestVectors()
    {
        return [
            /* path, headers */
            ['/path?query=123', ['date' => 'today', 'accept' => 'llamas']],
            ['/path?z=zebra&a=antelope', ['date' => 'today']],
        ];
    }
    /**
     * @dataProvider getVerifyGuzzleRequestVectors
     * @param string $path
     * @param array $headers
     */
    public function testVerifyGuzzleRequest($path, $headers)
    {
        $this->client->get($path, ['headers' => $headers]);
        // get last request
        $message = end($this->history);
        /** @var Request $request */
        $request = $message['request'];
        /** @var Response $response */
        $response = $message['request'];
        $this->assertTrue($this->context->verifier()->isValid($request));
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2011,2012, StatusNet, Inc.
 *
 * ActivitySpam Plugin
 *
 * PHP version 5
 *
 * 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.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Spam
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011,2012 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    // This check helps protect against security problems;
    // your code file can't be executed directly from the web.
    exit(1);
}

/**
 * Check new notices with activity spam service.
 *
 * @category  Spam
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2011,2012 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class ActivitySpamPlugin extends Plugin
{
    const PLUGIN_VERSION = '2.0.0';

    public $server = null;
    public $hideSpam = false;

    const REVIEWSPAM = 'ActivitySpamPlugin::REVIEWSPAM';
    const TRAINSPAM = 'ActivitySpamPlugin::TRAINSPAM';

    /**
     * Initializer
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */
    function initialize()
    {
        $this->filter = new SpamFilter(common_config('activityspam', 'server'),
                                       common_config('activityspam', 'consumerkey'),
                                       common_config('activityspam', 'secret'));

        $this->hideSpam = common_config('activityspam', 'hidespam');

        // Let DB_DataObject find Spam_score

        common_config_set('db', 'class_location',
                          common_config('db', 'class_location') .':'.dirname(__FILE__));

        return true;
    }

    /**
     * Database schema setup
     *
     * @see Schema
     * @see ColumnDef
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */

    function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('spam_score', Spam_score::schemaDef());

        Spam_score::upgrade();

        return true;
    }

    /**
     * When a notice is saved, check its spam score
     *
     * @param Notice $notice Notice that was just saved
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */

    function onEndNoticeSave($notice)
    {
        try {

            $result = $this->filter->test($notice);

            $score = Spam_score::saveNew($notice, $result);

            $this->log(LOG_INFO, "Notice " . $notice->id . " has spam score " . $score->score);

        } catch (Exception $e) {
            // Log but continue
            $this->log(LOG_ERR, $e->getMessage());
        }

        return true;
    }

    function onNoticeDeleteRelated($notice) {
        $score = Spam_score::getKV('notice_id', $notice->id);
        if (!empty($score)) {
            $score->delete();
        }
        return true;
    }

    function onUserRightsCheck($profile, $right, &$result) {
        switch ($right) {
        case self::REVIEWSPAM:
        case self::TRAINSPAM:
            $result = ($profile->hasRole(Profile_role::MODERATOR) || $profile->hasRole('modhelper'));
            return false;
        default:
            return true;
        }
    }

    function onGetSpamFilter(&$filter) {
        $filter = $this->filter;
        return false;
    }

    function onEndShowNoticeOptionItems($nli)
    {
        $profile = Profile::current();

        if (!empty($profile) && $profile->hasRight(self::TRAINSPAM)) {

            $notice = $nli->getNotice();
            $out = $nli->getOut();

            if (!empty($notice)) {

                $score = Spam_score::getKV('notice_id', $notice->id);

                if (empty($score)) {
                    // If it's empty, we can train it.
                    $form = new TrainSpamForm($out, $notice);
                    $form->show();
                } else if ($score->is_spam) {
                    $form = new TrainHamForm($out, $notice);
                    $form->show();
                } else if (!$score->is_spam) {
                    $form = new TrainSpamForm($out, $notice);
                    $form->show();
                }
            }
        }

        return true;
    }

    /**
     * Map URLs to actions
     *
     * @param URLMapper $m path-to-action mapper
     *
     * @return boolean hook value; true means continue processing, false means stop.
     */

    public function onRouterInitialized(URLMapper $m)
    {
        $m->connect('main/train/spam',
                    ['action' => 'train',
                     'category' => 'spam']);
        $m->connect('main/train/ham',
                    ['action' => 'train',
                     'category' => 'ham']);
        $m->connect('main/spam',
                    ['action' => 'spam']);
        return true;
    }

    function onEndShowStyles($action)
    {
        $action->element('style', null,
                         '.form-train-spam input.submit { background: url('.$this->path('icons/bullet_black.png').') no-repeat 0px 0px } ' . "\n" .
                         '.form-train-ham input.submit { background: url('.$this->path('icons/exclamation.png').') no-repeat 0px 0px } ');
        return true;
    }

    function onEndPublicGroupNav($nav)
    {
        $user = common_current_user();

        if (!empty($user) && $user->hasRight(self::REVIEWSPAM)) {
            $nav->out->menuItem(common_local_url('spam'),
                                _m('MENU','Spam'),
                                // TRANS: Menu item title in search group navigation panel.
                                _('Notices marked as spam'),
                                $nav->actionName == 'spam',
                                'nav_timeline_spam');
        }

        return true;
    }

    public function onPluginVersion(array &$versions): bool
    {
        $versions[] = array('name' => 'ActivitySpam',
                            'version' => self::PLUGIN_VERSION,
                            'author' => 'Evan Prodromou',
                            'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/ActivitySpam',
                            'description' =>
                            _m('Test notices against the Activity Spam service.'));
        return true;
    }

    function onEndNoticeInScope($notice, $profile, &$bResult)
    {
        if ($this->hideSpam) {
            if ($bResult) {

                $score = Spam_score::getKV('notice_id', $notice->id);

                if (!empty($score) && $score->is_spam) {
                    if (empty($profile) ||
                        ($profile->id !== $notice->profile_id &&
                         !$profile->hasRight(self::REVIEWSPAM))) {
                        $bResult = false;
                    }
                }
            }
        }

        return true;
    }

    /**
     * Pre-cache our spam scores if needed.
     */
    function onEndNoticeListPrefill(array &$notices, array &$profiles, array $notice_ids, Profile $scoped=null) {
        if ($this->hideSpam) {
            Spam_score::multiGet('notice_id', $notice_ids);
        }
        return true;
    }
}


code:
The ActivitySpam plugin is a spam filter for GNU social.

It needs to connect to a activityspam server.
You can run one yourself: https://github.com/e14n/activityspam
Or use a public instance ( ex: https://spamicity.info/ )

Installation
============
add "addPlugin('ActivitySpam');"
to the bottom of your config.php

Settings
========
server: URL to the activityspam server
consumerkey: The "key" provided by the activityspam server after you've registered and configured an account.
secret: The "secret" provided by the activityspam server after you've registered and configured an account

Example
=======
$config['activityspam']['server'] = 'https://spamicity.info/';
$config['activityspam']['consumerkey'] = 'CONSUMER_KEY';
$config['activityspam']['secret'] = 'SECRET';
addPlugin('ActivitySpam');

path: plugins/ActivitySpam/README


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2012, StatusNet, Inc.
 *
 * Stream of latest spam messages
 *
 * PHP version 5
 *
 * 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.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @category  Spam
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2012 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

if (!defined('STATUSNET')) {
    // This check helps protect against security problems;
    // your code file can't be executed directly from the web.
    exit(1);
}

require_once INSTALLDIR . '/lib/notices/noticelist.php';

/**
 * SpamAction
 *
 * Shows the latest spam on the service
 *
 * @category  Spam
 * @package   StatusNet
 * @author    Evan Prodromou <evan@status.net>
 * @copyright 2012 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */
class SpamAction extends Action
{
    var $page = null;
    var $notices = null;

    function title()
    {
        return _("Latest Spam");
    }

    /**
     * For initializing members of the class.
     *
     * @param array $args misc. arguments
     *
     * @return boolean true
     * @throws ClientException
     */

    function prepare(array $args = [])
    {
        parent::prepare($args);

        $this->page = ($this->arg('page')) ? ($this->arg('page') + 0) : 1;

        // User must be logged in.

        if (!$this->scoped instanceof Profile) {
            throw new ClientException(_("You must be logged in to review."), 403);
        }

        // User must have the right to review spam

        if (!$this->scoped->hasRight(ActivitySpamPlugin::REVIEWSPAM)) {
            throw new ClientException(_('You cannot review spam on this site.'), 403);
        }

        $stream = new SpamNoticeStream($this->scoped);

        $this->notices = $stream->getNotices(($this->page - 1) * NOTICES_PER_PAGE,
            NOTICES_PER_PAGE + 1);

        if ($this->page > 1 && $this->notices->N == 0) {
            throw new ClientException(_('No such page.'), 404);
        }

        return true;
    }

    /**
     * Handler method
     *
     * @return void
     */

    function handle()
    {
        parent::handle();

        $this->showPage();
    }

    /**
     * Fill the content area
     *
     * Shows a list of the notices in the public stream, with some pagination
     * controls.
     *
     * @return void
     */

    function showContent()
    {
        $nl = new NoticeList($this->notices, $this);

        $cnt = $nl->show();

        if ($cnt == 0) {
            $this->showEmptyList();
        }

        $this->pagination($this->page > 1,
            $cnt > NOTICES_PER_PAGE,
            $this->page,
            'spam');
    }

    function showEmptyList()
    {
        // TRANS: Text displayed for public feed when there are no public notices.
        $message = _('This is the timeline of spam messages for %%site.name%% but none have been detected yet.');

        $this->elementStart('div', 'guide');
        $this->raw(common_markup_to_html($message));
        $this->elementEnd('div');
    }

    /**
     * Return true if read only.
     *
     * MAY override
     *
     * @param array $args other arguments
     *
     * @return boolean is read only action?
     */

    function isReadOnly($args)
    {
        return true;
    }
}