From 2f1ddd828093e069bc6ba40ebe42d8eb6a53319e Mon Sep 17 00:00:00 2001 From: Diogo Cordeiro Date: Sat, 11 May 2019 12:27:21 +0100 Subject: [PATCH] [CORE] Add ActivityPub plugin This is not the same as the one in https://notabug.org/diogo/gnu-social-activitypub-plugin Differences to the first "release" -> Doesn't use guzzle nor has any composer dependencies -> Supports HTTP Signatures -> Has basic l10n/i18n -> Some minor bug fixes --- lib/default.php | 1 + plugins/ActivityPub/ActivityPubPlugin.php | 1017 +++++++++++++++++ plugins/ActivityPub/CONTRIBUTING.md | 93 ++ plugins/ActivityPub/COPYING | 661 +++++++++++ plugins/ActivityPub/README.md | 42 + .../ActivityPub/actions/apactorfollowers.php | 134 +++ .../ActivityPub/actions/apactorfollowing.php | 133 +++ plugins/ActivityPub/actions/apactorliked.php | 153 +++ plugins/ActivityPub/actions/apactoroutbox.php | 134 +++ .../ActivityPub/actions/apactorprofile.php | 75 ++ plugins/ActivityPub/actions/apinbox.php | 135 +++ plugins/ActivityPub/actions/apnotice.php | 67 ++ .../classes/Activitypub_accept.php | 86 ++ .../classes/Activitypub_announce.php | 57 + .../classes/Activitypub_attachment.php | 67 ++ .../classes/Activitypub_create.php | 85 ++ .../classes/Activitypub_delete.php | 58 + .../ActivityPub/classes/Activitypub_error.php | 53 + .../classes/Activitypub_follow.php | 91 ++ .../ActivityPub/classes/Activitypub_like.php | 58 + .../classes/Activitypub_mention_tag.php | 57 + .../classes/Activitypub_notice.php | 252 ++++ .../Activitypub_pending_follow_requests.php | 109 ++ .../classes/Activitypub_profile.php | 476 ++++++++ .../classes/Activitypub_reject.php | 55 + .../ActivityPub/classes/Activitypub_rsa.php | 179 +++ .../ActivityPub/classes/Activitypub_tag.php | 55 + .../ActivityPub/classes/Activitypub_undo.php | 87 ++ plugins/ActivityPub/doc/index.html | 24 + .../ActivityPub/doc/objects_and_activities.md | 101 ++ plugins/ActivityPub/doc/openapi.json | 673 +++++++++++ plugins/ActivityPub/lib/AcceptHeader.php | 116 ++ .../lib/Activitypub_activityverb2.php | 98 ++ plugins/ActivityPub/lib/discoveryhints.php | 158 +++ plugins/ActivityPub/lib/explorer.php | 482 ++++++++ plugins/ActivityPub/lib/httpsignature.php | 139 +++ plugins/ActivityPub/lib/inbox_handler.php | 324 ++++++ plugins/ActivityPub/lib/postman.php | 348 ++++++ plugins/ActivityPub/locale/ActivityPub.pot | 51 + .../locale/en_GB/LC_MESSAGES/ActivityPub.po | 54 + .../locale/pt/LC_MESSAGES/ActivityPub.po | 54 + .../locale/pt_BR/LC_MESSAGES/ActivityPub.po | 54 + plugins/ActivityPub/phpunit.xml | 25 + .../scripts/update_activitypub_profiles.php | 104 ++ .../ActivityPub/tests/CreatesApplication.php | 52 + plugins/ActivityPub/tests/TestCase.php | 39 + .../tests/Unit/AcceptHeaderTest.php | 67 ++ .../tests/Unit/ActivitypubProfileTest.php | 183 +++ .../ActivityPub/tests/Unit/ExampleTest.php | 42 + .../tests/Unit/HTTPSignatureTest.php | 165 +++ 50 files changed, 7823 insertions(+) create mode 100644 plugins/ActivityPub/ActivityPubPlugin.php create mode 100644 plugins/ActivityPub/CONTRIBUTING.md create mode 100644 plugins/ActivityPub/COPYING create mode 100644 plugins/ActivityPub/README.md create mode 100644 plugins/ActivityPub/actions/apactorfollowers.php create mode 100644 plugins/ActivityPub/actions/apactorfollowing.php create mode 100644 plugins/ActivityPub/actions/apactorliked.php create mode 100644 plugins/ActivityPub/actions/apactoroutbox.php create mode 100644 plugins/ActivityPub/actions/apactorprofile.php create mode 100644 plugins/ActivityPub/actions/apinbox.php create mode 100644 plugins/ActivityPub/actions/apnotice.php create mode 100644 plugins/ActivityPub/classes/Activitypub_accept.php create mode 100644 plugins/ActivityPub/classes/Activitypub_announce.php create mode 100644 plugins/ActivityPub/classes/Activitypub_attachment.php create mode 100644 plugins/ActivityPub/classes/Activitypub_create.php create mode 100644 plugins/ActivityPub/classes/Activitypub_delete.php create mode 100644 plugins/ActivityPub/classes/Activitypub_error.php create mode 100644 plugins/ActivityPub/classes/Activitypub_follow.php create mode 100644 plugins/ActivityPub/classes/Activitypub_like.php create mode 100644 plugins/ActivityPub/classes/Activitypub_mention_tag.php create mode 100644 plugins/ActivityPub/classes/Activitypub_notice.php create mode 100644 plugins/ActivityPub/classes/Activitypub_pending_follow_requests.php create mode 100644 plugins/ActivityPub/classes/Activitypub_profile.php create mode 100644 plugins/ActivityPub/classes/Activitypub_reject.php create mode 100644 plugins/ActivityPub/classes/Activitypub_rsa.php create mode 100644 plugins/ActivityPub/classes/Activitypub_tag.php create mode 100644 plugins/ActivityPub/classes/Activitypub_undo.php create mode 100644 plugins/ActivityPub/doc/index.html create mode 100644 plugins/ActivityPub/doc/objects_and_activities.md create mode 100644 plugins/ActivityPub/doc/openapi.json create mode 100644 plugins/ActivityPub/lib/AcceptHeader.php create mode 100644 plugins/ActivityPub/lib/Activitypub_activityverb2.php create mode 100644 plugins/ActivityPub/lib/discoveryhints.php create mode 100644 plugins/ActivityPub/lib/explorer.php create mode 100644 plugins/ActivityPub/lib/httpsignature.php create mode 100644 plugins/ActivityPub/lib/inbox_handler.php create mode 100644 plugins/ActivityPub/lib/postman.php create mode 100644 plugins/ActivityPub/locale/ActivityPub.pot create mode 100644 plugins/ActivityPub/locale/en_GB/LC_MESSAGES/ActivityPub.po create mode 100644 plugins/ActivityPub/locale/pt/LC_MESSAGES/ActivityPub.po create mode 100644 plugins/ActivityPub/locale/pt_BR/LC_MESSAGES/ActivityPub.po create mode 100644 plugins/ActivityPub/phpunit.xml create mode 100755 plugins/ActivityPub/scripts/update_activitypub_profiles.php create mode 100644 plugins/ActivityPub/tests/CreatesApplication.php create mode 100644 plugins/ActivityPub/tests/TestCase.php create mode 100644 plugins/ActivityPub/tests/Unit/AcceptHeaderTest.php create mode 100644 plugins/ActivityPub/tests/Unit/ActivitypubProfileTest.php create mode 100644 plugins/ActivityPub/tests/Unit/ExampleTest.php create mode 100644 plugins/ActivityPub/tests/Unit/HTTPSignatureTest.php diff --git a/lib/default.php b/lib/default.php index 150399bb46..40d7ae71eb 100644 --- a/lib/default.php +++ b/lib/default.php @@ -352,6 +352,7 @@ $default = 'Embed' => array(), 'OpenID' => array(), 'OpportunisticQM' => array(), + 'ActivityPub' => array(), 'OStatus' => array(), 'Poll' => array(), 'SimpleCaptcha' => array(), diff --git a/plugins/ActivityPub/ActivityPubPlugin.php b/plugins/ActivityPub/ActivityPubPlugin.php new file mode 100644 index 0000000000..3e27cff425 --- /dev/null +++ b/plugins/ActivityPub/ActivityPubPlugin.php @@ -0,0 +1,1017 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +// Ensure proper timezone +date_default_timezone_set('GMT'); + +// Import required files by the plugin +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'httpsignature.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'discoveryhints.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'AcceptHeader.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'explorer.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'postman.php'; +require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'inbox_handler.php'; + +// So that this isn't hardcoded everywhere +define('ACTIVITYPUB_BASE_ACTOR_URI', common_root_url().'index.php/user/'); +const ACTIVITYPUB_PUBLIC_TO = ['https://www.w3.org/ns/activitystreams#Public', + 'Public', + 'as:Public' + ]; + +/** + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class ActivityPubPlugin extends Plugin +{ + const PLUGIN_VERSION = '22.1.1dev'; + + /** + * Returns a Actor's URI from its local $profile + * Works both for local and remote users. + * + * @param Profile $profile Actor's local profile + * @return string Actor's URI + * @throws Exception + * @author Diogo Cordeiro + */ + public static function actor_uri($profile) + { + if ($profile->isLocal()) { + return ACTIVITYPUB_BASE_ACTOR_URI.$profile->getID(); + } else { + return $profile->getUri(); + } + } + + /** + * Returns a Actor's URL from its local $profile + * Works both for local and remote users. + * + * @param Profile $profile Actor's local profile + * @return string Actor's URL + * @throws Exception + * @author Diogo Cordeiro + */ + public static function actor_url($profile) + { + return ActivityPubPlugin::actor_uri($profile)."/"; + } + + /** + * Returns a notice from its URL. + * + * @author Diogo Cordeiro + * @param string $url Notice's URL + * @return Notice The Notice object + * @throws Exception This function or provides a Notice or fails with exception + */ + public static function grab_notice_from_url($url) + { + /* Offline Grabbing */ + try { + // Look for a known remote notice + return Notice::getByUri($url); + } catch (Exception $e) { + // Look for a local notice (unfortunately GNU social doesn't + // provide this functionality natively) + try { + $candidate = Notice::getByID(intval(substr($url, (strlen(common_local_url('apNotice', ['id' => 0]))-1)))); + if (common_local_url('apNotice', ['id' => $candidate->getID()]) === $url) { // Sanity check + return $candidate; + } else { + common_debug('ActivityPubPlugin Notice Grabber: '.$candidate->getUrl(). ' is different of '.$url); + } + } catch (Exception $e) { + common_debug('ActivityPubPlugin Notice Grabber: failed to find: '.$url.' offline.'); + } + } + + /* Online Grabbing */ + $client = new HTTPClient(); + $headers = []; + $headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; + $headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; + $response = $client->get($url, $headers); + $object = json_decode($response->getBody(), true); + Activitypub_notice::validate_note($object); + return Activitypub_notice::create_notice($object); + } + + /** + * Route/Reroute urls + * + * @param URLMapper $m + * @return void + * @throws Exception + */ + public function onRouterInitialized(URLMapper $m) + { + if (ActivityPubURLMapperOverwrite::should()) { + ActivityPubURLMapperOverwrite::variable( + $m, + 'user/:id', + ['id' => '[0-9]+'], + 'apActorProfile' + ); + + // Special route for webfinger purposes + ActivityPubURLMapperOverwrite::variable( + $m, + ':nickname', + ['nickname' => Nickname::DISPLAY_FMT], + 'apActorProfile' + ); + } + + // No .json here for convenience purposes on Notice grabber + $m->connect( + 'note/:id', + ['action' => 'apNotice'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'user/:id/liked.json', + ['action' => 'apActorLiked'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'user/:id/followers.json', + ['action' => 'apActorFollowers'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'user/:id/following.json', + ['action' => 'apActorFollowing'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'user/:id/inbox.json', + ['action' => 'apInbox'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'user/:id/outbox.json', + ['action' => 'apActorOutbox'], + ['id' => '[0-9]+'] + ); + + $m->connect( + 'inbox.json', + ['action' => 'apInbox'] + ); + } + + /** + * Plugin version information + * + * @param array $versions + * @return bool hook true + */ + public function onPluginVersion(array &$versions) + { + $versions[] = [ + 'name' => 'ActivityPub', + 'version' => self::PLUGIN_VERSION, + 'author' => 'Diogo Cordeiro', + 'homepage' => 'https://notabug.org/diogo/gnu-social/src/activitypub/plugins/ActivityPub', + // TRANS: Plugin description. + 'rawdescription' => _m('Follow people across social networks that implement '. + 'ActivityPub.') + ]; + return true; + } + + /** + * Plugin Nodeinfo information + * + * @param array $protocols + * @return bool hook true + */ + public function onNodeInfoProtocols(array &$protocols) + { + $protocols[] = "activitypub"; + return true; + } + + /** + * Adds an indicator on Remote ActivityPub profiles. + * + * @param HTMLOutputter $out + * @param Profile $profile + * @return boolean hook return value + * @throws Exception + * @author Diogo Cordeiro + */ + public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile) + { + if ($profile->isLocal()) { + return true; + } + try { + Activitypub_profile::from_profile($profile); + } catch (Exception $e) { + // Not a remote ActivityPub_profile! Maybe some other network + // that has imported a non-local user (e.g.: OStatus)? + return true; + } + + $out->elementStart('dl', 'entity_tags activitypub_profile'); + $out->element('dt', null, 'ActivityPub'); + $out->element('dd', null, _m('Remote Profile')); + $out->elementEnd('dl'); + + return true; + } + + /** + * Make sure necessary tables are filled out. + * + * @return boolean hook true + */ + public function onCheckSchema() + { + $schema = Schema::get(); + $schema->ensureTable('activitypub_profile', Activitypub_profile::schemaDef()); + $schema->ensureTable('activitypub_rsa', Activitypub_rsa::schemaDef()); + $schema->ensureTable('activitypub_pending_follow_requests', Activitypub_pending_follow_requests::schemaDef()); + return true; + } + + /******************************************************** + * WebFinger Events * + ********************************************************/ + + /** + * Get remote user's ActivityPub_profile via a identifier + * + * @author GNU social + * @author Diogo Cordeiro + * @param string $arg A remote user identifier + * @return Activitypub_profile|null Valid profile in success | null otherwise + */ + public static function pull_remote_profile($arg) + { + if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) { + // webfinger lookup + try { + return Activitypub_profile::ensure_web_finger($arg); + } catch (Exception $e) { + common_log(LOG_ERR, 'Webfinger lookup failed for ' . + $arg . ': ' . $e->getMessage()); + } + } + + // Look for profile URLs, with or without scheme: + $urls = []; + if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { + $urls[] = $arg; + } + if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) { + $schemes = array('http', 'https'); + foreach ($schemes as $scheme) { + $urls[] = "$scheme://$arg"; + } + } + + foreach ($urls as $url) { + try { + return Activitypub_profile::fromUri($url); + } catch (Exception $e) { + common_log(LOG_ERR, 'Profile lookup failed for ' . + $arg . ': ' . $e->getMessage()); + } + } + return null; + } + + /** + * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz + * + * @author GNU social + * @param string $text The text from which to extract webfinger IDs + * @param string $preMention Character(s) that signals a mention ('@', '!'...) + * @return array The matching IDs (without $preMention) and each respective position in the given string. + */ + public static function extractWebfingerIds($text, $preMention='@') + { + $wmatches = []; + $result = preg_match_all( + '/(? + */ + public function onEndWebFingerProfileLinks(XML_XRD &$xrd, Managed_DataObject $object) + { + if ($object->isPerson()) { + $link = new XML_XRD_Element_Link( + 'self', + ActivityPubPlugin::actor_uri($object->getProfile()), + 'application/activity+json' + ); + $xrd->links[] = clone ($link); + } + } + + /** + * Find any explicit remote mentions. Accepted forms: + * Webfinger: @user@example.com + * Profile link: + * @param Profile $sender + * @param string $text input markup text + * @param $mentions + * @return boolean hook return value + * @throws InvalidUrlException + * @author Diogo Cordeiro + * @example.com/mublog/user + * + * @author GNU social + */ + public function onEndFindMentions(Profile $sender, $text, &$mentions) + { + $matches = []; + + foreach (self::extractWebfingerIds($text, '@') as $wmatch) { + list($target, $pos) = $wmatch; + $this->log(LOG_INFO, "Checking webfinger person '$target'"); + $profile = null; + try { + $aprofile = Activitypub_profile::ensure_web_finger($target); + $profile = $aprofile->local_profile(); + } catch (Exception $e) { + $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage()); + continue; + } + assert($profile instanceof Profile); + + $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) + ? $profile->getNickname() // TODO: we could do getBestName() or getFullname() here + : $target; + $url = $profile->getUri(); + if (!common_valid_http_url($url)) { + $url = $profile->getUrl(); + } + $matches[$pos] = array('mentioned' => array($profile), + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen($target), + 'url' => $url); + } + + foreach (self::extractUrlMentions($text) as $wmatch) { + list($target, $pos) = $wmatch; + $schemes = array('https', 'http'); + foreach ($schemes as $scheme) { + $url = "$scheme://$target"; + $this->log(LOG_INFO, "Checking profile address '$url'"); + try { + $aprofile = Activitypub_profile::fromUri($url); + $profile = $aprofile->local_profile(); + $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ? + $profile->nickname : $target; + $matches[$pos] = array('mentioned' => array($profile), + 'type' => 'mention', + 'text' => $displayName, + 'position' => $pos, + 'length' => mb_strlen($target), + 'url' => $profile->getUrl()); + break; + } catch (Exception $e) { + $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage()); + } + } + } + + foreach ($mentions as $i => $other) { + // If we share a common prefix with a local user, override it! + $pos = $other['position']; + if (isset($matches[$pos])) { + $mentions[$i] = $matches[$pos]; + unset($matches[$pos]); + } + } + foreach ($matches as $mention) { + $mentions[] = $mention; + } + + return true; + } + + /** + * Allow remote profile references to be used in commands: + * sub update@status.net + * whois evan@identi.ca + * reply http://identi.ca/evan hey what's up + * + * @param Command $command + * @param string $arg + * @param Profile &$profile + * @return boolean hook return code + * @author GNU social + * @author Diogo Cordeiro + */ + public function onStartCommandGetProfile($command, $arg, &$profile) + { + try { + $aprofile = $this->pull_remote_profile($arg); + $profile = $aprofile->local_profile(); + } catch (Exception $e) { + // No remote ActivityPub profile found + return true; + } + + return false; + } + + /******************************************************** + * Discovery Events * + ********************************************************/ + + /** + * Profile URI for remote profiles. + * + * @author GNU social + * @author Diogo Cordeiro + * @param Profile $profile + * @param string $uri in/out + * @return mixed hook return code + */ + public function onStartGetProfileUri(Profile $profile, &$uri) + { + $aprofile = Activitypub_profile::getKV('profile_id', $profile->id); + if ($aprofile instanceof Activitypub_profile) { + $uri = $aprofile->getUri(); + return false; + } + return true; + } + + /** + * Profile from URI. + * + * @author GNU social + * @author Diogo Cordeiro + * @param string $uri + * @param Profile &$profile in/out param: Profile got from URI + * @return mixed hook return code + */ + public function onStartGetProfileFromURI($uri, &$profile) + { + try { + $explorer = new Activitypub_explorer(); + $profile = $explorer->lookup($uri)[0]; + return false; + } catch (Exception $e) { + return true; // It's not an ActivityPub profile as far as we know, continue event handling + } + } + + /******************************************************** + * Delivery Events * + ********************************************************/ + + /** + * Having established a remote subscription, send a notification to the + * remote ActivityPub profile's endpoint. + * + * @param Profile $profile subscriber + * @param Profile $other subscribee + * @return bool return value + * @throws HTTP_Request2_Exception + * @author Diogo Cordeiro + */ + public function onStartSubscribe(Profile $profile, Profile $other) + { + if (!$profile->isLocal() && $other->isLocal()) { + return true; + } + + try { + $other = Activitypub_profile::from_profile($other); + } catch (Exception $e) { + return true; // Let other plugin handle this instead + } + + $postman = new Activitypub_postman($profile, array($other)); + + $postman->follow(); + + return true; + } + + /** + * Notify remote server on unsubscribe. + * + * @param Profile $profile + * @param Profile $other + * @return bool return value + * @throws HTTP_Request2_Exception + * @author Diogo Cordeiro + */ + public function onStartUnsubscribe(Profile $profile, Profile $other) + { + if (!$profile->isLocal() && $other->isLocal()) { + return true; + } + + try { + $other = Activitypub_profile::from_profile($other); + } catch (Exception $e) { + return true; // Let other plugin handle this instead + } + + $postman = new Activitypub_postman($profile, array($other)); + + $postman->undo_follow(); + + return true; + } + + /** + * Notify remote users when their notices get favourited. + * + * @param Profile $profile of local user doing the faving + * @param Notice $notice Notice being favored + * @return bool return value + * @throws HTTP_Request2_Exception + * @throws InvalidUrlException + * @author Diogo Cordeiro + */ + public function onEndFavorNotice(Profile $profile, Notice $notice) + { + // Only distribute local users' favor actions, remote users + // will have already distributed theirs. + if (!$profile->isLocal()) { + return true; + } + + $other = []; + try { + $other[] = Activitypub_profile::from_profile($notice->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + foreach ($notice->getAttentionProfiles() as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent()->getAttentionProfiles(); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // This is not a reply to something (has no parent) + } catch (NoResultException $e) { + // Parent author's profile not found! Complain louder? + common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + } + } + + $postman = new Activitypub_postman($profile, $other); + + $postman->like($notice); + + return true; + } + + /** + * Notify remote users when their notices get de-favourited. + * + * @param Profile $profile of local user doing the de-faving + * @param Notice $notice Notice being favored + * @return bool return value + * @throws HTTP_Request2_Exception + * @throws InvalidUrlException + * @author Diogo Cordeiro + */ + public function onEndDisfavorNotice(Profile $profile, Notice $notice) + { + // Only distribute local users' favor actions, remote users + // will have already distributed theirs. + if (!$profile->isLocal()) { + return true; + } + + $other = []; + try { + $other[] = Activitypub_profile::from_profile($notice->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + foreach ($notice->getAttentionProfiles() as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent()->getAttentionProfiles(); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // This is not a reply to something (has no parent) + } catch (NoResultException $e) { + // Parent author's profile not found! Complain louder? + common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + } + } + + $postman = new Activitypub_postman($profile, $other); + + $postman->undo_like($notice); + + return true; + } + + /** + * Notify remote users when their notices get deleted + * + * @param $user + * @param $notice + * @return boolean hook flag + * @throws HTTP_Request2_Exception + * @throws InvalidUrlException + * @author Diogo Cordeiro + */ + public function onStartDeleteOwnNotice($user, $notice) + { + $profile = $user->getProfile(); + + // Only distribute local users' delete actions, remote users + // will have already distributed theirs. + if (!$profile->isLocal()) { + return true; + } + + $other = []; + + foreach ($notice->getAttentionProfiles() as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + $mentions = $notice->getParent()->getAttentionProfiles(); + foreach ($mentions as $to_profile) { + try { + $other[] = Activitypub_profile::from_profile($to_profile); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // This is not a reply to something (has no parent) + } catch (NoResultException $e) { + // Parent author's profile not found! Complain louder? + common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + } + } + + $postman = new Activitypub_postman($profile, $other); + $postman->delete($notice); + return true; + } + + /** + * Insert notifications for replies, mentions and repeats + * + * @param $notice + * @return boolean hook flag + * @throws EmptyPkeyValueException + * @throws HTTP_Request2_Exception + * @throws InvalidUrlException + * @throws ServerException + * @author Diogo Cordeiro + */ + public function onStartNoticeDistribute($notice) + { + assert($notice->id > 0); // Ignore if not a valid notice + + $profile = $notice->getProfile(); + + if (!$profile->isLocal()) { + return true; + } + + // Ignore for activity/non-post-verb notices + if (method_exists('ActivityUtils', 'compareVerbs')) { + $is_post_verb = ActivityUtils::compareVerbs( + $notice->verb, + [ActivityVerb::POST] + ); + } else { + $is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false); + } + if ($notice->source == 'activity' || !$is_post_verb) { + return true; + } + + $other = []; + foreach ($notice->getAttentionProfiles() as $mention) { + try { + $other[] = Activitypub_profile::from_profile($mention); + } catch (Exception $e) { + // Local user can be ignored + } + } + + // Is a reply? + if ($notice->reply_to) { + try { + $other[] = Activitypub_profile::from_profile($notice->getParent()->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + try { + foreach ($notice->getParent()->getAttentionProfiles() as $mention) { + try { + $other[] = Activitypub_profile::from_profile($mention); + } catch (Exception $e) { + // Local user can be ignored + } + } + } catch (NoParentNoticeException $e) { + // This is not a reply to something (has no parent) + } catch (NoResultException $e) { + // Parent author's profile not found! Complain louder? + common_log(LOG_ERR, "Parent notice's author not found: ".$e->getMessage()); + } + } + + // Is an Announce? + if ($notice->isRepeat()) { + $repeated_notice = Notice::getKV('id', $notice->repeat_of); + if ($repeated_notice instanceof Notice) { + try { + $other[] = Activitypub_profile::from_profile($repeated_notice->getProfile()); + } catch (Exception $e) { + // Local user can be ignored + } + + // That was it + $postman = new Activitypub_postman($profile, $other); + $postman->announce($repeated_notice); + return true; + } + } + + // That was it + $postman = new Activitypub_postman($profile, $other); + $postman->create_note($notice); + return true; + } + + /** + * Override the "from ActivityPub" bit in notice lists to link to the + * original post and show the domain it came from. + * + * @author Diogo Cordeiro + * @param $notice + * @param $name + * @param $url + * @param $title + * @return mixed hook return code + * @throws Exception + */ + public function onStartNoticeSourceLink($notice, &$name, &$url, &$title) + { + // If we don't handle this, keep the event handler going + if (!in_array($notice->source, array('ActivityPub', 'share'))) { + return true; + } + + try { + $url = $notice->getUrl(); + // If getUrl() throws exception, $url is never set + + $bits = parse_url($url); + $domain = $bits['host']; + if (substr($domain, 0, 4) == 'www.') { + $name = substr($domain, 4); + } else { + $name = $domain; + } + + // TRANS: Title. %s is a domain name. + $title = sprintf(_m('Sent from %s via ActivityPub'), $domain); + + // Abort event handler, we have a name and URL! + return false; + } catch (InvalidUrlException $e) { + // This just means we don't have the notice source data + return true; + } + } +} + +/** + * Plugin return handler + */ +class ActivityPubReturn +{ + /** + * Return a valid answer + * + * @param string $res + * @param int $code Status Code + * @return void + * @author Diogo Cordeiro + */ + public static function answer($res = '', $code = 202) + { + http_response_code($code); + header('Content-Type: application/activity+json'); + echo json_encode($res, JSON_UNESCAPED_SLASHES | (isset($_GET["pretty"]) ? JSON_PRETTY_PRINT : null)); + exit; + } + + /** + * Return an error + * + * @param string $m + * @param int $code Status Code + * @return void + * @author Diogo Cordeiro + */ + public static function error($m, $code = 400) + { + http_response_code($code); + header('Content-Type: application/activity+json'); + $res[] = Activitypub_error::error_message_to_array($m); + echo json_encode($res, JSON_UNESCAPED_SLASHES); + exit; + } +} + +/** + * Overwrites variables in URL-mapping + */ +class ActivityPubURLMapperOverwrite extends URLMapper +{ + /** + * Overwrites a route. + * + * @author Hannes Mannerheim + * @param URLMapper $m + * @param string $path + * @param string $paramPatterns + * @param string $newaction + * @return void + * @throws Exception + */ + public static function variable($m, $path, $paramPatterns, $newaction) + { + $m->connect($path, array('action' => $newaction), $paramPatterns); + $regex = self::makeRegex($path, $paramPatterns); + foreach ($m->variables as $n => $v) { + if ($v[1] == $regex) { + $m->variables[$n][0]['action'] = $newaction; + } + } + } + + /** + * Determines whether the route should or not be overwrited. + * If ACCEPT header isn't set false will be returned. + * + * @author Diogo Cordeiro + * @return boolean true if it should, false otherwise + */ + public static function should() + { + // Do not operate without Accept Header + if (!isset($_SERVER['HTTP_ACCEPT'])) { + return false; + } + + $mimes = [ + 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' => 0, + 'application/activity+json' => 1, + 'application/json' => 2, + 'application/ld+json' => 3 + ]; + + $acceptheader = new AcceptHeader($_SERVER['HTTP_ACCEPT']); + foreach ($acceptheader as $ah) { + if (isset($mimes[$ah['raw']])) { + return true; + } + } + + return false; + } +} diff --git a/plugins/ActivityPub/CONTRIBUTING.md b/plugins/ActivityPub/CONTRIBUTING.md new file mode 100644 index 0000000000..d38a0d5ffa --- /dev/null +++ b/plugins/ActivityPub/CONTRIBUTING.md @@ -0,0 +1,93 @@ +# Contributing + +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +# Coding Style +- We follow every [PSR-2](https://www.php-fig.org/psr/psr-2/) ... +- ... except camelCase, that's too bad, we use snake_case + +## Merge Request Process + +1. Ensure you strip any trailing spaces off +2. Increase the version numbers in any examples files and the README.md to the new version that this + Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). +3. You may merge the Pull Request in once you have the sign-off of two other developers, or if you + do not have permission to do that, you may request the second reviewer to merge it for you. + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +### Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at diogo@fc.up.pt. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/plugins/ActivityPub/COPYING b/plugins/ActivityPub/COPYING new file mode 100644 index 0000000000..dba13ed2dd --- /dev/null +++ b/plugins/ActivityPub/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/plugins/ActivityPub/README.md b/plugins/ActivityPub/README.md new file mode 100644 index 0000000000..270aa96d5f --- /dev/null +++ b/plugins/ActivityPub/README.md @@ -0,0 +1,42 @@ +# ActivityPub plugin for GNU social +(c) 2018-2019 Free Software Foundation, Inc + +This is the README file for GNU social's ActivityPub plugin. +It includes general information about the plugin. + +## About + +This plugin adds [ActivityPub](https://www.w3.org/TR/activitypub/) support to GNU social. + +## Credits + +* **[Diogo Cordeiro](https://www.diogo.site/)** + +## Special thanks + +* **[Daniel Supernault](https://github.com/dansup)** +* **[Mikael Nordfeldth](https://mmn-o.se/)** + +## License + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public +License along with this program, in the file "COPYING". If not, see +. + + IMPORTANT NOTE: The GNU Affero General Public License (AGPL) has + *different requirements* from the "regular" GPL. In particular, if + you make modifications to the plugin source code on your server, + you *MUST MAKE AVAILABLE* the modified version of the source code + to your users under the same license. This is a legal requirement + of using the software, and if you do not wish to share your + modifications, *YOU MAY NOT USE THIS PLUGIN*. diff --git a/plugins/ActivityPub/actions/apactorfollowers.php b/plugins/ActivityPub/actions/apactorfollowers.php new file mode 100644 index 0000000000..da8943ab86 --- /dev/null +++ b/plugins/ActivityPub/actions/apactorfollowers.php @@ -0,0 +1,134 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Actor's Followers Collection + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apActorFollowersAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Followers Collection request + * + * @return void + * @throws Exception + * @author Diogo Cordeiro + */ + protected function handle() + { + try { + $profile = Profile::getByID($this->trimmed('id')); + $profile_id = $profile->getID(); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Actor URI.', 404); + } + + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user.", 403); + } + + if (!isset($_GET["page"])) { + $page = 0; + } else { + $page = intval($this->trimmed('page')); + } + + if ($page < 0) { + ActivityPubReturn::error('Invalid page number.'); + } + + $since = ($page - 1) * PROFILES_PER_MINILIST; + $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; + + /* Calculate total items */ + $total_subs = $profile->subscriberCount(); + $total_pages = ceil($total_subs / PROFILES_PER_MINILIST); + + $res = [ + '@context' => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + 'id' => common_local_url('apActorFollowers', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), + 'type' => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), + 'totalItems' => $total_subs + ]; + + if ($page == 0) { + $res['first'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page=1'; + } else { + $res['orderedItems'] = $this->generate_followers($profile, $since, $limit); + $res['partOf'] = common_local_url('apActorFollowers', ['id' => $profile_id]); + + if ($page+1 < $total_pages) { + $res['next'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); + } + + if ($page > 1) { + $res['prev'] = common_local_url('apActorFollowers', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); + } + } + + ActivityPubReturn::answer($res); + } + + /** + * Generates a list of stalkers for a given profile. + * + * @param Profile $profile + * @param int $since + * @param int $limit + * @return array of URIs + * @throws Exception + * @author Diogo Cordeiro + */ + public static function generate_followers($profile, $since, $limit) + { + /* Fetch Followers */ + try { + $sub = $profile->getSubscribers($since, $limit); + } catch (NoResultException $e) { + // Just let the exception go on its merry way + } + + /* Get followers' URLs */ + $subs = []; + while ($sub->fetch()) { + $subs[] = ActivityPubPlugin::actor_uri($sub); + } + + return $subs; + } +} diff --git a/plugins/ActivityPub/actions/apactorfollowing.php b/plugins/ActivityPub/actions/apactorfollowing.php new file mode 100644 index 0000000000..600509f06f --- /dev/null +++ b/plugins/ActivityPub/actions/apactorfollowing.php @@ -0,0 +1,133 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Actor's Following Collection + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apActorFollowingAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Following Collection request + * + * @return void + * @throws Exception + * @author Diogo Cordeiro + */ + protected function handle() + { + try { + $profile = Profile::getByID($this->trimmed('id')); + $profile_id = $profile->getID(); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Actor URI.', 404); + } + + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user.", 403); + } + + if (!isset($_GET["page"])) { + $page = 0; + } else { + $page = intval($this->trimmed('page')); + } + + if ($page < 0) { + ActivityPubReturn::error('Invalid page number.'); + } + + $since = ($page - 1) * PROFILES_PER_MINILIST; + $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; + + /* Calculate total items */ + $total_subs = $profile->subscriptionCount(); + $total_pages = ceil($total_subs / PROFILES_PER_MINILIST); + + $res = [ + '@context' => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + 'id' => common_local_url('apActorFollowing', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), + 'type' => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), + 'totalItems' => $total_subs + ]; + + if ($page == 0) { + $res['first'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page=1'; + } else { + $res['orderedItems'] = $this->generate_following($profile, $since, $limit); + $res['partOf'] = common_local_url('apActorFollowing', ['id' => $profile_id]); + + if ($page+1 < $total_pages) { + $res['next'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); + } + + if ($page > 1) { + $res['prev'] = common_local_url('apActorFollowing', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); + } + } + + ActivityPubReturn::answer($res); + } + + /** + * Generates the list of those a given profile is stalking. + * + * @param Profile $profile + * @param int $since + * @param int $limit + * @return array of URIs + * @throws Exception + * @author Diogo Cordeiro + */ + public function generate_following($profile, $since, $limit) + { + /* Fetch Following */ + try { + $sub = $profile->getSubscribed($since, $limit); + } catch (NoResultException $e) { + // Just let the exception go on its merry way + } + + /* Get followed' URLs */ + $subs = []; + while ($sub->fetch()) { + $subs[] = ActivityPubPlugin::actor_uri($sub); + } + return $subs; + } +} diff --git a/plugins/ActivityPub/actions/apactorliked.php b/plugins/ActivityPub/actions/apactorliked.php new file mode 100644 index 0000000000..318eaab2d1 --- /dev/null +++ b/plugins/ActivityPub/actions/apactorliked.php @@ -0,0 +1,153 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Actor's Liked Collection + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apActorLikedAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Liked Collection request + * + * @return void + * @throws EmptyPkeyValueException + * @throws ServerException + * @author Diogo Cordeiro + */ + protected function handle() + { + try { + $profile = Profile::getByID($this->trimmed('id')); + $profile_id = $profile->getID(); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Actor URI.', 404); + } + + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user.", 403); + } + + $limit = intval($this->trimmed('limit')); + $since_id = intval($this->trimmed('since_id')); + $max_id = intval($this->trimmed('max_id')); + + $limit = empty($limit) ? 40 : $limit; // Default is 40 + $since_id = empty($since_id) ? null : $since_id; + $max_id = empty($max_id) ? null : $max_id; + + // Max is 80 + if ($limit > 80) { + $limit = 80; + } + + $fave = $this->fetch_faves($profile_id, $limit, $since_id, $max_id); + + $faves = array(); + while ($fave->fetch()) { + $faves[] = $this->pretty_fave(clone ($fave)); + } + + $res = [ + '@context' => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + 'id' => common_local_url('apActorLiked', ['id' => $profile_id]), + 'type' => 'OrderedCollection', + 'totalItems' => Fave::countByProfile($profile), + 'orderedItems' => $faves + ]; + + ActivityPubReturn::answer($res); + } + + /** + * Take a fave object and turns it in a pretty array to be used + * as a plugin answer + * + * @param Fave $fave_object + * @return array pretty array representating a Fave + * @throws EmptyPkeyValueException + * @throws ServerException + * @author Diogo Cordeiro + */ + protected function pretty_fave($fave_object) + { + $res = [ + 'created' => $fave_object->created, + 'object' => Activitypub_notice::notice_to_array(Notice::getByID($fave_object->notice_id)) + ]; + + return $res; + } + + /** + * Fetch faves + * + * @author Diogo Cordeiro + * @param int $user_id + * @param int $limit + * @param int $since_id + * @param int $max_id + * @return Fave fetchable fave collection + */ + private static function fetch_faves( + $user_id, + $limit = 40, + $since_id = null, + $max_id = null + ) { + $fav = new Fave(); + + $fav->user_id = $user_id; + + $fav->orderBy('modified DESC'); + + if ($since_id != null) { + $fav->whereAdd("notice_id > {$since_id}"); + } + + if ($max_id != null) { + $fav->whereAdd("notice_id < {$max_id}"); + } + + $fav->limit($limit); + + $fav->find(); + + return $fav; + } +} diff --git a/plugins/ActivityPub/actions/apactoroutbox.php b/plugins/ActivityPub/actions/apactoroutbox.php new file mode 100644 index 0000000000..2c3449949a --- /dev/null +++ b/plugins/ActivityPub/actions/apactoroutbox.php @@ -0,0 +1,134 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Inbox Request Handler + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apActorOutboxAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Outbox request + * + * @author Daniel Supernault + */ + protected function handle() + { + try { + $profile = Profile::getByID($this->trimmed('id')); + $profile_id = $profile->getID(); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Actor URI.', 404); + } + + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user.", 403); + } + + if (!isset($_GET["page"])) { + $page = 0; + } else { + $page = intval($this->trimmed('page')); + } + + if ($page < 0) { + ActivityPubReturn::error('Invalid page number.'); + } + + $since = ($page - 1) * PROFILES_PER_MINILIST; + $limit = (($page - 1) == 0 ? 1 : $page) * PROFILES_PER_MINILIST; + + /* Calculate total items */ + $total_notes = $profile->noticeCount(); + $total_pages = ceil($total_notes / PROFILES_PER_MINILIST); + + $res = [ + '@context' => [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + 'id' => common_local_url('apActorOutbox', ['id' => $profile_id]).(($page != 0) ? '?page='.$page : ''), + 'type' => ($page == 0 ? 'OrderedCollection' : 'OrderedCollectionPage'), + 'totalItems' => $total_notes + ]; + + if ($page == 0) { + $res['first'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page=1'; + } else { + $res['orderedItems'] = $this->generate_outbox($profile); + $res['partOf'] = common_local_url('apActorOutbox', ['id' => $profile_id]); + + if ($page+1 < $total_pages) { + $res['next'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'page='.($page+1 == 1 ? 2 : $page+1); + } + + if ($page > 1) { + $res['prev'] = common_local_url('apActorOutbox', ['id' => $profile_id]).'?page='.($page-1 <= 0 ? 1 : $page-1); + } + } + + ActivityPubReturn::answer($res); + } + + /** + * Generates a list of people following given profile. + * + * @param Profile $profile + * @return array of Notices + * @throws EmptyPkeyValueException + * @throws InvalidUrlException + * @throws ServerException + * @author Daniel Supernault + */ + public function generate_outbox($profile) + { + /* Fetch Notices */ + $notices = []; + $notice = $profile->getNotices(); + while ($notice->fetch()) { + $note = $notice; + + // TODO: Handle other types + if ($note->object_type == 'http://activitystrea.ms/schema/1.0/note') { + $notices[] = Activitypub_create::create_to_array( + ActivityPubPlugin::actor_uri($note->getProfile()), + Activitypub_notice::notice_to_array($note) + ); + } + } + + return $notices; + } +} diff --git a/plugins/ActivityPub/actions/apactorprofile.php b/plugins/ActivityPub/actions/apactorprofile.php new file mode 100644 index 0000000000..20d4608248 --- /dev/null +++ b/plugins/ActivityPub/actions/apactorprofile.php @@ -0,0 +1,75 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Actor's profile (Local users only) + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apActorProfileAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Actor Profile request + * + * @return void + * @throws InvalidUrlException + * @throws ServerException + * @author Diogo Cordeiro + */ + protected function handle() + { + if (!empty($id = $this->trimmed('id'))) { + try { + $profile = Profile::getByID($id); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Actor URI.', 404); + } + unset($id); + } else { + try { + $profile = User::getByNickname($this->trimmed('nickname'))->getProfile(); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid username.', 404); + } + } + + if (!$profile->isLocal()) { + ActivityPubReturn::error("This is not a local user.", 403); + } + + $res = Activitypub_profile::profile_to_array($profile); + + ActivityPubReturn::answer($res, 200); + } +} diff --git a/plugins/ActivityPub/actions/apinbox.php b/plugins/ActivityPub/actions/apinbox.php new file mode 100644 index 0000000000..e7f8663b9d --- /dev/null +++ b/plugins/ActivityPub/actions/apinbox.php @@ -0,0 +1,135 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Inbox Request Handler + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apInboxAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Inbox request + * + * @return void + * @throws ServerException + * @author Diogo Cordeiro + */ + protected function handle() + { + $path = !empty($this->trimmed('id')) ? common_local_url('apInbox', ['id' => $this->trimmed('id')]) : common_local_url('apInbox'); + $path = parse_url($path, PHP_URL_PATH); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + ActivityPubReturn::error('Only POST requests allowed.'); + } + + common_debug('ActivityPub Inbox: Received a POST request.'); + $body = $data = file_get_contents('php://input'); + common_debug('ActivityPub Inbox: Request contents: '.$data); + $data = json_decode(file_get_contents('php://input'), true); + + if (!isset($data['actor'])) { + ActivityPubReturn::error('Actor not found in the request.'); + } + + $actor = Activitypub_explorer::get_profile_from_url($data['actor']); + $aprofile = Activitypub_profile::from_profile($actor); + + $actor_public_key = new Activitypub_rsa(); + $actor_public_key = $actor_public_key->ensure_public_key($actor); + + common_debug('ActivityPub Inbox: HTTP Signature: Validation will now start!'); + + $headers = $this->get_all_headers(); + common_debug('ActivityPub Inbox: Request Headers: '.print_r($headers, true)); + + if (!isset($headers['signature'])) { + common_debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.'); + ActivityPubReturn::error('Missing Signature header.', 400); + } + + // Extract the signature properties + $signatureData = HTTPSignature::parseSignatureHeader($headers['signature']); + common_debug('ActivityPub Inbox: HTTP Signature Data: '.print_r($signatureData, true)); + if (isset($signatureData['error'])) { + common_debug('ActivityPub Inbox: HTTP Signature: '.json_encode($signatureData, true)); + ActivityPubReturn::error(json_encode($signatureData, true), 400); + } + + list($verified, $headers) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body); + + // If the signature fails verification the first time, update profile as it might have change public key + if($verified !== 1) { + $res = Activitypub_explorer::get_remote_user_activity($aprofile->getUri()); + $actor = Activitypub_profile::update_profile($aprofile, $res); + $actor_public_key = new Activitypub_rsa(); + $actor_public_key = $actor_public_key->ensure_public_key($actor); + list($verified, $headers) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body); + } + + // If it still failed despite profile update + if($verified !== 1) { + common_debug('ActivityPub Inbox: HTTP Signature: Invalid signature.'); + ActivityPubReturn::error('Invalid signature.'); + } + + // HTTP signature checked out, make sure the "actor" of the activity matches that of the signature + common_debug('ActivityPub Inbox: HTTP Signature: Authorized request. Will now start the inbox handler.'); + + try { + new Activitypub_inbox_handler($data, $actor); + ActivityPubReturn::answer(); + } catch (Exception $e) { + ActivityPubReturn::error($e->getMessage()); + } + } + + /** + * Get all HTTP header key/values as an associative array for the current request. + * + * @return array [string] The HTTP header key/value pairs. + * @author PHP Manual Contributed Notes + */ + private function get_all_headers() + { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $headers[strtolower(str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))))] = $value; + } + } + return $headers; + } +} diff --git a/plugins/ActivityPub/actions/apnotice.php b/plugins/ActivityPub/actions/apnotice.php new file mode 100644 index 0000000000..ff85e36898 --- /dev/null +++ b/plugins/ActivityPub/actions/apnotice.php @@ -0,0 +1,67 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * Notice (Local notices only) + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class apNoticeAction extends ManagedAction +{ + protected $needLogin = false; + protected $canPost = true; + + /** + * Handle the Notice request + * + * @return void + * @throws EmptyPkeyValueException + * @throws InvalidUrlException + * @throws ServerException + * @author Diogo Cordeiro + */ + protected function handle() + { + try { + $notice = Notice::getByID($this->trimmed('id')); + } catch (Exception $e) { + ActivityPubReturn::error('Invalid Notice URI.', 404); + } + + if (!$notice->isLocal()) { + ActivityPubReturn::error("This is not a local notice.", 403); + } + + $res = Activitypub_notice::notice_to_array($notice); + + ActivityPubReturn::answer($res); + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_accept.php b/plugins/ActivityPub/classes/Activitypub_accept.php new file mode 100644 index 0000000000..1c76a281b1 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_accept.php @@ -0,0 +1,86 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_accept extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Accept + * + * @author Diogo Cordeiro + * @param array $object + * @return array pretty array to be used in a response + */ + public static function accept_to_array($object) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => common_root_url().'accept_follow_from_'.urlencode($object['actor']).'_to_'.urlencode($object['object']), + 'type' => 'Accept', + 'actor' => $object['object'], + 'object' => $object + ]; + return $res; + } + + /** + * Verifies if a given object is acceptable for an Accept Activity. + * + * @param array $object + * @return bool + * @throws Exception + * @author Diogo Cordeiro + */ + public static function validate_object($object) + { + if (!is_array($object)) { + throw new Exception('Invalid Object Format for Accept Activity.'); + } + if (!isset($object['type'])) { + throw new Exception('Object type was not specified for Accept Activity.'); + } + switch ($object['type']) { + case 'Follow': + // Validate data + if (!filter_var($object['object'], FILTER_VALIDATE_URL)) { + throw new Exception("Object is not a valid Object URI for Activity."); + } + break; + default: + throw new Exception('This is not a supported Object Type for Accept Activity.'); + } + return true; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_announce.php b/plugins/ActivityPub/classes/Activitypub_announce.php new file mode 100644 index 0000000000..a85a480a1c --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_announce.php @@ -0,0 +1,57 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_announce extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Announce + * + * @param $actor + * @param array $object + * @return array pretty array to be used in a response + * @author Diogo Cordeiro + */ + public static function announce_to_array($actor, $object) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + "type" => "Announce", + "actor" => $actor, + "object" => $object + ]; + return $res; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_attachment.php b/plugins/ActivityPub/classes/Activitypub_attachment.php new file mode 100644 index 0000000000..0dec3c3afe --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_attachment.php @@ -0,0 +1,67 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + * @link http://www.gnu.org/software/social/ + */ + +defined('GNUSOCIAL') || die(); + +/** + * ActivityPub Attachment representation + * + * @category Plugin + * @package GNUsocial + * @author Diogo Cordeiro + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_attachment extends Managed_DataObject +{ + /** + * Generates a pretty array from an Attachment object + * + * @author Diogo Cordeiro + * @param Attachment $attachment + * @return array pretty array to be used in a response + */ + public static function attachment_to_array($attachment) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Document', + 'mediaType' => $attachment->mimetype, + 'url' => $attachment->getUrl(), + 'size' => $attachment->getSize(), + 'name' => $attachment->getTitle(), + ]; + + // Image + if (substr($res["mediaType"], 0, 5) == "image") { + $res["meta"]= [ + 'width' => $attachment->width, + 'height' => $attachment->height + ]; + } + + return $res; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_create.php b/plugins/ActivityPub/classes/Activitypub_create.php new file mode 100644 index 0000000000..9c5d62b03a --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_create.php @@ -0,0 +1,85 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_create extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Create + * + * @author Diogo Cordeiro + * @param string $actor + * @param array $object + * @return array pretty array to be used in a response + */ + public static function create_to_array($actor, $object) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $object['id'].'/create', + 'type' => 'Create', + 'to' => $object['to'], + 'cc' => $object['cc'], + 'actor' => $actor, + 'object' => $object + ]; + return $res; + } + + /** + * Verifies if a given object is acceptable for a Create Activity. + * + * @author Diogo Cordeiro + * @param array $object + * @throws Exception + */ + public static function validate_object($object) + { + if (!is_array($object)) { + throw new Exception('Invalid Object Format for Create Activity.'); + } + if (!isset($object['type'])) { + throw new Exception('Object type was not specified for Create Activity.'); + } + switch ($object['type']) { + case 'Note': + // Validate data + Activitypub_notice::validate_note($object); + break; + default: + throw new Exception('This is not a supported Object Type for Create Activity.'); + } + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_delete.php b/plugins/ActivityPub/classes/Activitypub_delete.php new file mode 100644 index 0000000000..b0d0b3a736 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_delete.php @@ -0,0 +1,58 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_delete extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Delete + * + * @param $actor + * @param array $object + * @return array pretty array to be used in a response + * @author Diogo Cordeiro + */ + public static function delete_to_array($actor, $object) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $object.'/delete', + 'type' => 'Delete', + 'actor' => $actor, + 'object' => $object + ]; + return $res; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_error.php b/plugins/ActivityPub/classes/Activitypub_error.php new file mode 100644 index 0000000000..2515dbdb98 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_error.php @@ -0,0 +1,53 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_error extends Managed_DataObject +{ + /** + * Generates a pretty error from a string + * + * @author Diogo Cordeiro + * @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; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_follow.php b/plugins/ActivityPub/classes/Activitypub_follow.php new file mode 100644 index 0000000000..7463e4b10a --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_follow.php @@ -0,0 +1,91 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_follow extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a subscription + * + * @author Diogo Cordeiro + * @param string $actor + * @param string $object + * @return array pretty array to be used in a response + */ + public static function follow_to_array($actor, $object) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => common_root_url().'follow_from_'.urlencode($actor).'_to_'.urlencode($object), + 'type' => 'Follow', + 'actor' => $actor, + 'object' => $object + ]; + return $res; + } + + /** + * Handles a Follow Activity received by our inbox. + * + * @param Profile $actor_profile Remote Actor + * @param string $object Local Actor + * @throws AlreadyFulfilledException + * @throws HTTP_Request2_Exception + * @throws NoProfileException + * @throws ServerException + * @author Diogo Cordeiro + */ + public static function follow($actor_profile, $object) + { + // Get Actor's Aprofile + $actor_aprofile = Activitypub_profile::from_profile($actor_profile); + + // Get Object profile + $object_profile = new Activitypub_explorer; + $object_profile = $object_profile->lookup($object)[0]; + + if (!Subscription::exists($actor_profile, $object_profile)) { + Subscription::start($actor_profile, $object_profile); + common_debug('ActivityPubPlugin: Accepted Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); + } else { + common_debug('ActivityPubPlugin: Received a repeated Follow request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); + } + + // Notify remote instance that we have accepted their request + common_debug('ActivityPubPlugin: Notifying remote instance that we have accepted their Follow request request from '.ActivityPubPlugin::actor_uri($actor_profile).' to '.$object); + $postman = new Activitypub_postman($actor_profile, [$actor_aprofile]); + $postman->accept_follow(); + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_like.php b/plugins/ActivityPub/classes/Activitypub_like.php new file mode 100644 index 0000000000..b0710973f4 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_like.php @@ -0,0 +1,58 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_like extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Like + * + * @author Diogo Cordeiro + * @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; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_mention_tag.php b/plugins/ActivityPub/classes/Activitypub_mention_tag.php new file mode 100644 index 0000000000..bccb2e44f5 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_mention_tag.php @@ -0,0 +1,57 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_mention_tag extends Managed_DataObject +{ + /** + * Generates an ActivityPub representation of a Mention Tag + * + * @author Diogo Cordeiro + * @param string $href Actor Uri + * @param array $name Mention name + * @return array pretty array to be used in a response + */ + public static function mention_tag_to_array_from_values($href, $name) + { + $res = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + "type" => "Mention", + "href" => $href, + "name" => $name + ]; + return $res; + } +} diff --git a/plugins/ActivityPub/classes/Activitypub_notice.php b/plugins/ActivityPub/classes/Activitypub_notice.php new file mode 100644 index 0000000000..883d768c70 --- /dev/null +++ b/plugins/ActivityPub/classes/Activitypub_notice.php @@ -0,0 +1,252 @@ +. + +/** + * ActivityPub implementation for GNU social + * + * @package GNUsocial + * @author Diogo Cordeiro + * @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 + * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later + */ +class Activitypub_notice extends Managed_DataObject +{ + /** + * Generates a pretty notice from a Notice object + * + * @param Notice $notice + * @return array array to be used in a response + * @throws EmptyPkeyValueException + * @throws InvalidUrlException + * @throws ServerException + * @author Diogo Cordeiro + */ + public static function notice_to_array($notice) + { + $profile = $notice->getProfile(); + $attachments = []; + foreach ($notice->attachments() as $attachment) { + $attachments[] = Activitypub_attachment::attachment_to_array($attachment); + } + + $tags = []; + foreach ($notice->getTags() as $tag) { + if ($tag != "") { // Hacky workaround to avoid stupid outputs + $tags[] = Activitypub_tag::tag_to_array($tag); + } + } + + $cc = [common_local_url('apActorFollowers', ['id' => $profile->getID()])]; + foreach ($notice->getAttentionProfiles() as $to_profile) { + $cc[] = $href = $to_profile->getUri(); + $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST)); + } + + // In a world without walls and fences, we should make everything Public! + $to[]= 'https://www.w3.org/ns/activitystreams#Public'; + + $item = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => common_local_url('apNotice', ['id' => $notice->getID()]), + 'type' => 'Note', + 'published' => str_replace(' ', 'T', $notice->getCreated()).'Z', + 'url' => $notice->getUrl(), + 'attributedTo' => ActivityPubPlugin::actor_uri($profile), + 'to' => ['https://www.w3.org/ns/activitystreams#Public'], + 'cc' => $cc, + 'atomUri' => $notice->getUrl(), + 'conversation' => $notice->getConversationUrl(), + 'content' => $notice->getRendered(), + 'isLocal' => $notice->isLocal(), + 'attachment' => $attachments, + 'tag' => $tags + ]; + + // Is this a reply? + if (!empty($notice->reply_to)) { + $item['inReplyTo'] = common_local_url('apNotice', ['id' => $notice->getID()]); + $item['inReplyToAtomUri'] = Notice::getById($notice->reply_to)->getUrl(); + } + + // Do we have a location for this notice? + try { + $location = Notice_location::locFromStored($notice); + $item['latitude'] = $location->lat; + $item['longitude'] = $location->lon; + } catch (Exception $e) { + // Apparently no. + } + + return $item; + } + + /** + * Create a Notice via ActivityPub Note Object. + * Returns created Notice. + * + * @author Diogo Cordeiro + * @param array $object + * @param Profile|null $actor_profile + * @return Notice + * @throws Exception + */ + public static function create_notice($object, $actor_profile = null) + { + $id = $object['id']; // int + $url = $object['url']; // string + $content = $object['content']; // string + + // possible keys: ['inReplyTo', 'latitude', 'longitude'] + $settings = []; + if (isset($object['inReplyTo'])) { + $settings['inReplyTo'] = $object['inReplyTo']; + } + if (isset($object['latitude'])) { + $settings['latitude'] = $object['latitude']; + } + if (isset($object['longitude'])) { + $settings['longitude'] = $object['longitude']; + } + + // Ensure Actor Profile + if (is_null($actor_profile)) { + $actor_profile = ActivityPub_explorer::get_profile_from_url($object['actor']); + } + + $act = new Activity(); + $act->verb = ActivityVerb::POST; + $act->time = time(); + $act->actor = $actor_profile->asActivityObject(); + $act->context = new ActivityContext(); + $options = ['source' => 'ActivityPub', 'uri' => $id, 'url' => $url]; + + // Is this a reply? + if (isset($settings['inReplyTo'])) { + try { + $inReplyTo = ActivityPubPlugin::grab_notice_from_url($settings['inReplyTo']); + $act->context->replyToID = $inReplyTo->getUri(); + $act->context->replyToUrl = $inReplyTo->getUrl(); + } catch (Exception $e) { + // It failed to grab, maybe we got this note from another source + // (e.g.: OStatus) that handles this differently or we really + // failed to get it... + // Welp, nothing that we can do about, let's + // just fake we don't have such notice. + } + } else { + $inReplyTo = null; + } + + // Mentions + $mentions = []; + if (isset($object['tag']) && is_array($object['tag'])) { + foreach ($object['tag'] as $tag) { + if ($tag['type'] == 'Mention') { + $mentions[] = $tag['href']; + } + } + } + $mentions_profiles = []; + $discovery = new Activitypub_explorer; + foreach ($mentions as $mention) { + try { + $mentions_profiles[] = $discovery->lookup($mention)[0]; + } catch (Exception $e) { + // Invalid actor found, just let it go. // TODO: Fallback to OStatus + } + } + unset($discovery); + + foreach ($mentions_profiles as $mp) { + $act->context->attention[ActivityPubPlugin::actor_uri($mp)] = 'http://activitystrea.ms/schema/1.0/person'; + } + + // Add location if that is set + if (isset($settings['latitude'], $settings['longitude'])) { + $act->context->location = Location::fromLatLon($settings['latitude'], $settings['longitude']); + } + + /* Reject notice if it is too long (without the HTML) + if (Notice::contentTooLong($content)) { + throw new Exception('That\'s too long. Maximum notice size is %d character.'); + }*/ + + $actobj = new ActivityObject(); + $actobj->type = ActivityObject::NOTE; + $actobj->content = strip_tags($content, '