[PLUGIN][ActivityPub] Finish base ActivityStreams 2.0 interface

Instructions below

To extend an Activity properties do:

public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): bool {
    if ($type_name === '{Type}') {
        $validators['attribute'] = myValidator::class;
    }
    return Event::next;
}

The Validator should be of the form:

use ActivityPhp\Type;
use ActivityPhp\Type\Util;
use Plugin\ActivityPub\Util\ModelValidator;

class myValidator extends ModelValidator
{
    /**
     * Validate Attribute's value
     *
     * @param mixed $value from JSON's attribute
     * @param mixed $container A {Type}
     * @return bool
     * @throws Exception
     */
    public function validate($value, $container): bool
    {
        // Validate that container is a {Type}
        Util::subclassOf($container, Type\Extended\Object\{Type}::class, true);

        return {Validation Result};

To act on received activities do:

public function onActivityPubNew{Type}(&$obj): bool {

To add information to Activities being federated by ActivityPub do:

public function ActivityPubAddActivityStreamsTwoData(string $type_name, &$type): bool {

To implement an ActivityStreams 2.0 representation do:

public function onActivityPubActivityStreamsTwoResponse(string $route, arrray $vars, ?TypeResponse &$response = null): bool {
        if ($route === '{Object route}') {
                $response = ModelResponse::handle($vars[{Object}]);
                return Event::stop;
        }
        return Event::next;
}
This commit is contained in:
Diogo Peralta Cordeiro 2021-12-04 04:07:08 +00:00
parent 044649c745
commit 778cb57d83
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
153 changed files with 2194 additions and 7768 deletions

790
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,33 @@
declare(strict_types=1);
// {{{ License
// 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
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\ActivityPub;
use App\Core\Event;
@ -22,18 +49,30 @@ use Exception;
use Plugin\ActivityPub\Controller\Inbox;
use Plugin\ActivityPub\Entity\ActivitypubActor;
use Plugin\ActivityPub\Util\HTTPSignature;
use Plugin\ActivityPub\Util\Model\EntityToType\EntityToType;
use Plugin\ActivityPub\Util\Model;
use Plugin\ActivityPub\Util\Response\ActorResponse;
use Plugin\ActivityPub\Util\Response\NoteResponse;
use Plugin\ActivityPub\Util\Response\TypeResponse;
use Plugin\ActivityPub\Util\Type;
use Plugin\ActivityPub\Util\TypeResponse;
use Plugin\ActivityPub\Util\Validator\contentLangModelValidator;
use Plugin\ActivityPub\Util\Validator\manuallyApprovesFollowersModelValidator;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use XML_XRD;
use XML_XRD_Element_Link;
use function count;
use function Psy\debug;
use function is_null;
use const PHP_URL_HOST;
use const PREG_SET_ORDER;
/**
* Adds ActivityPub support to GNU social when enabled
*
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class ActivityPub extends Plugin
{
// ActivityStreams 2.0 Accept Headers
@ -89,6 +128,14 @@ class ActivityPub extends Plugin
return Event::next;
}
/**
* Fill Actor->getUrl() calls with correct URL coming from ActivityPub
*
* @param Actor $actor
* @param int $type
* @param string|null $url
* @return bool
*/
public function onStartGetActorUrl(Actor $actor, int $type, ?string &$url): bool
{
if (
@ -106,12 +153,80 @@ class ActivityPub extends Plugin
return Event::next;
}
/**
* Overload core endpoints to make resources available in ActivityStreams 2.0
*
* @throws Exception
*/
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): bool
{
if (count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
return Event::next;
}
switch ($route) {
case 'actor_view_id':
case 'actor_view_nickname':
$response = ActorResponse::handle($vars['actor']);
return Event::stop;
case 'note_view':
$response = NoteResponse::handle($vars['note']);
return Event::stop;
default:
if (Event::handle('ActivityPubActivityStreamsTwoResponse', [$route, $vars, &$response]) === Event::stop) {
return Event::stop;
}
return Event::next;
}
}
/**
* Add ActivityStreams 2 Extensions
*
* @param string $type_name
* @param array $validators
* @return bool
*/
public function onActivityPubValidateActivityStreamsTwoData(string $type_name, array &$validators): bool
{
switch ($type_name) {
case 'Person':
$validators['manuallyApprovesFollowers'] = manuallyApprovesFollowersModelValidator::class;
break;
case 'Note':
$validators['contentLang'] = contentLangModelValidator::class;
break;
}
return Event::next;
}
// FreeNetworkComponent Events
/**
* Let FreeNetwork Component know we exist and which class to use to call the freeNetworkDistribute method
*
* @param array $protocols
* @return bool
*/
public function onAddFreeNetworkProtocol(array &$protocols): bool
{
$protocols[] = '\Plugin\ActivityPub\ActivityPub';
return Event::next;
}
/**
* The FreeNetwork component will call this function to distribute this instance's activities
*
* @param Actor $sender
* @param Activity $activity
* @param array $targets
* @param string|null $reason
* @param array $delivered
* @return bool
* @throws ClientExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
public static function freeNetworkDistribute(Actor $sender, Activity $activity, array $targets, ?string $reason = null, array &$delivered = []): bool
{
$to_addr = [];
@ -130,14 +245,13 @@ class ActivityPub extends Plugin
//$to_failed = [];
foreach ($to_addr as $inbox => $dummy) {
try {
$res = self::postman($sender, EntityToType::translate($activity), $inbox);
$res = self::postman($sender, Model::toJson($activity), $inbox);
// accumulate errors for later use, if needed
$status_code = $res->getStatusCode();
if (!($status_code === 200 || $status_code === 202 || $status_code === 409)) {
$res_body = json_decode($res->getContent(), true);
$errors[] = isset($res_body['error']) ?
$res_body['error'] : "An unknown error occurred.";
$errors[] = $res_body['error'] ?? 'An unknown error occurred.';
//$to_failed[$inbox] = $activity;
} else {
array_push($delivered, ...$dummy);
@ -145,7 +259,7 @@ class ActivityPub extends Plugin
FreeNetworkActorProtocol::protocolSucceeded(
'activitypub',
$actor,
Discovery::normalize($actor->getNickname() . '@' . parse_url($inbox, PHP_URL_HOST))
Discovery::normalize($actor->getNickname() . '@' . parse_url($inbox, PHP_URL_HOST)),
);
}
}
@ -153,7 +267,6 @@ class ActivityPub extends Plugin
Log::error('ActivityPub @ freeNetworkDistribute: ' . $e->getMessage());
//$to_failed[$inbox] = $activity;
}
}
if (!empty($errors)) {
@ -165,27 +278,117 @@ class ActivityPub extends Plugin
}
/**
* Internal tool to sign and send activities out
*
* @param Actor $sender
* @param Type $activity
* @param string $json_activity
* @param string $inbox
* @param string $method
* @return ResponseInterface
* @throws Exception
*/
public static function postman(Actor $sender, mixed $activity, string $inbox, string $method = 'post'): ResponseInterface
private static function postman(Actor $sender, string $json_activity, string $inbox, string $method = 'post'): ResponseInterface
{
$data = $activity->toJson();
Log::debug('ActivityPub Postman: Delivering ' . $data . ' to ' . $inbox);
Log::debug('ActivityPub Postman: Delivering ' . $json_activity . ' to ' . $inbox);
$headers = HTTPSignature::sign($sender, $inbox, $data);
$headers = HTTPSignature::sign($sender, $inbox, $json_activity);
Log::debug('ActivityPub Postman: Delivery headers were: ' . print_r($headers, true));
$response = HTTPClient::$method($inbox, ['headers' => $headers, 'body' => $data]);
$response = HTTPClient::$method($inbox, ['headers' => $headers, 'body' => $json_activity]);
Log::debug('ActivityPub Postman: Delivery result with status code ' . $response->getStatusCode() . ': ' . $response->getContent());
return $response;
}
// WebFinger Events
public static function getActorByUri(string $resource, ?bool $attempt_fetch = true): Actor
/**
* Add activity+json mimetype to WebFinger
*
* @param XML_XRD $xrd
* @param Actor $object
* @return bool
*/
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): bool
{
if ($object->isPerson()) {
$link = new XML_XRD_Element_Link(
rel: 'self',
href: $object->getUri(Router::ABSOLUTE_URL),//Router::url('actor_view_id', ['id' => $object->getId()], Router::ABSOLUTE_URL),
type: 'application/activity+json',
);
$xrd->links[] = clone $link;
}
return Event::next;
}
/**
* When FreeNetwork component asks us to help with identifying Actors from XRDs
*
* @param XML_XRD $xrd
* @param Actor|null $actor
* @return bool
*/
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): bool
{
$addr = null;
foreach ($xrd->aliases as $alias) {
if (Discovery::isAcct($alias)) {
$addr = Discovery::normalize($alias);
}
}
if (is_null($addr)) {
return Event::next;
} else {
if (!FreeNetworkActorProtocol::canIAddr('activitypub', $addr)) {
return Event::next;
}
}
try {
$ap_actor = ActivitypubActor::fromXrd($addr, $xrd);
$actor = Actor::getById($ap_actor->getActorId());
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor, $addr);
return Event::stop;
} catch (Exception $e) {
Log::error('ActivityPub Actor from URL Mention check failed: ' . $e->getMessage());
return Event::next;
}
}
// Discovery Events
/**
* When FreeNetwork component asks us to help with identifying Actors from URIs
*
* @param string $target
* @param Actor|null $actor
* @return bool
*/
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool
{
try {
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
$ap_actor = ActivitypubActor::getByAddr($addr);
$actor = Actor::getById($ap_actor->getActorId());
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId(), $addr);
return Event::stop;
} else {
return Event::next;
}
} catch (Exception $e) {
Log::error('ActivityPub Webfinger Mention check failed: ' . $e->getMessage());
return Event::next;
}
}
/**
* Get an Actor from ActivityPub URI, if it doesn't exist, attempt to fetch it
* This should only be necessary internally.
*
* @param string $resource
* @return Actor got from URI
* @throws NoSuchActorException
*/
public static function getActorByUri(string $resource): Actor
{
// Try local
if (Common::isValidHttpUrl($resource)) {
@ -213,168 +416,4 @@ class ActivityPub extends Plugin
throw new NoSuchActorException("From URI: {$resource}");
}
}
/**
* @throws Exception
*/
public function onControllerResponseInFormat(string $route, array $accept_header, array $vars, ?TypeResponse &$response = null): bool
{
if (count(array_intersect(self::$accept_headers, $accept_header)) === 0) {
return Event::next;
}
switch ($route) {
case 'actor_view_id':
case 'actor_view_nickname':
$response = ActorResponse::handle($vars['actor']);
return Event::stop;
case 'note_view':
$response = NoteResponse::handle($vars['note']);
return Event::stop;
/*case 'actor_favourites_id':
case 'actor_favourites_nickname':
$response = LikeResponse::handle($vars['actor']);
return Event::stop;
case 'actor_subscriptions_id':
case 'actor_subscriptions_nickname':
$response = FollowingResponse::handle($vars['actor']);
return Event::stop;
case 'actor_subscribers_id':
case 'actor_subscribers_nickname':
$response = FollowersResponse::handle($vars['actor']);
return Event::stop;*/
default:
if (Event::handle('ActivityStreamsTwoResponse', [$route, &$response]) == Event::stop) {
return Event::stop;
}
return Event::next;
}
}
/********************************************************
* WebFinger Events *
********************************************************/
/**
* Add activity+json mimetype on WebFinger
*
* @throws Exception
*/
public function onEndWebFingerProfileLinks(XML_XRD $xrd, Actor $object): bool
{
if ($object->isPerson()) {
$link = new XML_XRD_Element_Link(
rel: 'self',
href: $object->getUri(Router::ABSOLUTE_URL),//Router::url('actor_view_id', ['id' => $object->getId()], Router::ABSOLUTE_URL),
type: 'application/activity+json',
);
$xrd->links[] = clone $link;
}
return Event::next;
}
public function onFreeNetworkFindMentions(string $target, ?Actor &$actor = null): bool
{
try {
if (FreeNetworkActorProtocol::canIAddr('activitypub', $addr = Discovery::normalize($target))) {
$ap_actor = ActivitypubActor::getByAddr($addr);
$actor = Actor::getById($ap_actor->getActorId());
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId(), $addr);
return Event::stop;
} else {
return Event::next;
}
} catch (Exception $e) {
Log::error("ActivityPub Webfinger Mention check failed: " . $e->getMessage());
return Event::next;
}
}
public function onFreeNetworkFoundXrd(XML_XRD $xrd, ?Actor &$actor = null): bool
{
$addr = null;
foreach ($xrd->aliases as $alias) {
if (Discovery::isAcct($alias)) {
$addr = Discovery::normalize($alias);
}
}
if (is_null($addr)) {
return Event::next;
} else {
if (!FreeNetworkActorProtocol::canIAddr('activitypub', $addr)) {
return Event::next;
}
}
try {
$ap_actor = ActivitypubActor::fromXrd($addr, $xrd);
$actor = Actor::getById($ap_actor->getActorId());
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor, $addr);
return Event::stop;
} catch (Exception $e) {
Log::error("ActivityPub Actor from URL Mention check failed: " . $e->getMessage());
return Event::next;
}
}
/**
* Allow remote profile references to be used in commands:
* sub update@status.net
* whois evan@identi.ca
* reply http://identi.ca/evan hey what's up
*
* @param Command $command
* @param string $arg
* @param Actor &$profile
* @return bool hook return code
* @author GNU social
*/
//public function onStartCommandGetProfile($command, $arg, &$profile)
//{
// $aprofile = ActivitypubActor::fromUri($arg);
// if (!($aprofile instanceof ActivitypubActor)) {
// // No remote ActivityPub profile found
// return Event::next;
// }
//
// return Event::stop;
//}
/********************************************************
* Discovery Events *
********************************************************/
/**
* Profile from URI.
*
* @param string $uri
* @param Actor &$profile in/out param: Profile got from URI
* @return mixed hook return code
* @author GNU social
*/
//public function onStartGetProfileFromURI($uri, &$profile)
//{
// try {
// $profile = Explorer::get_profile_from_url($uri);
// return Event::stop;
// } catch (Exception) {
// return Event::next; // It's not an ActivityPub profile as far as we know, continue event handling
// }
//}
/**
* Try to grab and store the remote profile by the given uri
*
* @param string $uri
* @param Actor|null &$profile
* @return bool
*/
//public function onRemoteFollowPullProfile(string $uri, ?Actor &$profile): bool
//{
// $aprofile = ActivitypubActor::fromUri($uri);
// if (!($aprofile instanceof ActivitypubActor)) {
// // No remote ActivityPub profile found
// return Event::next;
// }
//
// return is_null($profile) ? Event::next : Event::stop;
//}
}

