forked from GNUsocial/gnu-social
[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
This commit is contained in:
116
plugins/ActivityPub/lib/AcceptHeader.php
Normal file
116
plugins/ActivityPub/lib/AcceptHeader.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Note : Code is released under the GNU LGPL
|
||||
*
|
||||
* Please do not change the header of this file
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it under the terms of the GNU
|
||||
* Lesser General Public License as published by the Free Software Foundation; either version 2 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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 Lesser General Public License for more details.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The AcceptHeader page will parse and sort the different
|
||||
* allowed types for the content negociations
|
||||
*
|
||||
* @author Pierrick Charron <pierrick@webstart.fr>
|
||||
*/
|
||||
class AcceptHeader extends \ArrayObject
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $header Value of the Accept header
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($header)
|
||||
{
|
||||
$acceptedTypes = $this->_parse($header);
|
||||
usort($acceptedTypes, [$this, '_compare']);
|
||||
parent::__construct($acceptedTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the accept header and return an array containing
|
||||
* all the informations about the Accepted types
|
||||
*
|
||||
* @param string $data Value of the Accept header
|
||||
* @return array
|
||||
*/
|
||||
private function _parse($data)
|
||||
{
|
||||
$array = [];
|
||||
$items = explode(',', $data);
|
||||
foreach ($items as $item) {
|
||||
$elems = explode(';', $item);
|
||||
|
||||
$acceptElement = [];
|
||||
$mime = current($elems);
|
||||
list($type, $subtype) = explode('/', $mime);
|
||||
$acceptElement['type'] = trim($type);
|
||||
$acceptElement['subtype'] = trim($subtype);
|
||||
$acceptElement['raw'] = $mime;
|
||||
|
||||
$acceptElement['params'] = [];
|
||||
while (next($elems)) {
|
||||
list($name, $value) = explode('=', current($elems));
|
||||
$acceptElement['params'][trim($name)] = trim($value);
|
||||
}
|
||||
|
||||
$array[] = $acceptElement;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two Accepted types with their parameters to know
|
||||
* if one media type should be used instead of an other
|
||||
*
|
||||
* @param array $a The first media type and its parameters
|
||||
* @param array $b The second media type and its parameters
|
||||
* @return int
|
||||
*/
|
||||
private function _compare($a, $b)
|
||||
{
|
||||
$a_q = isset($a['params']['q']) ? floatval($a['params']['q']) : 1.0;
|
||||
$b_q = isset($b['params']['q']) ? floatval($b['params']['q']) : 1.0;
|
||||
if ($a_q === $b_q) {
|
||||
$a_count = count($a['params']);
|
||||
$b_count = count($b['params']);
|
||||
if ($a_count === $b_count) {
|
||||
if ($r = $this->_compareSubType($a['subtype'], $b['subtype'])) {
|
||||
return $r;
|
||||
} else {
|
||||
return $this->_compareSubType($a['type'], $b['type']);
|
||||
}
|
||||
} else {
|
||||
return $a_count < $b_count;
|
||||
}
|
||||
} else {
|
||||
return $a_q < $b_q;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two subtypes
|
||||
*
|
||||
* @param string $a First subtype to compare
|
||||
* @param string $b Second subtype to compare
|
||||
* @return int
|
||||
*/
|
||||
private function _compareSubType($a, $b)
|
||||
{
|
||||
if ($a === '*' && $b !== '*') {
|
||||
return 1;
|
||||
} elseif ($b === '*' && $a !== '*') {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
plugins/ActivityPub/lib/Activitypub_activityverb2.php
Normal file
98
plugins/ActivityPub/lib/Activitypub_activityverb2.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* ActivityPub implementation for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
* @link http://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* Utility class to hold a bunch of constant defining default verb types
|
||||
*
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Activitypub_activityverb2 extends Managed_DataObject
|
||||
{
|
||||
const FULL_LIST =
|
||||
[
|
||||
'Accept' => 'https://www.w3.org/ns/activitystreams#Accept',
|
||||
'TentativeAccept' => 'https://www.w3.org/ns/activitystreams#TentativeAccept',
|
||||
'Add' => 'https://www.w3.org/ns/activitystreams#Add',
|
||||
'Arrive' => 'https://www.w3.org/ns/activitystreams#Arrive',
|
||||
'Create' => 'https://www.w3.org/ns/activitystreams#Create',
|
||||
'Delete' => 'https://www.w3.org/ns/activitystreams#Delete',
|
||||
'Follow' => 'https://www.w3.org/ns/activitystreams#Follow',
|
||||
'Ignore' => 'https://www.w3.org/ns/activitystreams#Ignore',
|
||||
'Join' => 'https://www.w3.org/ns/activitystreams#Join',
|
||||
'Leave' => 'https://www.w3.org/ns/activitystreams#Leave',
|
||||
'Like' => 'https://www.w3.org/ns/activitystreams#Like',
|
||||
'Offer' => 'https://www.w3.org/ns/activitystreams#Offer',
|
||||
'Invite' => 'https://www.w3.org/ns/activitystreams#Invite',
|
||||
'Reject' => 'https://www.w3.org/ns/activitystreams#Reject',
|
||||
'TentativeReject' => 'https://www.w3.org/ns/activitystreams#TentativeReject',
|
||||
'Remove' => 'https://www.w3.org/ns/activitystreams#Remove',
|
||||
'Undo' => 'https://www.w3.org/ns/activitystreams#Undo',
|
||||
'Update' => 'https://www.w3.org/ns/activitystreams#Update',
|
||||
'View' => 'https://www.w3.org/ns/activitystreams#View',
|
||||
'Listen' => 'https://www.w3.org/ns/activitystreams#Listen',
|
||||
'Read' => 'https://www.w3.org/ns/activitystreams#Read',
|
||||
'Move' => 'https://www.w3.org/ns/activitystreams#Move',
|
||||
'Travel' => 'https://www.w3.org/ns/activitystreams#Travel',
|
||||
'Announce' => 'https://www.w3.org/ns/activitystreams#Announce',
|
||||
'Block' => 'https://www.w3.org/ns/activitystreams#Block',
|
||||
'Flag' => 'https://www.w3.org/ns/activitystreams#Flag',
|
||||
'Dislike' => 'https://www.w3.org/ns/activitystreams#Dislike',
|
||||
'Question' => 'https://www.w3.org/ns/activitystreams#Question'
|
||||
];
|
||||
|
||||
const KNOWN =
|
||||
[
|
||||
'Accept',
|
||||
'Create',
|
||||
'Delete',
|
||||
'Follow',
|
||||
'Like',
|
||||
'Undo',
|
||||
'Announce'
|
||||
];
|
||||
|
||||
/**
|
||||
* Converts canonical into verb.
|
||||
*
|
||||
* @author GNU social
|
||||
* @param string $verb
|
||||
* @return string
|
||||
*/
|
||||
public static function canonical($verb)
|
||||
{
|
||||
$ns = 'https://www.w3.org/ns/activitystreams#';
|
||||
if (substr($verb, 0, mb_strlen($ns)) == $ns) {
|
||||
return substr($verb, mb_strlen($ns));
|
||||
} else {
|
||||
return $verb;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
plugins/ActivityPub/lib/discoveryhints.php
Normal file
158
plugins/ActivityPub/lib/discoveryhints.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* ActivityPub implementation for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Evan Prodromou
|
||||
* @author Brion Vibber
|
||||
* @author James Walker
|
||||
* @author Siebrand Mazeland
|
||||
* @author Mikael Nordfeldth
|
||||
* @author Diogo Cordeiro
|
||||
* @copyright 2010-2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
* @link http://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
class DiscoveryHints
|
||||
{
|
||||
public static function fromXRD(XML_XRD $xrd)
|
||||
{
|
||||
$hints = [];
|
||||
|
||||
if (Event::handle('StartDiscoveryHintsFromXRD', [$xrd, &$hints])) {
|
||||
foreach ($xrd->links as $link) {
|
||||
switch ($link->rel) {
|
||||
case WebFingerResource_Profile::PROFILEPAGE:
|
||||
$hints['profileurl'] = $link->href;
|
||||
break;
|
||||
case Discovery::UPDATESFROM:
|
||||
if (empty($link->type) || $link->type == 'application/atom+xml') {
|
||||
$hints['feedurl'] = $link->href;
|
||||
}
|
||||
break;
|
||||
case Discovery::HCARD:
|
||||
case Discovery::MF2_HCARD:
|
||||
$hints['hcard'] = $link->href;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::handle('EndDiscoveryHintsFromXRD', [$xrd, &$hints]);
|
||||
}
|
||||
|
||||
return $hints;
|
||||
}
|
||||
|
||||
public static function fromHcardUrl($url)
|
||||
{
|
||||
$client = new HTTPClient();
|
||||
$client->setHeader('Accept', 'text/html,application/xhtml+xml');
|
||||
try {
|
||||
$response = $client->get($url);
|
||||
|
||||
if (!$response->isOk()) {
|
||||
return null;
|
||||
}
|
||||
} catch (HTTP_Request2_Exception $e) {
|
||||
// Any HTTPClient error that might've been thrown
|
||||
common_log(LOG_ERR, __METHOD__ . ':'.$e->getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::hcardHints(
|
||||
$response->getBody(),
|
||||
$response->getEffectiveUrl()
|
||||
);
|
||||
}
|
||||
|
||||
public static function hcardHints($body, $url)
|
||||
{
|
||||
$hcard = self::_hcard($body, $url);
|
||||
|
||||
if (empty($hcard)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$hints = [];
|
||||
|
||||
// XXX: don't copy stuff into an array and then copy it again
|
||||
|
||||
if (array_key_exists('nickname', $hcard) && !empty($hcard['nickname'][0])) {
|
||||
$hints['nickname'] = $hcard['nickname'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('name', $hcard) && !empty($hcard['name'][0])) {
|
||||
$hints['fullname'] = $hcard['name'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('photo', $hcard) && count($hcard['photo'])) {
|
||||
$hints['avatar'] = $hcard['photo'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('note', $hcard) && !empty($hcard['note'][0])) {
|
||||
$hints['bio'] = $hcard['note'][0];
|
||||
}
|
||||
|
||||
if (array_key_exists('adr', $hcard) && !empty($hcard['adr'][0])) {
|
||||
$hints['location'] = $hcard['adr'][0]['value'];
|
||||
}
|
||||
|
||||
if (array_key_exists('url', $hcard) && !empty($hcard['url'][0])) {
|
||||
$hints['homepage'] = $hcard['url'][0];
|
||||
}
|
||||
|
||||
return $hints;
|
||||
}
|
||||
|
||||
public static function _hcard($body, $url)
|
||||
{
|
||||
$mf2 = new Mf2\Parser($body, $url);
|
||||
$mf2 = $mf2->parse();
|
||||
|
||||
if (empty($mf2['items'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hcards = [];
|
||||
|
||||
foreach ($mf2['items'] as $item) {
|
||||
if (!in_array('h-card', $item['type'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We found a match, return it immediately
|
||||
if (isset($item['properties']['url']) && in_array($url, $item['properties']['url'])) {
|
||||
return $item['properties'];
|
||||
}
|
||||
|
||||
// Let's keep all the hcards for later, to return one of them at least
|
||||
$hcards[] = $item['properties'];
|
||||
}
|
||||
|
||||
// No match immediately for the url we expected, but there were h-cards found
|
||||
if (count($hcards) > 0) {
|
||||
return $hcards[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
482
plugins/ActivityPub/lib/explorer.php
Normal file
482
plugins/ActivityPub/lib/explorer.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* ActivityPub implementation for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
* @link http://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* ActivityPub's own Explorer
|
||||
*
|
||||
* Allows to discovery new (or the same) Profiles (both local or remote)
|
||||
*
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Activitypub_explorer
|
||||
{
|
||||
private $discovered_actor_profiles = [];
|
||||
|
||||
/**
|
||||
* Shortcut function to get a single profile from its URL.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url
|
||||
* @return Profile
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function get_profile_from_url($url)
|
||||
{
|
||||
$discovery = new Activitypub_explorer;
|
||||
// Get valid Actor object
|
||||
$actor_profile = $discovery->lookup($url);
|
||||
if (!empty($actor_profile)) {
|
||||
return $actor_profile[0];
|
||||
}
|
||||
throw new Exception('Invalid Actor.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get every profile from the given URL
|
||||
* This function cleans the $this->discovered_actor_profiles array
|
||||
* so that there is no erroneous data
|
||||
*
|
||||
* @param string $url User's url
|
||||
* @return array of Profile objects
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function lookup($url)
|
||||
{
|
||||
if (in_array($url, ACTIVITYPUB_PUBLIC_TO)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
common_debug('ActivityPub Explorer: Started now looking for '.$url);
|
||||
$this->discovered_actor_profiles = [];
|
||||
|
||||
return $this->_lookup($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get every profile from the given URL
|
||||
* This is a recursive function that will accumulate the results on
|
||||
* $discovered_actor_profiles array
|
||||
*
|
||||
* @param string $url User's url
|
||||
* @return array of Profile objects
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function _lookup($url)
|
||||
{
|
||||
// First check if we already have it locally and, if so, return it
|
||||
// If the local fetch fails: grab it remotely, store locally and return
|
||||
if (! ($this->grab_local_user($url) || $this->grab_remote_user($url))) {
|
||||
throw new Exception('User not found.');
|
||||
}
|
||||
|
||||
return $this->discovered_actor_profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* This ensures that we are using a valid ActivityPub URI
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $url
|
||||
* @return boolean success state (related to the response)
|
||||
* @throws Exception (If the HTTP request fails)
|
||||
*/
|
||||
private function ensure_proper_remote_uri($url)
|
||||
{
|
||||
$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);
|
||||
$res = json_decode($response->getBody(), true);
|
||||
if (self::validate_remote_response($res)) {
|
||||
$this->temp_res = $res;
|
||||
return true;
|
||||
} else {
|
||||
common_debug('ActivityPub Explorer: Invalid potential remote actor while ensuring URI: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a local user profile from its URL and joins it on
|
||||
* $this->discovered_actor_profiles
|
||||
*
|
||||
* @param string $uri Actor's uri
|
||||
* @param bool $online
|
||||
* @return boolean success state
|
||||
* @throws NoProfileException
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function grab_local_user($uri, $online = false)
|
||||
{
|
||||
if ($online) {
|
||||
common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' with online resources.');
|
||||
} else {
|
||||
common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' offline.');
|
||||
}
|
||||
// Ensure proper remote URI
|
||||
// If an exception occurs here it's better to just leave everything
|
||||
// break than to continue processing
|
||||
if ($online && $this->ensure_proper_remote_uri($uri)) {
|
||||
$uri = $this->temp_res["id"];
|
||||
}
|
||||
|
||||
// Try standard ActivityPub route
|
||||
// Is this a known filthy little mudblood?
|
||||
$aprofile = self::get_aprofile_by_url($uri);
|
||||
if ($aprofile instanceof Activitypub_profile) {
|
||||
$profile = $aprofile->local_profile();
|
||||
common_debug('ActivityPub Explorer: Found a local Aprofile for '.$uri);
|
||||
// We found something!
|
||||
$this->discovered_actor_profiles[]= $profile;
|
||||
unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
|
||||
return true;
|
||||
} else {
|
||||
common_debug('ActivityPub Explorer: Unable to find a local Aprofile for '.$uri.' - looking for a Profile instead.');
|
||||
// Well, maybe it is a pure blood?
|
||||
// Iff, we are in the same instance:
|
||||
$ACTIVITYPUB_BASE_ACTOR_URI_length = strlen(ACTIVITYPUB_BASE_ACTOR_URI);
|
||||
if (substr($uri, 0, $ACTIVITYPUB_BASE_ACTOR_URI_length) == ACTIVITYPUB_BASE_ACTOR_URI) {
|
||||
try {
|
||||
$profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_ACTOR_URI_length)));
|
||||
common_debug('ActivityPub Explorer: Found a Profile for '.$uri);
|
||||
// We found something!
|
||||
$this->discovered_actor_profiles[]= $profile;
|
||||
unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
// Let the exception go on its merry way.
|
||||
common_debug('ActivityPub Explorer: Unable to find a Profile for '.$uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If offline grabbing failed, attempt again with online resources
|
||||
if (!$online) {
|
||||
common_debug('ActivityPub Explorer: Will try everything again with online resources against: '.$uri);
|
||||
return $this->grab_local_user($uri, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a remote user(s) profile(s) from its URL and joins it on
|
||||
* $this->discovered_actor_profiles
|
||||
*
|
||||
* @param string $url User's url
|
||||
* @return boolean success state
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function grab_remote_user($url)
|
||||
{
|
||||
common_debug('ActivityPub Explorer: Trying to grab a remote actor for '.$url);
|
||||
if (!isset($this->temp_res)) {
|
||||
$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);
|
||||
$res = json_decode($response->getBody(), true);
|
||||
} else {
|
||||
$res = $this->temp_res;
|
||||
unset($this->temp_res);
|
||||
}
|
||||
if (isset($res['type']) && $res['type'] === 'OrderedCollection' && isset($res['first'])) { // It's a potential collection of actors!!!
|
||||
common_debug('ActivityPub Explorer: Found a collection of actors for '.$url);
|
||||
$this->travel_collection($res['first']);
|
||||
return true;
|
||||
} elseif (self::validate_remote_response($res)) {
|
||||
common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url);
|
||||
$this->discovered_actor_profiles[]= $this->store_profile($res);
|
||||
return true;
|
||||
} else {
|
||||
common_debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: '.$url. '. He returned the following: '.json_encode($res, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
// TODO: Fallback to OStatus
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save remote user profile in local instance
|
||||
*
|
||||
* @param array $res remote response
|
||||
* @return Profile remote Profile object
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function store_profile($res)
|
||||
{
|
||||
// ActivityPub Profile
|
||||
$aprofile = new Activitypub_profile;
|
||||
$aprofile->uri = $res['id'];
|
||||
$aprofile->nickname = $res['preferredUsername'];
|
||||
$aprofile->fullname = isset($res['name']) ? $res['name'] : null;
|
||||
$aprofile->bio = isset($res['summary']) ? substr(strip_tags($res['summary']), 0, 1000) : null;
|
||||
$aprofile->inboxuri = $res['inbox'];
|
||||
$aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'];
|
||||
|
||||
$aprofile->do_insert();
|
||||
$profile = $aprofile->local_profile();
|
||||
|
||||
// Public Key
|
||||
$apRSA = new Activitypub_rsa();
|
||||
$apRSA->profile_id = $profile->getID();
|
||||
$apRSA->public_key = $res['publicKey']['publicKeyPem'];
|
||||
$apRSA->store_keys();
|
||||
|
||||
// Avatar
|
||||
if (isset($res['icon']['url'])) {
|
||||
try {
|
||||
$this->update_avatar($profile, $res['icon']['url']);
|
||||
} catch (Exception $e) {
|
||||
// Let the exception go, it isn't a serious issue
|
||||
common_debug('ActivityPub Explorer: An error ocurred while grabbing remote avatar: '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and update given avatar image
|
||||
*
|
||||
* @author GNU social
|
||||
* @param Profile $profile
|
||||
* @param string $url
|
||||
* @return Avatar The Avatar we have on disk.
|
||||
* @throws Exception in various failure cases
|
||||
*/
|
||||
public static function update_avatar(Profile $profile, $url)
|
||||
{
|
||||
common_debug('ActivityPub Explorer: Started grabbing remote avatar from: '.$url);
|
||||
if (!filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
// TRANS: Server exception. %s is a URL.
|
||||
common_debug('ActivityPub Explorer: Failed because it is an invalid url: '.$url);
|
||||
throw new ServerException(sprintf('Invalid avatar URL %s.', $url));
|
||||
}
|
||||
|
||||
// @todo FIXME: This should be better encapsulated
|
||||
// ripped from oauthstore.php (for old OMB client)
|
||||
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
|
||||
try {
|
||||
$imgData = HTTPClient::quickGet($url);
|
||||
// Make sure it's at least an image file. ImageFile can do the rest.
|
||||
if (false === getimagesizefromstring($imgData)) {
|
||||
common_debug('ActivityPub Explorer: Failed because the downloaded avatar: '.$url. 'is not a valid image.');
|
||||
throw new UnsupportedMediaException('Downloaded avatar was not an image.');
|
||||
}
|
||||
file_put_contents($temp_filename, $imgData);
|
||||
unset($imgData); // No need to carry this in memory.
|
||||
common_debug('ActivityPub Explorer: Stored dowloaded avatar in: '.$temp_filename);
|
||||
|
||||
$id = $profile->getID();
|
||||
|
||||
$imagefile = new ImageFile(null, $temp_filename);
|
||||
$filename = Avatar::filename(
|
||||
$id,
|
||||
image_type_to_extension($imagefile->type),
|
||||
null,
|
||||
common_timestamp()
|
||||
);
|
||||
rename($temp_filename, Avatar::path($filename));
|
||||
common_debug('ActivityPub Explorer: Moved avatar from: '.$temp_filename.' to '.$filename);
|
||||
} catch (Exception $e) {
|
||||
common_debug('ActivityPub Explorer: Something went wrong while processing the avatar from: '.$url.' details: '.$e->getMessage());
|
||||
unlink($temp_filename);
|
||||
throw $e;
|
||||
}
|
||||
// @todo FIXME: Hardcoded chmod is lame, but seems to be necessary to
|
||||
// keep from accidentally saving images from command-line (queues)
|
||||
// that can't be read from web server, which causes hard-to-notice
|
||||
// problems later on:
|
||||
//
|
||||
// http://status.net/open-source/issues/2663
|
||||
chmod(Avatar::path($filename), 0644);
|
||||
|
||||
$profile->setOriginal($filename);
|
||||
|
||||
$orig = clone($profile);
|
||||
$profile->avatar = $url;
|
||||
$profile->update($orig);
|
||||
|
||||
common_debug('ActivityPub Explorer: Seted Avatar from: '.$url.' to profile.');
|
||||
return Avatar::getUploaded($profile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a remote response in order to determine whether this
|
||||
* response is a valid profile or not
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param array $res remote response
|
||||
* @return boolean success state
|
||||
*/
|
||||
public static function validate_remote_response($res)
|
||||
{
|
||||
if (!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a ActivityPub Profile from it's uri
|
||||
* Unfortunately GNU social cache is not truly reliable when handling
|
||||
* potential ActivityPub remote profiles, as so it is important to use
|
||||
* this hacky workaround (at least for now)
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param string $v URL
|
||||
* @return boolean|Activitypub_profile false if fails | Aprofile object if successful
|
||||
*/
|
||||
public static function get_aprofile_by_url($v)
|
||||
{
|
||||
$i = Managed_DataObject::getcached("Activitypub_profile", "uri", $v);
|
||||
if (empty($i)) { // false = cache miss
|
||||
$i = new Activitypub_profile;
|
||||
$result = $i->get("uri", $v);
|
||||
if ($result) {
|
||||
// Hit!
|
||||
$i->encache();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return $i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a valid actor profile url returns its inboxes
|
||||
*
|
||||
* @param string $url of Actor profile
|
||||
* @return boolean|array false if fails | array with inbox and shared inbox if successful
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public static function get_actor_inboxes_uri($url)
|
||||
{
|
||||
$client = new HTTPClient();
|
||||
$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);
|
||||
if (!$response->isOk()) {
|
||||
throw new Exception('Invalid Actor URL.');
|
||||
}
|
||||
$res = json_decode($response->getBody(), true);
|
||||
if (self::validate_remote_response($res)) {
|
||||
return [
|
||||
'inbox' => $res['inbox'],
|
||||
'sharedInbox' => isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']
|
||||
];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the Explorer to transverse a collection of persons.
|
||||
*
|
||||
* @param string $url
|
||||
* @return boolean
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function travel_collection($url)
|
||||
{
|
||||
$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);
|
||||
$res = json_decode($response->getBody(), true);
|
||||
|
||||
if (!isset($res['orderedItems'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($res["orderedItems"] as $profile) {
|
||||
if ($this->_lookup($profile) == false) {
|
||||
common_debug('ActivityPub Explorer: Found an invalid actor for '.$profile);
|
||||
// TODO: Invalid actor found, fallback to OStatus
|
||||
}
|
||||
}
|
||||
// Go through entire collection
|
||||
if (!is_null($res["next"])) {
|
||||
$this->_lookup($res["next"]);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a remote user array from its URL (this function is only used for
|
||||
* profile updating and shall not be used for anything else)
|
||||
*
|
||||
* @param string $url User's url
|
||||
* @return mixed
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public static function get_remote_user_activity($url)
|
||||
{
|
||||
$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);
|
||||
$res = json_decode($response->getBody(), true);
|
||||
if (Activitypub_explorer::validate_remote_response($res)) {
|
||||
common_debug('ActivityPub Explorer: Found a valid remote actor for '.$url);
|
||||
return $res;
|
||||
}
|
||||
throw new Exception('ActivityPub Explorer: Failed to get activity.');
|
||||
}
|
||||
}
|
||||
139
plugins/ActivityPub/lib/httpsignature.php
Normal file
139
plugins/ActivityPub/lib/httpsignature.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
/**
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @category Network
|
||||
* @package Nautilus
|
||||
* @author Aaron Parecki <aaron@parecki.com>
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
* @link https://github.com/aaronpk/Nautilus/blob/master/app/ActivityPub/HTTPSignature.php
|
||||
*/
|
||||
|
||||
class HttpSignature
|
||||
{
|
||||
public static function sign($user, $url, $body = false, $addlHeaders = [])
|
||||
{
|
||||
$digest = false;
|
||||
if ($body) {
|
||||
$digest = self::_digest($body);
|
||||
}
|
||||
$headers = self::_headersToSign($url, $digest);
|
||||
$headers = array_merge($headers, $addlHeaders);
|
||||
$stringToSign = self::_headersToSigningString($headers);
|
||||
$signedHeaders = implode(' ', array_map('strtolower', array_keys($headers)));
|
||||
$actor_private_key = new Activitypub_rsa();
|
||||
$actor_private_key = $actor_private_key->get_private_key($user);
|
||||
$key = openssl_pkey_get_private($actor_private_key);
|
||||
openssl_sign($stringToSign, $signature, $key, OPENSSL_ALGO_SHA256);
|
||||
$signature = base64_encode($signature);
|
||||
$signatureHeader = 'keyId="' . ActivityPubPlugin::actor_uri($user).'#public-key' . '",headers="' . $signedHeaders . '",algorithm="rsa-sha256",signature="' . $signature . '"';
|
||||
unset($headers['(request-target)']);
|
||||
$headers['Signature'] = $signatureHeader;
|
||||
|
||||
return self::_headersToCurlArray($headers);
|
||||
}
|
||||
|
||||
private static function _digest($body)
|
||||
{
|
||||
if (is_array($body)) {
|
||||
$body = json_encode($body);
|
||||
}
|
||||
return base64_encode(hash('sha256', $body, true));
|
||||
}
|
||||
|
||||
protected static function _headersToSign($url, $digest = false)
|
||||
{
|
||||
$date = new DateTime('UTC');
|
||||
|
||||
$headers = [
|
||||
'(request-target)' => 'post ' . parse_url($url, PHP_URL_PATH),
|
||||
'Date' => $date->format('D, d M Y H:i:s \G\M\T'),
|
||||
'Host' => parse_url($url, PHP_URL_HOST),
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams", application/activity+json, application/json',
|
||||
'User-Agent' => 'GNU social ActivityPub Plugin - https://gnu.io/social',
|
||||
'Content-Type' => 'application/activity+json'
|
||||
];
|
||||
|
||||
if ($digest) {
|
||||
$headers['Digest'] = 'SHA-256=' . $digest;
|
||||
}
|
||||
|
||||
return $headers;
|
||||
}
|
||||
|
||||
private static function _headersToSigningString($headers)
|
||||
{
|
||||
return implode("\n", array_map(function ($k, $v) {
|
||||
return strtolower($k) . ': ' . $v;
|
||||
}, array_keys($headers), $headers));
|
||||
}
|
||||
|
||||
private static function _headersToCurlArray($headers)
|
||||
{
|
||||
return array_map(function ($k, $v) {
|
||||
return "$k: $v";
|
||||
}, array_keys($headers), $headers);
|
||||
}
|
||||
|
||||
public static function parseSignatureHeader($signature)
|
||||
{
|
||||
$parts = explode(',', $signature);
|
||||
$signatureData = [];
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (preg_match('/(.+)="(.+)"/', $part, $match)) {
|
||||
$signatureData[$match[1]] = $match[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($signatureData['keyId'])) {
|
||||
return [
|
||||
'error' => 'No keyId was found in the signature header. Found: ' . implode(', ', array_keys($signatureData))
|
||||
];
|
||||
}
|
||||
|
||||
if (!filter_var($signatureData['keyId'], FILTER_VALIDATE_URL)) {
|
||||
return [
|
||||
'error' => 'keyId is not a URL: ' . $signatureData['keyId']
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($signatureData['headers']) || !isset($signatureData['signature'])) {
|
||||
return [
|
||||
'error' => 'Signature is missing headers or signature parts'
|
||||
];
|
||||
}
|
||||
|
||||
return $signatureData;
|
||||
}
|
||||
|
||||
public static function verify($publicKey, $signatureData, $inputHeaders, $path, $body)
|
||||
{
|
||||
$digest = 'SHA-256=' . base64_encode(hash('sha256', $body, true));
|
||||
$headersToSign = [];
|
||||
foreach (explode(' ', $signatureData['headers']) as $h) {
|
||||
if ($h == '(request-target)') {
|
||||
$headersToSign[$h] = 'post ' . $path;
|
||||
} elseif ($h == 'digest') {
|
||||
$headersToSign[$h] = $digest;
|
||||
} elseif (isset($inputHeaders[$h][0])) {
|
||||
$headersToSign[$h] = $inputHeaders[$h];
|
||||
}
|
||||
}
|
||||
$signingString = self::_headersToSigningString($headersToSign);
|
||||
|
||||
$verified = openssl_verify($signingString, base64_decode($signatureData['signature']), $publicKey, OPENSSL_ALGO_SHA256);
|
||||
|
||||
return [$verified, $signingString];
|
||||
}
|
||||
}
|
||||
324
plugins/ActivityPub/lib/inbox_handler.php
Normal file
324
plugins/ActivityPub/lib/inbox_handler.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* ActivityPub implementation for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
* @link http://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* ActivityPub Inbox Handler
|
||||
*
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Activitypub_inbox_handler
|
||||
{
|
||||
private $activity;
|
||||
private $actor;
|
||||
private $object;
|
||||
|
||||
/**
|
||||
* Create a Inbox Handler to receive something from someone.
|
||||
*
|
||||
* @param array $activity Activity we are receiving
|
||||
* @param Profile $actor_profile Actor originating the activity
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function __construct($activity, $actor_profile = null)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
$this->object = $activity['object'];
|
||||
|
||||
// Validate Activity
|
||||
$this->validate_activity();
|
||||
|
||||
// Get Actor's Profile
|
||||
if (!is_null($actor_profile)) {
|
||||
$this->actor = $actor_profile;
|
||||
} else {
|
||||
$this->actor = ActivityPub_explorer::get_profile_from_url($this->activity['actor']);
|
||||
}
|
||||
|
||||
// Handle the Activity
|
||||
$this->process();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a given Activity is valid. Throws exception if not.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @throws Exception
|
||||
*/
|
||||
private function validate_activity()
|
||||
{
|
||||
// Activity validation
|
||||
// Validate data
|
||||
if (!(isset($this->activity['type']))) {
|
||||
throw new Exception('Activity Validation Failed: Type was not specified.');
|
||||
}
|
||||
if (!isset($this->activity['actor'])) {
|
||||
throw new Exception('Activity Validation Failed: Actor was not specified.');
|
||||
}
|
||||
if (!isset($this->activity['object'])) {
|
||||
throw new Exception('Activity Validation Failed: Object was not specified.');
|
||||
}
|
||||
|
||||
// Object validation
|
||||
switch ($this->activity['type']) {
|
||||
case 'Accept':
|
||||
Activitypub_accept::validate_object($this->object);
|
||||
break;
|
||||
case 'Create':
|
||||
Activitypub_create::validate_object($this->object);
|
||||
break;
|
||||
case 'Delete':
|
||||
case 'Follow':
|
||||
case 'Like':
|
||||
case 'Announce':
|
||||
if (!filter_var($this->object, FILTER_VALIDATE_URL)) {
|
||||
throw new Exception('Object is not a valid Object URI for Activity.');
|
||||
}
|
||||
break;
|
||||
case 'Undo':
|
||||
Activitypub_undo::validate_object($this->object);
|
||||
break;
|
||||
default:
|
||||
throw new Exception('Unknown Activity Type.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the Activity to proper handler in order to be processed.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function process()
|
||||
{
|
||||
switch ($this->activity['type']) {
|
||||
case 'Accept':
|
||||
$this->handle_accept($this->actor, $this->object);
|
||||
break;
|
||||
case 'Create':
|
||||
$this->handle_create($this->actor, $this->object);
|
||||
break;
|
||||
case 'Delete':
|
||||
$this->handle_delete($this->actor, $this->object);
|
||||
break;
|
||||
case 'Follow':
|
||||
$this->handle_follow($this->actor, $this->object);
|
||||
break;
|
||||
case 'Like':
|
||||
$this->handle_like($this->actor, $this->object);
|
||||
break;
|
||||
case 'Undo':
|
||||
$this->handle_undo($this->actor, $this->object);
|
||||
break;
|
||||
case 'Announce':
|
||||
$this->handle_announce($this->actor, $this->object);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an Accept Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_accept($actor, $object)
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'Follow':
|
||||
$this->handle_accept_follow($actor, $object);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an Accept Follow Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_accept_follow($actor, $object)
|
||||
{
|
||||
// Get valid Object profile
|
||||
$object_profile = new Activitypub_explorer;
|
||||
$object_profile = $object_profile->lookup($object['object'])[0];
|
||||
|
||||
$pending_list = new Activitypub_pending_follow_requests($actor->getID(), $object_profile->getID());
|
||||
$pending_list->remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Create Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_create($actor, $object)
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'Note':
|
||||
Activitypub_notice::create_notice($object, $actor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Delete Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws AuthorizationException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_delete($actor, $object)
|
||||
{
|
||||
$notice = ActivityPubPlugin::grab_notice_from_url($object['object']);
|
||||
$notice->deleteAs($actor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Follow Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws AlreadyFulfilledException
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @throws \GuzzleHttp\Exception\GuzzleException
|
||||
* @throws \HttpSignatures\Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_follow($actor, $object)
|
||||
{
|
||||
Activitypub_follow::follow($actor, $object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Like Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_like($actor, $object)
|
||||
{
|
||||
$notice = ActivityPubPlugin::grab_notice_from_url($object);
|
||||
Fave::addNew($actor, $notice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Undo Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws AlreadyFulfilledException
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_undo($actor, $object)
|
||||
{
|
||||
switch ($object['type']) {
|
||||
case 'Follow':
|
||||
$this->handle_undo_follow($actor, $object['object']);
|
||||
break;
|
||||
case 'Like':
|
||||
$this->handle_undo_like($actor, $object['object']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Undo Like Activity received by our inbox.
|
||||
*
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws AlreadyFulfilledException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
private function handle_undo_like($actor, $object)
|
||||
{
|
||||
$notice = ActivityPubPlugin::grab_notice_from_url($object);
|
||||
Fave::removeEntry($actor, $notice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Undo Follow Activity received by our inbox.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws AlreadyFulfilledException
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws NoProfileException
|
||||
* @throws ServerException
|
||||
*/
|
||||
private function handle_undo_follow($actor, $object)
|
||||
{
|
||||
// Get Object profile
|
||||
$object_profile = new Activitypub_explorer;
|
||||
$object_profile = $object_profile->lookup($object)[0];
|
||||
|
||||
if (Subscription::exists($actor, $object_profile)) {
|
||||
Subscription::cancel($actor, $object_profile);
|
||||
// You are no longer following this person.
|
||||
} else {
|
||||
// 409: You are not following this person already.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a Announce Activity received by our inbox.
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @param Profile $actor Actor
|
||||
* @param array $object Activity
|
||||
* @throws Exception
|
||||
*/
|
||||
private function handle_announce($actor, $object)
|
||||
{
|
||||
$object_notice = ActivityPubPlugin::grab_notice_from_url($object);
|
||||
$object_notice->repeat($actor, 'ActivityPub');
|
||||
}
|
||||
}
|
||||
348
plugins/ActivityPub/lib/postman.php
Normal file
348
plugins/ActivityPub/lib/postman.php
Normal file
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
// This file is part of GNU social - https://www.gnu.org/software/social
|
||||
//
|
||||
// GNU social is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// GNU social is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* ActivityPub implementation for GNU social
|
||||
*
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
* @link http://www.gnu.org/software/social/
|
||||
*/
|
||||
|
||||
defined('GNUSOCIAL') || die();
|
||||
|
||||
/**
|
||||
* ActivityPub's own Postman
|
||||
*
|
||||
* Standard workflow expects that we send an Explorer to find out destinataries'
|
||||
* inbox address. Then we send our postman to deliver whatever we want to send them.
|
||||
*
|
||||
* @category Plugin
|
||||
* @package GNUsocial
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
|
||||
*/
|
||||
class Activitypub_postman
|
||||
{
|
||||
private $actor;
|
||||
private $actor_uri;
|
||||
private $to = [];
|
||||
private $client;
|
||||
private $headers;
|
||||
|
||||
/**
|
||||
* Create a postman to deliver something to someone
|
||||
*
|
||||
* @param Profile $from Profile of sender
|
||||
* @param $to
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function __construct($from, $to)
|
||||
{
|
||||
$this->actor = $from;
|
||||
$discovery = new Activitypub_explorer();
|
||||
$this->to = $to;
|
||||
$followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
|
||||
foreach ($followers as $sub) {
|
||||
try {
|
||||
$to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
|
||||
} catch (Exception $e) {
|
||||
// Not an ActivityPub Remote Follower, let it go
|
||||
}
|
||||
}
|
||||
unset($discovery);
|
||||
|
||||
$this->actor_uri = ActivityPubPlugin::actor_uri($this->actor);
|
||||
|
||||
$this->client = new HTTPClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send something to remote instance
|
||||
*
|
||||
* @param string $data request body
|
||||
* @param string $inbox url of remote inbox
|
||||
* @param string $method request method
|
||||
* @return GNUsocial_HTTPResponse
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function send($data, $inbox, $method = 'POST')
|
||||
{
|
||||
common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
|
||||
|
||||
$headers = HttpSignature::sign($this->actor, $inbox, $data);
|
||||
|
||||
common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));
|
||||
|
||||
$this->client->setBody($data);
|
||||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
$response = $this->client->post($inbox, $headers);
|
||||
break;
|
||||
case 'GET':
|
||||
$response = $this->client->get($inbox, $headers);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unsupported request method for postman.");
|
||||
}
|
||||
|
||||
common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a follow notification to remote instance
|
||||
*
|
||||
* @return bool
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function follow()
|
||||
{
|
||||
$data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl());
|
||||
$res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
|
||||
$res_body = json_decode($res->getBody());
|
||||
|
||||
if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
|
||||
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
|
||||
$pending_list->add();
|
||||
return true;
|
||||
} elseif (isset($res_body[0]->error)) {
|
||||
throw new Exception($res_body[0]->error);
|
||||
}
|
||||
|
||||
throw new Exception("An unknown error occurred.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Undo Follow notification to remote instance
|
||||
*
|
||||
* @return bool
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function undo_follow()
|
||||
{
|
||||
$data = Activitypub_undo::undo_to_array(
|
||||
Activitypub_follow::follow_to_array(
|
||||
ActivityPubPlugin::actor_uri($this->actor),
|
||||
$this->to[0]->getUrl()
|
||||
)
|
||||
);
|
||||
$res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
|
||||
$res_body = json_decode($res->getBody());
|
||||
|
||||
if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
|
||||
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
|
||||
$pending_list->remove();
|
||||
return true;
|
||||
}
|
||||
if (isset($res_body[0]->error)) {
|
||||
throw new Exception($res_body[0]->error);
|
||||
}
|
||||
throw new Exception("An unknown error occurred.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Accept Follow notification to remote instance
|
||||
*
|
||||
* @return bool
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function accept_follow()
|
||||
{
|
||||
$data = Activitypub_accept::accept_to_array(
|
||||
Activitypub_follow::follow_to_array(
|
||||
$this->to[0]->getUrl(),
|
||||
ActivityPubPlugin::actor_uri($this->actor)
|
||||
|
||||
)
|
||||
);
|
||||
$res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
|
||||
$res_body = json_decode($res->getBody());
|
||||
|
||||
if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
|
||||
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
|
||||
$pending_list->remove();
|
||||
return true;
|
||||
}
|
||||
if (isset($res_body[0]->error)) {
|
||||
throw new Exception($res_body[0]->error);
|
||||
}
|
||||
throw new Exception("An unknown error occurred.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Like notification to remote instances holding the notice
|
||||
*
|
||||
* @param Notice $notice
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws InvalidUrlException
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function like($notice)
|
||||
{
|
||||
$data = Activitypub_like::like_to_array(
|
||||
ActivityPubPlugin::actor_uri($this->actor),
|
||||
$notice->getUrl()
|
||||
);
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
foreach ($this->to_inbox() as $inbox) {
|
||||
$this->send($data, $inbox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Undo Like notification to remote instances holding the notice
|
||||
*
|
||||
* @param Notice $notice
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws InvalidUrlException
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function undo_like($notice)
|
||||
{
|
||||
$data = Activitypub_undo::undo_to_array(
|
||||
Activitypub_like::like_to_array(
|
||||
ActivityPubPlugin::actor_uri($this->actor),
|
||||
$notice->getUrl()
|
||||
)
|
||||
);
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
foreach ($this->to_inbox() as $inbox) {
|
||||
$this->send($data, $inbox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Create notification to remote instances
|
||||
*
|
||||
* @param Notice $notice
|
||||
* @throws EmptyPkeyValueException
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws InvalidUrlException
|
||||
* @throws ServerException
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function create_note($notice)
|
||||
{
|
||||
$data = Activitypub_create::create_to_array(
|
||||
$this->actor_uri,
|
||||
Activitypub_notice::notice_to_array($notice)
|
||||
);
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
foreach ($this->to_inbox() as $inbox) {
|
||||
$this->send($data, $inbox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Announce notification to remote instances
|
||||
*
|
||||
* @param Notice $notice
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function announce($notice)
|
||||
{
|
||||
$data = Activitypub_announce::announce_to_array(
|
||||
ActivityPubPlugin::actor_uri($this->actor),
|
||||
$notice->getUri()
|
||||
);
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
|
||||
foreach ($this->to_inbox() as $inbox) {
|
||||
$this->send($data, $inbox);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a Delete notification to remote instances holding the notice
|
||||
*
|
||||
* @param Notice $notice
|
||||
* @throws HTTP_Request2_Exception
|
||||
* @throws InvalidUrlException
|
||||
* @throws Exception
|
||||
* @throws Exception
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
*/
|
||||
public function delete($notice)
|
||||
{
|
||||
$data = Activitypub_delete::delete_to_array(
|
||||
ActivityPubPlugin::actor_uri($notice->getProfile()),
|
||||
$notice->getUrl()
|
||||
);
|
||||
$errors = [];
|
||||
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
|
||||
foreach ($this->to_inbox() as $inbox) {
|
||||
$res = $this->send($data, $inbox);
|
||||
if (!$res->getStatus() == 200) {
|
||||
$res_body = json_decode($res->getBody(), true);
|
||||
if (isset($res_body[0]['error'])) {
|
||||
$errors[] = ($res_body[0]['error']);
|
||||
continue;
|
||||
}
|
||||
$errors[] = ("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
if (!empty($errors)) {
|
||||
throw new Exception(json_encode($errors));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean list of inboxes to deliver messages
|
||||
*
|
||||
* @author Diogo Cordeiro <diogo@fc.up.pt>
|
||||
* @return array To Inbox URLs
|
||||
*/
|
||||
private function to_inbox()
|
||||
{
|
||||
$to_inboxes = [];
|
||||
foreach ($this->to as $to_profile) {
|
||||
$i = $to_profile->get_inbox();
|
||||
// Prevent delivering to self
|
||||
if ($i == [common_local_url('apInbox')]) {
|
||||
continue;
|
||||
}
|
||||
$to_inboxes[] = $i;
|
||||
}
|
||||
|
||||
return array_unique($to_inboxes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user