First unstable federation release

This commit is contained in:
Diogo Cordeiro 2018-07-31 20:09:47 +01:00
parent 20738f48cd
commit f8048c7565
14 changed files with 348 additions and 1904 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/vendor/ /vendor/
composer.lock

View File

@ -29,7 +29,11 @@ if (!defined('GNUSOCIAL')) {
exit(1); exit(1);
} }
// Ensure proper timezone
date_default_timezone_set('UTC');
// Import required files by the plugin // Import required files by the plugin
require __DIR__.'/vendor/autoload.php';
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "discoveryhints.php"; require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "discoveryhints.php";
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php"; require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "explorer.php";
require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php"; require_once __DIR__ . DIRECTORY_SEPARATOR . "utils" . DIRECTORY_SEPARATOR . "postman.php";
@ -92,7 +96,7 @@ class ActivityPubPlugin extends Plugin
} catch (Exception $e) { } catch (Exception $e) {
try { try {
$candidate = Notice::getByID(intval(substr($url, strlen(common_local_url('shownotice', ['notice' => '']))))); $candidate = Notice::getByID(intval(substr($url, strlen(common_local_url('shownotice', ['notice' => ''])))));
if ($candidate->getUrl() == $url) { if ($candidate->getUrl() == $url) { // Sanity check
return $candidate; return $candidate;
} else { } else {
throw new Exception("Notice not found."); throw new Exception("Notice not found.");
@ -111,23 +115,28 @@ class ActivityPubPlugin extends Plugin
*/ */
public function onRouterInitialized(URLMapper $m) public function onRouterInitialized(URLMapper $m)
{ {
ActivityPubURLMapperOverwrite::overwrite_variable( ActivityPubURLMapperOverwrite::variable(
$m, $m,
'user/:id', 'user/:id',
['action' => 'showstream'], ['id' => '[0-9]+'],
['id' => '[0-9]+'],
'apActorProfile' 'apActorProfile'
); );
// Special route for webfinger purposes // Special route for webfinger purposes
ActivityPubURLMapperOverwrite::overwrite_variable( ActivityPubURLMapperOverwrite::variable(
$m, $m,
':nickname', ':nickname',
['action' => 'showstream'],
['nickname' => Nickname::DISPLAY_FMT], ['nickname' => Nickname::DISPLAY_FMT],
'apActorProfile' 'apActorProfile'
); );
ActivityPubURLMapperOverwrite::variable(
$m,
'notice/:id',
['id' => '[0-9]+'],
'apNotice'
);
$m->connect( $m->connect(
'user/:id/liked.json', 'user/:id/liked.json',
['action' => 'apActorLiked'], ['action' => 'apActorLiked'],
@ -170,9 +179,7 @@ class ActivityPubPlugin extends Plugin
'version' => GNUSOCIAL_VERSION, 'version' => GNUSOCIAL_VERSION,
'author' => 'Diogo Cordeiro, Daniel Supernault', 'author' => 'Diogo Cordeiro, Daniel Supernault',
'homepage' => 'https://www.gnu.org/software/social/', 'homepage' => 'https://www.gnu.org/software/social/',
'rawdescription' => 'rawdescription' => 'Adds ActivityPub Support'];
// Todo: Translation
'Adds ActivityPub Support'];
return true; return true;
} }
@ -771,7 +778,7 @@ class ActivityPubPlugin extends Plugin
if (method_exists('ActivityUtils', 'compareVerbs')) { if (method_exists('ActivityUtils', 'compareVerbs')) {
$is_post_verb = ActivityUtils::compareVerbs( $is_post_verb = ActivityUtils::compareVerbs(
$notice->verb, $notice->verb,
array(ActivityVerb::POST) [ActivityVerb::POST]
); );
} else { } else {
$is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false); $is_post_verb = ($notice->verb == ActivityVerb::POST ? true : false);
@ -887,6 +894,53 @@ class ActivityPubReturn
echo json_encode($res, JSON_UNESCAPED_SLASHES); echo json_encode($res, JSON_UNESCAPED_SLASHES);
exit; exit;
} }
/**
* Select content type from HTTP Accept header
*
* @author Maciej Łebkowski <m.lebkowski@gmail.com>
* @param array $mimeTypes Supported Types
* @return array|null of supported mime types sorted | null if none valid
*/
public static function getBestSupportedMimeType($mimeTypes = null)
{
// Values will be stored in this array
$AcceptTypes = array();
// Accept header is case insensitive, and whitespace isnt important
$accept = strtolower(str_replace(' ', '', $_SERVER['HTTP_ACCEPT']));
// divide it into parts in the place of a ","
$accept = explode(',', $accept);
foreach ($accept as $a) {
// the default quality is 1.
$q = 1;
// check if there is a different quality
if (strpos($a, ';q=')) {
// divide "mime/type;q=X" into two parts: "mime/type" i "X"
list($a, $q) = explode(';q=', $a);
}
// mime-type $a is accepted with the quality $q
// WARNING: $q == 0 means, that mime-type isnt supported!
$AcceptTypes[$a] = $q;
}
arsort($AcceptTypes);
// if no parameter was passed, just return parsed data
if (!$mimeTypes) {
return $AcceptTypes;
}
$mimeTypes = array_map('strtolower', (array)$mimeTypes);
// lets check our supported types:
foreach ($AcceptTypes as $mime => $q) {
if ($q && in_array($mime, $mimeTypes)) {
return $mime;
}
}
// no mime-type found
return null;
}
} }
/** /**
@ -894,15 +948,16 @@ class ActivityPubReturn
*/ */
class ActivityPubURLMapperOverwrite extends URLMapper class ActivityPubURLMapperOverwrite extends URLMapper
{ {
public static function overwrite_variable($m, $path, $args, $paramPatterns, $newaction) public static function variable($m, $path, $paramPatterns, $newaction)
{ {
$mimes = [ $mimes = [
'application/activity+json', 'application/json',
'application/ld+json', 'application/activity+json',
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' 'application/ld+json',
]; 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
];
if (in_array($_SERVER["HTTP_ACCEPT"], $mimes) == false) { if (is_null(ActivityPubReturn::getBestSupportedMimeType($mimes))) {
return true; return true;
} }

68
actions/apnotice.php Normal file
View File

@ -0,0 +1,68 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 Free Software Foundation http://fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link https://www.gnu.org/software/social/
*/
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
* Notice (Local notices only)
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class apNoticeAction extends ManagedAction
{
protected $needLogin = false;
protected $canPost = true;
/**
* Handle the Notice request
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return void
*/
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.");
}
$res = Activitypub_notice::notice_to_array($notice);
ActivityPubReturn::answer($res);
}
}

View File

@ -128,7 +128,7 @@ foreach ($to_profiles as $tp) {
} }
// Add location if that is set // Add location if that is set
if (isset ($data->object->latitude, $data->object->longitude)) { if (isset($data->object->latitude, $data->object->longitude)) {
$act->context->location = Location::fromLatLon($data->object->latitude, $data->object->longitude); $act->context->location = Location::fromLatLon($data->object->latitude, $data->object->longitude);
} }

View File

@ -50,12 +50,15 @@ class Activitypub_create extends Managed_DataObject
*/ */
public static function create_to_array($id, $actor, $object) public static function create_to_array($id, $actor, $object)
{ {
$res = array("@context" => "https://www.w3.org/ns/activitystreams", $res = [
"id" => $id, '@context' => 'https://www.w3.org/ns/activitystreams',
"type" => "Create", 'id' => $id,
"actor" => $actor, 'type' => 'Create',
"object" => $object 'to' => $object['to'],
); 'cc' => $object['cc'],
'actor' => $actor,
'object' => $object
];
return $res; return $res;
} }
} }

View File

@ -0,0 +1,60 @@
<?php
/**
* GNU social - a federating social network
*
* ActivityPubPlugin implementation for GNU Social
*
* LICENCE: This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @author Daniel Supernault <danielsupernault@gmail.com>
* @copyright 2018 Free Software Foundation http://fsf.org
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link https://www.gnu.org/software/social/
*/
if (!defined('GNUSOCIAL')) {
exit(1);
}
/**
* ActivityPub Mention Tag representation
*
* @category Plugin
* @package GNUsocial
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://www.gnu.org/software/social/
*/
class Activitypub_mention_tag extends Managed_DataObject
{
/**
* Generates an ActivityPub representation of a Mention Tag
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $href Actor Uri
* @param array $name Mention name
* @return pretty array to be used in a response
*/
public static function mention_tag_to_array_from_values($href, $name)
{
$res = [
"type" => "Mention",
"href" => $href,
"name" => $name
];
return $res;
}
}

View File

@ -50,12 +50,12 @@ class Activitypub_notice extends Managed_DataObject
public static function notice_to_array($notice) public static function notice_to_array($notice)
{ {
$profile = $notice->getProfile(); $profile = $notice->getProfile();
$attachments = array(); $attachments = [];
foreach ($notice->attachments() as $attachment) { foreach ($notice->attachments() as $attachment) {
$attachments[] = Activitypub_attachment::attachment_to_array($attachment); $attachments[] = Activitypub_attachment::attachment_to_array($attachment);
} }
$tags = array(); $tags = [];
foreach ($notice->getTags() as $tag) { foreach ($notice->getTags() as $tag) {
if ($tag != "") { // Hacky workaround to avoid stupid outputs if ($tag != "") { // Hacky workaround to avoid stupid outputs
$tags[] = Activitypub_tag::tag_to_array($tag); $tags[] = Activitypub_tag::tag_to_array($tag);
@ -64,30 +64,36 @@ class Activitypub_notice extends Managed_DataObject
$to = []; $to = [];
foreach ($notice->getAttentionProfiles() as $to_profile) { foreach ($notice->getAttentionProfiles() as $to_profile) {
$to[] = $to_profile->getUri(); $to[] = $href = $to_profile->getUri();
} $tags[] = Activitypub_mention_tag::mention_tag_to_array_from_values($href, $to_profile->getNickname().'@'.parse_url($href, PHP_URL_HOST));
if (empty($to)) {
$to = array("https://www.w3.org/ns/activitystreams#Public");
} }
// In a world without walls and fences, we should make everything Public!
$to[]= 'https://www.w3.org/ns/activitystreams#Public';
$item = [ $item = [
'context' => 'https://www.w3.org/ns/activitystreams', '@context' => 'https://www.w3.org/ns/activitystreams',
'id' => $notice->getUrl(), 'id' => $notice->getUrl(),
'type' => 'Note', 'type' => 'Note',
'inReplyTo' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl(), 'published' => str_replace(' ', 'T', $notice->getCreated()).'Z',
'published' => $notice->getCreated(),
'url' => $notice->getUrl(), 'url' => $notice->getUrl(),
'atributedTo' => ActivityPubPlugin::actor_uri($profile), 'atributedTo' => ActivityPubPlugin::actor_uri($profile),
'to' => $to, 'to' => $to,
'cc' => common_local_url('apActorFollowers', ['id' => $profile->getID()]),
'atomUri' => $notice->getUrl(), 'atomUri' => $notice->getUrl(),
'inReplyToAtomUri' => empty($notice->reply_to) ? null : Notice::getById($notice->reply_to)->getUrl(),
'conversation' => $notice->getConversationUrl(), 'conversation' => $notice->getConversationUrl(),
'content' => $notice->getContent(), 'content' => $notice->getContent(),
'is_local' => $notice->isLocal(), 'isLocal' => $notice->isLocal(),
'attachment' => $attachments, 'attachment' => $attachments,
'tag' => $tags 'tag' => $tags
]; ];
// Is this a reply?
if (!empty($notice->reply_to)) {
$item['inReplyTo'] = Notice::getById($notice->reply_to)->getUrl();
$item['inReplyToAtomUri'] = Notice::getById($notice->reply_to)->getUrl();
}
// Do we have a location for this notice? // Do we have a location for this notice?
try { try {
$location = Notice_location::locFromStored($notice); $location = Notice_location::locFromStored($notice);

View File

@ -79,15 +79,22 @@ class Activitypub_pending_follow_requests extends Managed_DataObject
/** /**
* Add Follow request to table. * Add Follow request to table.
* *
* @author Diogo Cordeiro * @author Diogo Cordeiro <diogo@fc.up.pt>
* @param int32 $actor actor id * @param int32 $actor actor id
* @param int32 $remote_actor remote actor id * @param int32 $remote_actor remote actor id
* @return boolean true if added, false otherwise
*/ */
public function add() public function add()
{ {
return !$this->exists() && $this->insert(); return !$this->exists() && $this->insert();
} }
/**
* Check if a Follow request is pending.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean true if is pending, false otherwise
*/
public function exists() public function exists()
{ {
$this->_reldb = clone ($this); $this->_reldb = clone ($this);
@ -98,6 +105,12 @@ class Activitypub_pending_follow_requests extends Managed_DataObject
return false; return false;
} }
/**
* Remove a request from the pending table.
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @return boolean true if removed, false otherwise
*/
public function remove() public function remove()
{ {
return $this->exists() && $this->_reldb->delete(); return $this->exists() && $this->_reldb->delete();

View File

@ -98,20 +98,7 @@ class Activitypub_profile extends Managed_DataObject
$res = [ $res = [
'@context' => [ '@context' => [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1", "https://w3id.org/security/v1"
[
'manuallyApprovesFollowers' => 'as=>manuallyApprovesFollowers',
'sensitive' => 'as=>sensitive',
'movedTo' => 'as=>movedTo',
'Hashtag' => 'as=>Hashtag',
'ostatus' => 'http=>//ostatus.org#',
'atomUri' => 'ostatus=>atomUri',
'inReplyToAtomUri' => 'ostatus=>inReplyToAtomUri',
'conversation' => 'ostatus=>conversation',
'schema' => 'http=>//schema.org#',
'PropertyValue' => 'schema=>PropertyValue',
'value' => 'schema=>value'
]
], ],
'id' => $uri, 'id' => $uri,
'type' => 'Person', 'type' => 'Person',
@ -125,7 +112,7 @@ class Activitypub_profile extends Managed_DataObject
'url' => $profile->getUrl(), 'url' => $profile->getUrl(),
'manuallyApprovesFollowers' => false, 'manuallyApprovesFollowers' => false,
'publicKey' => [ 'publicKey' => [
'id' => $uri."#main-key", 'id' => $uri."#public-key",
'owner' => $uri, 'owner' => $uri,
'publicKeyPem' => $public_key 'publicKeyPem' => $public_key
], ],

View File

@ -68,6 +68,23 @@ class Activitypub_rsa extends Managed_DataObject
]; ];
} }
public function get_private_key($profile)
{
$this->profile_id = $profile->getID();
$apRSA = self::getKV('profile_id', $this->profile_id);
if (!$apRSA instanceof Activitypub_rsa) {
// No existing key pair for this profile
if ($profile->isLocal()) {
self::generate_keys($this->private_key, $this->public_key);
$this->store_keys();
} else {
throw new Exception('This is a remote Profile, there is no Private Key for this Profile.');
}
}
return $apRSA->private_key;
}
/** /**
* Guarantees a Public Key for a given profile. * Guarantees a Public Key for a given profile.
* *

View File

@ -5,9 +5,6 @@
"require": { "require": {
"pixelfed/http-signatures-guzzlehttp": "^4.0" "pixelfed/http-signatures-guzzlehttp": "^4.0"
}, },
"require-dev": {
"phpunit/phpunit": "^7.2"
},
"license": "AGPL", "license": "AGPL",
"autoload": { "autoload": {
"psr-4": { "psr-4": {

1813
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -100,6 +100,8 @@ class Activitypub_explorer
if (self::validate_remote_response($res)) { if (self::validate_remote_response($res)) {
$this->temp_res = $res; $this->temp_res = $res;
return true; return true;
} else {
common_debug('ActivityPub Explorer: Invalid potential remote actor while ensuring URI: '.$url. '. He returned the following: '.json_encode($res, JSON_PRETTY_PRINT));
} }
return false; return false;
@ -116,9 +118,9 @@ class Activitypub_explorer
private function grab_local_user($uri, $online = false) private function grab_local_user($uri, $online = false)
{ {
if ($online) { if ($online) {
common_debug("Explorer is searching locally for ".$uri. " online."); common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' with online resources.');
} else { } else {
common_debug("Explorer is searching locally for ".$uri. " offline."); common_debug('ActivityPub Explorer: Searching locally for '.$uri. ' offline.');
} }
// Ensure proper remote URI // Ensure proper remote URI
// If an exception occurs here it's better to just leave everything // If an exception occurs here it's better to just leave everything
@ -132,32 +134,34 @@ class Activitypub_explorer
$aprofile = self::get_aprofile_by_url($uri); $aprofile = self::get_aprofile_by_url($uri);
if ($aprofile instanceof Activitypub_profile) { if ($aprofile instanceof Activitypub_profile) {
$profile = $aprofile->local_profile(); $profile = $aprofile->local_profile();
common_debug("Explorer found a local Aprofile for ".$uri); common_debug('ActivityPub Explorer: Found a local Aprofile for '.$uri);
// We found something! // We found something!
$this->discovered_actor_profiles[]= $profile; $this->discovered_actor_profiles[]= $profile;
unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
return true; return true;
} else { } else {
common_debug("Explorer didn't find a local Aprofile for ".$uri); common_debug('ActivityPub Explorer: Unable to find a local Aprofile for '.$uri.' - looking for a Profile instead.');
// Well, maybe it is a pure blood? // Well, maybe it is a pure blood?
// Iff, we are in the same instance: // Iff, we are in the same instance:
$ACTIVITYPUB_BASE_INSTANCE_URI_length = strlen(ACTIVITYPUB_BASE_INSTANCE_URI); $ACTIVITYPUB_BASE_INSTANCE_URI_length = strlen(ACTIVITYPUB_BASE_INSTANCE_URI);
if (substr($uri, 0, $ACTIVITYPUB_BASE_INSTANCE_URI_length) == ACTIVITYPUB_BASE_INSTANCE_URI) { if (substr($uri, 0, $ACTIVITYPUB_BASE_INSTANCE_URI_length) == ACTIVITYPUB_BASE_INSTANCE_URI) {
try { try {
$profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_INSTANCE_URI_length))); $profile = Profile::getByID(intval(substr($uri, $ACTIVITYPUB_BASE_INSTANCE_URI_length)));
common_debug('ActivityPub Explorer: Found a Profile for '.$uri);
// We found something! // We found something!
$this->discovered_actor_profiles[]= $profile; $this->discovered_actor_profiles[]= $profile;
unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system unset($this->temp_res); // IMPORTANT to avoid _dangerous_ noise in the Explorer system
return true; return true;
} catch (Exception $e) { } catch (Exception $e) {
// Let the exception go on its merry way. // 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 offline grabbing failed, attempt again with online resources
if (!$online) { if (!$online) {
common_debug('ActivityPub Explorer: Will try everything again with online resources against: '.$uri);
return $this->grab_local_user($uri, true); return $this->grab_local_user($uri, true);
} }
@ -174,7 +178,7 @@ class Activitypub_explorer
*/ */
private function grab_remote_user($url) private function grab_remote_user($url)
{ {
common_debug("Explorer is grabbing a remote profile for ".$url); common_debug('ActivityPub Explorer: Trying to grab a remote actor for '.$url);
if (!isset($this->temp_res)) { if (!isset($this->temp_res)) {
$client = new HTTPClient(); $client = new HTTPClient();
$headers = array(); $headers = array();
@ -187,8 +191,10 @@ class Activitypub_explorer
unset($this->temp_res); unset($this->temp_res);
} }
if (isset($res["orderedItems"])) { // It's a potential collection of actors!!! if (isset($res["orderedItems"])) { // It's a potential collection of actors!!!
common_debug('ActivityPub Explorer: Found a collection of actors for '.$url);
foreach ($res["orderedItems"] as $profile) { foreach ($res["orderedItems"] as $profile) {
if ($this->_lookup($profile) == false) { if ($this->_lookup($profile) == false) {
common_debug('ActivityPub Explorer: Found an inavlid actor for '.$profile);
// XXX: Invalid actor found, not sure how we handle those // XXX: Invalid actor found, not sure how we handle those
} }
} }
@ -198,8 +204,11 @@ class Activitypub_explorer
} }
return true; return true;
} elseif (self::validate_remote_response($res)) { } 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); $this->discovered_actor_profiles[]= $this->store_profile($res);
return true; return true;
} else {
common_debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: '.$url. '. He returned the following: '.json_encode($res, JSON_PRETTY_PRINT));
} }
return false; return false;
@ -218,8 +227,8 @@ class Activitypub_explorer
$aprofile = new Activitypub_profile; $aprofile = new Activitypub_profile;
$aprofile->uri = $res['id']; $aprofile->uri = $res['id'];
$aprofile->nickname = $res['preferredUsername']; $aprofile->nickname = $res['preferredUsername'];
$aprofile->fullname = $res['name']; $aprofile->fullname = isset($res['name']) ? $res['name'] : null;
$aprofile->bio = substr($res['summary'], 0, 1000); $aprofile->bio = isset($res['summary']) ? substr($res['summary'], 0, 1000) : null;
$aprofile->inboxuri = $res['inbox']; $aprofile->inboxuri = $res['inbox'];
$aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox']; $aprofile->sharedInboxuri = isset($res['endpoints']['sharedInbox']) ? $res['endpoints']['sharedInbox'] : $res['inbox'];
@ -245,7 +254,7 @@ class Activitypub_explorer
*/ */
public static function validate_remote_response($res) public static function validate_remote_response($res)
{ {
if (!isset($res['id'], $res['preferredUsername'], $res['name'], $res['summary'], $res['inbox'], $res['publicKey']['publicKeyPem'])) { if (!isset($res['id'], $res['preferredUsername'], $res['inbox'], $res['publicKey']['publicKeyPem'])) {
return false; return false;
} }

View File

@ -29,6 +29,10 @@ if (!defined('GNUSOCIAL')) {
exit(1); exit(1);
} }
use GuzzleHttp\Client;
use HttpSignatures\Context;
use HttpSignatures\GuzzleHttpSignatures;
/** /**
* ActivityPub's own Postman * ActivityPub's own Postman
* *
@ -44,6 +48,7 @@ if (!defined('GNUSOCIAL')) {
class Activitypub_postman class Activitypub_postman
{ {
private $actor; private $actor;
private $actor_uri;
private $to = []; private $to = [];
private $client; private $client;
private $headers; private $headers;
@ -57,12 +62,46 @@ class Activitypub_postman
*/ */
public function __construct($from, $to = []) public function __construct($from, $to = [])
{ {
$this->client = new HTTPClient();
$this->actor = $from; $this->actor = $from;
$this->to = $to; $this->to = $to;
$this->headers = []; $this->actor_uri = ActivityPubPlugin::actor_uri($this->actor);
$this->headers[] = 'Accept: application/ld+json; profile="https://www.w3.org/ns/activitystreams"';
$this->headers[] = 'User-Agent: GNUSocialBot v0.1 - https://gnu.io/social'; $actor_private_key = new Activitypub_rsa();
$actor_private_key = $actor_private_key->get_private_key($this->actor);
$context = new Context([
'keys' => [$this->actor_uri."#public-key" => $actor_private_key],
'algorithm' => 'rsa-sha256',
'headers' => ['(request-target)', 'date', 'content-type', 'accept', 'user-agent'],
]);
$this->to = $to;
$this->headers = [
'content-type' => 'application/activity+json',
'accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
'user-agent' => 'GNUSocialBot v0.1 - https://gnu.io/social',
'date' => date('D, d M Y h:i:s') . ' GMT'
];
$handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
$this->client = new Client(['handler' => $handlerStack]);
}
/**
* Send something to remote instance
*
* @author Diogo Cordeiro <diogo@fc.up.pt>
* @param string $data request body
* @param string $inbox url of remote inbox
* @param string $method request method
* @return Psr\Http\Message\ResponseInterface
*/
public function send($data, $inbox, $method = 'POST')
{
common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);
$response = $this->client->request($method, $inbox, ['headers' => array_merge($this->headers, ['(request-target)' => strtolower($method).' '.parse_url($inbox, PHP_URL_PATH)]),'body' => $data]);
common_debug('ActivityPub Postman: Delivery result: '.$response->getBody()->getContents());
return $response;
} }
/** /**
@ -74,13 +113,12 @@ class Activitypub_postman
public function follow() public function follow()
{ {
$data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl()); $data = Activitypub_follow::follow_to_array(ActivityPubPlugin::actor_uri($this->actor), $this->to[0]->getUrl());
$this->client->setBody(json_encode($data)); $res = $this->send(json_encode($data), $this->to[0]->get_inbox());
$res = $this->client->post($this->to[0]->get_inbox(), $this->headers); $res_body = json_decode($res->getBody()->getContents());
$res_body = json_decode($res->getBody());
if ($res->isOk() || $res->getStatus() == 409) { if ($res->getStatusCode() == 200 || $res->getStatusCode() == 409) {
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID()); $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
if (! ($res->getStatus() == 409 || $res_body->type == "Accept")) { if (! ($res->getStatusCode() == 409 || $res_body->type == "Accept")) {
$pending_list->add(); $pending_list->add();
throw new Exception("Your follow request is pending acceptation."); throw new Exception("Your follow request is pending acceptation.");
} }
@ -106,11 +144,10 @@ class Activitypub_postman
$this->to[0]->getUrl() $this->to[0]->getUrl()
) )
); );
$this->client->setBody(json_encode($data)); $res = $this->send(json_encode($data), $this->to[0]->get_inbox());
$res = $this->client->post($this->to[0]->get_inbox(), $this->headers); $res_body = json_decode($res->getBody()->getContents());
$res_body = json_decode($res->getBody());
if ($res->isOk() || $res->getStatus() == 409) { if ($res->getStatusCode() == 200 || $res->getStatusCode() == 409) {
$pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID()); $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
$pending_list->remove(); $pending_list->remove();
return true; return true;
@ -133,9 +170,10 @@ class Activitypub_postman
ActivityPubPlugin::actor_uri($this->actor), ActivityPubPlugin::actor_uri($this->actor),
Activitypub_notice::notice_to_array($notice) Activitypub_notice::notice_to_array($notice)
); );
$this->client->setBody(json_encode($data)); $data = json_encode($data);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers); $this->send($data, $inbox);
} }
} }
@ -153,9 +191,10 @@ class Activitypub_postman
Activitypub_notice::notice_to_array($notice) Activitypub_notice::notice_to_array($notice)
) )
); );
$this->client->setBody(json_encode($data)); $data = json_encode($data);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers); $this->send($data, $inbox);
} }
} }
@ -169,15 +208,16 @@ class Activitypub_postman
{ {
$data = Activitypub_create::create_to_array( $data = Activitypub_create::create_to_array(
$notice->getUrl(), $notice->getUrl(),
ActivityPubPlugin::actor_uri($this->actor), $this->actor_uri,
array_merge(Activitypub_notice::notice_to_array($notice), ['cc' => common_local_url('apActorFollowers', ['id' => $this->actor->getID()]),]) Activitypub_notice::notice_to_array($notice)
); );
if (isset($notice->reply_to)) { if (isset($notice->reply_to)) {
$data["object"]["reply_to"] = $notice->getParent()->getUrl(); $data["object"]["reply_to"] = $notice->getParent()->getUrl();
} }
$this->client->setBody(json_encode($data)); $data = json_encode($data);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers); $this->send($data, $inbox);
} }
} }
@ -193,9 +233,10 @@ class Activitypub_postman
ActivityPubPlugin::actor_uri($this->actor), ActivityPubPlugin::actor_uri($this->actor),
Activitypub_notice::notice_to_array($notice) Activitypub_notice::notice_to_array($notice)
); );
$this->client->setBody(json_encode($data)); $data = json_encode($data);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$this->client->post($inbox, $this->headers); $this->send($data, $inbox);
} }
} }
@ -208,12 +249,12 @@ class Activitypub_postman
public function delete($notice) public function delete($notice)
{ {
$data = Activitypub_delete::delete_to_array(Activitypub_notice::notice_to_array($notice)); $data = Activitypub_delete::delete_to_array(Activitypub_notice::notice_to_array($notice));
$this->client->setBody(json_encode($data)); $errors = [];
$errors = array(); $data = json_encode($data);
foreach ($this->to_inbox() as $inbox) { foreach ($this->to_inbox() as $inbox) {
$res = $this->client->post($inbox, $this->headers); $res = $this->send($data, $inbox);
if (!$res->isOk()) { if (!$res->getStatusCode() == 200) {
$res_body = json_decode($res->getBody()); $res_body = json_decode($res->getBody()->getContents());
if (isset($res_body[0]->error)) { if (isset($res_body[0]->error)) {
$errors[] = ($res_body[0]->error); $errors[] = ($res_body[0]->error);
continue; continue;
@ -234,7 +275,7 @@ class Activitypub_postman
*/ */
private function to_inbox() private function to_inbox()
{ {
$to_inboxes = array(); $to_inboxes = [];
foreach ($this->to as $to_profile) { foreach ($this->to as $to_profile) {
$i = $to_profile->get_inbox(); $i = $to_profile->get_inbox();
// Prevent delivering to self // Prevent delivering to self