View File

@ -1,9 +1,8 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
// {{{ License
// 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
@ -18,9 +17,18 @@ declare(strict_types = 1);
//
// 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
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\ActivityPub\Controller;
use App\Core\Controller;
@ -28,30 +36,37 @@ use App\Core\DB\DB;
use App\Core\Log;
use App\Core\Router\Router;
use App\Entity\Actor;
use App\Util\Exception\ClientException;
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol;
use Component\FreeNetwork\Util\Discovery;
use Exception;
use Plugin\ActivityPub\Entity\ActivitypubActor;
use Plugin\ActivityPub\Entity\ActivitypubRsa;
use Plugin\ActivityPub\Util\Explorer;
use Plugin\ActivityPub\Util\HTTPSignature;
use Plugin\ActivityPub\Util\Model;
use Plugin\ActivityPub\Util\TypeResponse;
use function App\Core\I18n\_m;
use App\Util\Exception\ClientException;
use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Util\Model\AS2ToEntity\AS2ToEntity;
use Plugin\ActivityPub\Util\Response\TypeResponse;
use Plugin\ActivityPub\Util\Type;
use Plugin\ActivityPub\Util\Type\Util;
use function is_null;
use const PHP_URL_HOST;
/**
* ActivityPub Inbox Handler
*
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Inbox extends Controller
{
/**
* Inbox handler
* Create an Inbox Handler to receive something from someone.
*/
public function handle(?int $gsactor_id = null): TypeResponse
{
$error = fn(string $m): TypeResponse => new TypeResponse(json_encode(['error' => $m]));
$path = Router::url('activitypub_inbox', type: Router::ABSOLUTE_PATH);
if (!\is_null($gsactor_id)) {
if (!is_null($gsactor_id)) {
try {
$user = DB::findOneBy('local_user', ['id' => $gsactor_id]);
$path = Router::url('activitypub_actor_inbox', ['gsactor_id' => $user->getId()], type: Router::ABSOLUTE_PATH);
@ -61,19 +76,19 @@ class Inbox extends Controller
}
Log::debug('ActivityPub Inbox: Received a POST request.');
$body = (string) $this->request->getContent();
$type = Type::fromJson($body);
$body = (string)$this->request->getContent();
$type = Model::jsonToType($body);
if ($type->has('actor') === false) {
ActivityPubReturn::error('Actor not found in the request.');
$error('Actor not found in the request.');
}
try {
$ap_actor = ActivitypubActor::fromUri($type->get('actor'));
$actor = Actor::getById($ap_actor->getActorId());
DB::flush();
} catch (Exception) {
ActivityPubReturn::error('Invalid actor.');
} catch (Exception $e) {
$error('Invalid actor.');
}
$actor_public_key = ActivitypubRsa::getByActor($actor)->getPublicKey();
@ -86,7 +101,7 @@ class Inbox extends Controller
if (!isset($headers['signature'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: Missing Signature header.');
ActivityPubReturn::error('Missing Signature header.', 400);
$error('Missing Signature header.', 400);
// TODO: support other methods beyond HTTP Signatures
}
@ -95,7 +110,7 @@ class Inbox extends Controller
Log::debug('ActivityPub Inbox: HTTP Signature Data: ' . print_r($signatureData, true));
if (isset($signatureData['error'])) {
Log::debug('ActivityPub Inbox: HTTP Signature: ' . json_encode($signatureData, JSON_PRETTY_PRINT));
ActivityPubReturn::error(json_encode($signatureData, JSON_PRETTY_PRINT), 400);
$error(json_encode($signatureData, JSON_PRETTY_PRINT), 400);
}
list($verified, /*$headers*/) = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
@ -105,12 +120,12 @@ class Inbox extends Controller
try {
$res = Explorer::get_remote_user_activity($ap_actor->getUri());
} catch (Exception) {
ActivityPubReturn::error('Invalid remote actor.');
$error('Invalid remote actor.');
}
try {
$actor = ActivitypubActor::update_profile($ap_actor, $res);
} catch (Exception) {
ActivityPubReturn::error('Failed to updated remote actor information.');
$error('Failed to updated remote actor information.');
}
[$verified, /*$headers*/] = HTTPSignature::verify($actor_public_key, $signatureData, $headers, $path, $body);
@ -119,7 +134,7 @@ class Inbox extends Controller
// If it still failed despite profile update
if ($verified !== 1) {
Log::debug('ActivityPub Inbox: HTTP Signature: Invalid signature.');
ActivityPubReturn::error('Invalid signature.');
$error('Invalid signature.');
}
// HTTP signature checked out, make sure the "actor" of the activity matches that of the signature
@ -128,8 +143,12 @@ class Inbox extends Controller
// TODO: Check if Actor has authority over payload
// Store Activity
$ap_act = AS2ToEntity::store(activity: $type->toArray(), source: 'ActivityPub');
FreeNetworkActorProtocol::protocolSucceeded('activitypub', $actor->getId());
$ap_act = Model\Activity::fromJson($type, ['source' => 'ActivityPub']);
FreeNetworkActorProtocol::protocolSucceeded(
'activitypub',
$ap_actor->getActorId(),
Discovery::normalize($actor->getNickname() . '@' . parse_url($ap_actor->getInboxUri(), PHP_URL_HOST))
);
DB::flush();
dd($ap_act, $act = $ap_act->getActivity(), $act->getActor(), $act->getObject());

View File

@ -1,9 +1,8 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
// {{{ License
// 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
@ -18,9 +17,18 @@ declare(strict_types = 1);
//
// 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
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\ActivityPub\Entity;
use App\Core\DB\DB;
@ -29,14 +37,9 @@ use App\Entity\Activity;
use DateTimeInterface;
/**
* Entity for all activities we know about
* Table Definition for activitypub_activity
*
* @category DB
* @package GNUsocial
*
* @author Hugo Sales <hugo@hsal.es>
* @author Diogo Peralta Cordeiro <mail@diogo.site>
* @copyright 2020-2021 Free Software Foundation, Inc http://www.fsf.org
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class ActivitypubActivity extends Entity
@ -127,19 +130,19 @@ class ActivitypubActivity extends Entity
public static function schemaDef(): array
{
return [
'name' => 'activitypub_activity',
'name' => 'activitypub_activity',
'fields' => [
'activity_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Activity.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'activity_id to give attention'],
'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'],
'object_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Object\'s URI'],
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'whether this was a locally generated or an imported activity'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
'activity_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'Activity.id', 'multiplicity' => 'one to one', 'not null' => true, 'description' => 'activity_id to give attention'],
'activity_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Activity\'s URI'],
'object_uri' => ['type' => 'text', 'not null' => true, 'description' => 'Object\'s URI'],
'is_local' => ['type' => 'bool', 'not null' => true, 'description' => 'whether this was a locally generated or an imported activity'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
],
'primary key' => ['activity_uri'],
'indexes' => [
'indexes' => [
'activity_activity_uri_idx' => ['activity_uri'],
'activity_object_uri_idx' => ['object_uri'],
'activity_object_uri_idx' => ['object_uri'],
],
];
}

View File

@ -1,8 +1,8 @@
<?php
declare(strict_types = 1);
// {{{ License
declare(strict_types=1);
// {{{ License
// 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
@ -17,16 +17,15 @@ declare(strict_types = 1);
//
// 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 Peralta Cordeiro <mail@diogo.site
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
@ -34,20 +33,21 @@ namespace Plugin\ActivityPub\Entity;
use App\Core\Cache;
use App\Core\Entity;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Entity\Actor;
use Component\FreeNetwork\Util\Discovery;
use DateTimeInterface;
use Exception;
use Plugin\ActivityPub\Util\DiscoveryHints;
use Plugin\ActivityPub\Util\Explorer;
use XML_XRD;
use function App\Core\I18n\_m;
use function array_key_exists;
use function is_null;
/**
* Table Definition for activitypub_actor
*
* @author Diogo Peralta Cordeiro <mail@diogo.site
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class ActivitypubActor extends Entity
@ -157,10 +157,10 @@ class ActivitypubActor extends Entity
$addr = Discovery::normalize($addr);
// Try the cache
$uri = Cache::get(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), fn () => false);
$uri = Cache::get(sprintf('ActivitypubActor-webfinger-%s', urlencode($addr)), fn() => false);
if ($uri !== false) {
if (\is_null($uri)) {
if (is_null($uri)) {
// TRANS: Exception.
throw new Exception(_m('Not a valid WebFinger address (via cache).'));
}
@ -189,14 +189,14 @@ class ActivitypubActor extends Entity
return self::fromXrd($addr, $xrd);
}
public static function fromXrd(string $addr, \XML_XRD $xrd): self
public static function fromXrd(string $addr, XML_XRD $xrd): self
{
$hints = array_merge(
['webfinger' => $addr],
DiscoveryHints::fromXRD($xrd),
);
if (\array_key_exists('activitypub', $hints)) {
if (array_key_exists('activitypub', $hints)) {
$uri = $hints['activitypub'];
try {
LOG::info("Discovery on acct:{$addr} with URI:{$uri}");
@ -240,17 +240,17 @@ class ActivitypubActor extends Entity
public static function schemaDef(): array
{
return [
'name' => 'activitypub_actor',
'name' => 'activitypub_actor',
'fields' => [
'uri' => ['type' => 'text', 'not null' => true],
'actor_id' => ['type' => 'int', 'not null' => true],
'inbox_uri' => ['type' => 'text', 'not null' => true],
'uri' => ['type' => 'text', 'not null' => true],
'actor_id' => ['type' => 'int', 'not null' => true],
'inbox_uri' => ['type' => 'text', 'not null' => true],
'inbox_shared_uri' => ['type' => 'text'],
'url' => ['type' => 'text'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
'url' => ['type' => 'text'],
'created' => ['type' => 'datetime', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was created'],
'modified' => ['type' => 'timestamp', 'not null' => true, 'default' => 'CURRENT_TIMESTAMP', 'description' => 'date this record was modified'],
],
'primary key' => ['actor_id'],
'primary key' => ['actor_id'],
'foreign keys' => [
'activitypub_actor_actor_id_fkey' => ['actor', ['actor_id' => 'id']],
],

View File

@ -1,4 +1,8 @@
<?php
declare(strict_types=1);
// {{{ License
// 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
@ -13,6 +17,17 @@
//
// 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
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\ActivityPub\Entity;
@ -22,25 +37,11 @@ use App\Core\Log;
use App\Entity\Actor;
use App\Util\Exception\ServerException;
use DateTimeInterface;
use Doctrine\ORM\UnitOfWork;
use Exception;
/**
* ActivityPub implementation for GNU social
*
* @package GNUsocial
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 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/
*/
/**
* ActivityPub Keys System
*
* @category Plugin
* @package GNUsocial
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class ActivitypubRsa extends Entity
@ -137,7 +138,7 @@ class ActivitypubRsa extends Entity
* Guarantees RSA keys for a given actor.
*
* @param Actor $gsactor
* @param bool $fetch=true Should attempt to fetch keys from a remote profile?
* @param bool $fetch =true Should attempt to fetch keys from a remote profile?
* @return ActivitypubRsa The keys (private key is null for remote actors)
* @throws ServerException It should never occur, but if so, we break everything!
*/

View File

@ -1,6 +1,33 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
// {{{ License
// 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
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
namespace Plugin\ActivityPub\Util;
@ -19,31 +46,24 @@ namespace Plugin\ActivityPub\Util;
// 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/>.
use App\Core\Event;
use App\Core\HTTPClient;
use Component\FreeNetwork\Util\Discovery;
use Component\FreeNetwork\Util\WebfingerResource\WebfingerResourceActor;
use Mf2 as Mf2;
use XML_XRD;
/**
* ActivityPub implementation for GNU social
* DiscoveryHints 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, 2021 Free Software Foundation, Inc http://www.fsf.org
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*
* @see http://www.gnu.org/software/social/
*/
class DiscoveryHints
{
public static function fromXRD(XML_XRD $xrd)
/**
* Search the WebFinger XRD after an ActivityPub URI
*
* @param XML_XRD $xrd
* @return array
*/
public static function fromXRD(XML_XRD $xrd): array
{
$hints = [];
@ -59,121 +79,4 @@ class DiscoveryHints
return $hints;
}
}
//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 WebfingerResourceActor::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)
// {
// $response = HTTPClient::get($url, ['headers' => ['Accept' => 'text/html,application/xhtml+xml']]);
//
// if (!HTTPClient::statusCodeIsOkay($response)) {
// return null;
// }
//
// return self::hcardHints(
// $response->getContent(),
// HTTPClient::getEffectiveUrl($response)
// );
// }
//
// 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;
// }
//
// private 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;
// }
//}
}

View File

@ -1,7 +1,8 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
// {{{ License
// 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
@ -16,65 +17,58 @@ declare(strict_types = 1);
//
// 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/>.
namespace Plugin\ActivityPub\Util;
use App\Core\DB\DB;
use App\Core\HTTPClient;
use App\Core\Log;
use App\Core\Security;
use App\Entity\Actor;
use App\Util\Exception\NoSuchActorException;
use App\Util\Formatting;
use DateTime;
use Exception;
use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubActor;
use Plugin\ActivityPub\Entity\ActivitypubRsa;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
// }}}
/**
* ActivityPub implementation for GNU social
*
* @package GNUsocial
*
* @category ActivityPub
* @author Diogo Peralta Cordeiro <@diogo.site>
* @copyright 2018-2019, 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*
* @see http://www.gnu.org/software/social/
*/
namespace Plugin\ActivityPub\Util;
use App\Core\HTTPClient;
use App\Core\Log;
use App\Util\Exception\NoSuchActorException;
use Exception;
use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubActor;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use function in_array;
use function is_null;
use const JSON_UNESCAPED_SLASHES;
/**
* ActivityPub's own Explorer
*
* Allows to discovery new remote actors
*
* @author Diogo Peralta Cordeiro (@diogo.site)
*
* @category Plugin
* @package GNUsocial
*
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
* @copyright 2021 Free Software Foundation, Inc http://www.fsf.org
* @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
*/
class Explorer
{
private $discovered_actor_profiles = [];
private array $discovered_actor_profiles = [];
/**
* Shortcut function to get a single profile from its URL.
*
* @param string $url
* @param bool $grab_online whether to try online grabbing, defaults to true
*
* @return ActivitypubActor
* @throws ClientExceptionInterface
* @throws NoSuchActorException
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*
* @return Actor
*/
public static function get_profile_from_url(string $url, bool $grab_online = true): ActivitypubActor
{
@ -92,8 +86,8 @@ class Explorer
* This function cleans the $this->discovered_actor_profiles array
* so that there is no erroneous data
*
* @param string $url User's url
* @param bool $grab_online whether to try online grabbing, defaults to true
* @param string $url User's url
* @param bool $grab_online whether to try online grabbing, defaults to true
*
* @throws ClientExceptionInterface
* @throws NoSuchActorException
@ -101,11 +95,11 @@ class Explorer
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*
* @return array of Profile objects
* @return array of Actor objects
*/
public function lookup(string $url, bool $grab_online = true)
{
if (\in_array($url, ActivityPub::PUBLIC_TO)) {
if (in_array($url, ActivityPub::PUBLIC_TO)) {
return [];
}
@ -120,8 +114,8 @@ class Explorer
* This is a recursive function that will accumulate the results on
* $discovered_actor_profiles array
*
* @param string $url User's url
* @param bool $grab_online whether to try online grabbing, defaults to true
* @param string $url User's url
* @param bool $grab_online whether to try online grabbing, defaults to true
*
* @throws ClientExceptionInterface
* @throws NoSuchActorException
@ -193,95 +187,38 @@ class Explorer
{
Log::debug('ActivityPub Explorer: Trying to grab a remote actor for ' . $url);
$response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
$res = json_decode($response->getContent(), true);
$res = json_decode($response->getContent(), true);
if ($response->getStatusCode() == 410) { // If it was deleted
return true; // Nothing to add.
} elseif (!HTTPClient::statusCodeIsOkay($response)) { // If it is unavailable
return false; // Try to add at another time.
}
if (\is_null($res)) {
Log::debug('ActivityPub Explorer: Invalid JSON returned from given Actor URL: ' . $response->getContent());
if (is_null($res)) {
Log::debug('ActivityPub Explorer: Invalid response returned from given Actor URL: ' . $res);
return true; // Nothing to add.
}
if (isset($res['type']) && $res['type'] === 'OrderedCollection' && isset($res['first'])) { // It's a potential collection of actors!!!
if ($res['type'] === 'OrderedCollection') { // It's a potential collection of actors!!!
Log::debug('ActivityPub Explorer: Found a collection of actors for ' . $url);
$this->travel_collection($res['first']);
return true;
} elseif (self::validate_remote_response($res)) {
Log::debug('ActivityPub Explorer: Found a valid remote actor for ' . $url);
$this->discovered_actor_profiles[] = $this->store_profile($res);
return true;
} else {
Log::debug('ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: ' . $url . '. He returned the following: ' . json_encode($res, \JSON_UNESCAPED_SLASHES));
return false;
try {
$this->discovered_actor_profiles[] = Model\Actor::fromJson(json_encode($res));
return true;
} catch (Exception $e) {
Log::debug(
'ActivityPub Explorer: Invalid potential remote actor while grabbing remotely: ' . $url
. '. He returned the following: ' . json_encode($res, JSON_UNESCAPED_SLASHES)
. ' and the following exception: ' . $e->getMessage()
);
return false;
}
}
return false;
}
/**
* Save remote user profile in known instance
*
* @param array $res remote response
*
* @throws Exception
* @throws NoSuchActorException
*
* @return Actor remote Profile object
*/
private function store_profile(array $res): ActivitypubActor
{
// Actor
$actor_map = [
'nickname' => $res['preferredUsername'],
'fullname' => $res['name'] ?? null,
'created' => new DateTime($res['published'] ?? 'now'),
'bio' => isset($res['summary']) ? mb_substr(Security::sanitize($res['summary']), 0, 1000) : null,
'is_local' => false,
'modified' => new DateTime(),
];
$actor = new Actor();
foreach ($actor_map as $prop => $val) {
$set = Formatting::snakeCaseToCamelCase("set_{$prop}");
$actor->{$set}($val);
}
DB::persist($actor);
// ActivityPub Actor
$aprofile = ActivitypubActor::create([
'inbox_uri' => $res['inbox'],
'inbox_shared_uri' => $res['endpoints']['sharedInbox'],
'uri' => $res['id'],
'actor_id' => $actor->getId(),
'url' => $res['url'] ?? null,
]);
DB::persist($aprofile);
// Public Key
$apRSA = ActivitypubRsa::create([
'actor_id' => $actor->getID(),
'public_key' => $res['publicKey']['publicKeyPem'],
]);
DB::persist($apRSA);
// 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
// Log::debug('ActivityPub Explorer: An error ocurred while grabbing remote avatar: ' . $e->getMessage());
// }
//}
return $aprofile;
}
/**
* Validates a remote response in order to determine whether this
* response is a valid profile or not
@ -305,18 +242,24 @@ class Explorer
public static function get_aprofile_by_url(string $v): ActivitypubActor|bool
{
$aprofile = ActivitypubActor::getWithPK(['uri' => $v]);
return \is_null($aprofile) ? false : ActivitypubActor::getWithPK(['uri' => $v]);
return is_null($aprofile) ? false : ActivitypubActor::getWithPK(['uri' => $v]);
}
/**
* Allows the Explorer to transverse a collection of persons.
*
* @param string $url
* @return bool
* @throws ClientExceptionInterface
* @throws NoSuchActorException
* @throws RedirectionExceptionInterface
* @throws ServerExceptionInterface
* @throws TransportExceptionInterface
*/
private function travel_collection(string $url): bool
{
$response = HTTPClient::get($url, ['headers' => ACTIVITYPUB::HTTP_CLIENT_HEADERS]);
$res = json_decode($response->getContent(), true);
$res = json_decode($response->getContent(), true);
if (!isset($res['orderedItems'])) {
return false;
@ -328,7 +271,7 @@ class Explorer
}
}
// Go through entire collection