First unstable federation release
This commit is contained in:
parent
20738f48cd
commit
f8048c7565
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/vendor/
|
/vendor/
|
||||||
|
composer.lock
|
||||||
|
@ -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 isn’t 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 isn’t 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);
|
||||||
|
|
||||||
|
// let’s 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
68
actions/apnotice.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
60
classes/Activitypub_mention_tag.php
Normal file
60
classes/Activitypub_mention_tag.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
@ -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
|
||||||
],
|
],
|
||||||
|
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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
1813
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user