2021-08-24 20:29:26 +01:00
< ? php
2021-12-24 01:58:41 +00:00
declare ( strict_types = 1 );
2021-10-10 09:26:18 +01:00
2021-12-04 04:07:08 +00:00
// {{{ 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
2021-12-24 01:58:41 +00:00
*
2021-12-04 04:07:08 +00:00
* @ 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
*/
2021-08-24 20:29:26 +01:00
namespace Plugin\ActivityPub ;
2022-02-11 11:39:25 +00:00
use ActivityPhp\Type ;
2022-03-05 14:23:08 +00:00
use ActivityPhp\Type\AbstractObject ;
2022-03-27 15:19:09 +01:00
use App\Core\DB ;
2021-08-24 20:29:26 +01:00
use App\Core\Event ;
2021-11-29 23:58:42 +00:00
use App\Core\HTTPClient ;
2022-03-05 14:23:08 +00:00
use function App\Core\I18n\_m ;
2021-11-01 12:16:46 +00:00
use App\Core\Log ;
2021-08-24 20:29:26 +01:00
use App\Core\Modules\Plugin ;
2022-03-27 16:21:19 +01:00
use App\Core\Queue ;
2022-03-27 16:43:59 +01:00
use App\Core\Router ;
2021-11-29 23:58:42 +00:00
use App\Entity\Activity ;
2021-10-18 13:22:02 +01:00
use App\Entity\Actor ;
2021-12-08 22:24:52 +00:00
use App\Entity\Note ;
2021-11-01 12:16:46 +00:00
use App\Util\Common ;
2022-01-02 20:37:15 +00:00
use App\Util\Exception\BugFoundException ;
2022-01-11 20:28:15 +00:00
use Component\Collection\Util\Controller\OrderedCollection ;
2021-12-02 04:25:58 +00:00
use Component\FreeNetwork\Entity\FreeNetworkActorProtocol ;
use Component\FreeNetwork\Util\Discovery ;
2022-04-03 21:40:32 +01:00
use EventResult ;
2021-08-24 20:29:26 +01:00
use Exception ;
2021-12-24 01:58:41 +00:00
use InvalidArgumentException ;
2021-08-24 20:29:26 +01:00
use Plugin\ActivityPub\Controller\Inbox ;
2022-01-11 20:28:15 +00:00
use Plugin\ActivityPub\Controller\Outbox ;
2021-12-08 22:24:52 +00:00
use Plugin\ActivityPub\Entity\ActivitypubActivity ;
2021-10-27 04:14:01 +01:00
use Plugin\ActivityPub\Entity\ActivitypubActor ;
2021-12-08 22:24:52 +00:00
use Plugin\ActivityPub\Entity\ActivitypubObject ;
2022-02-25 01:05:28 +00:00
use Plugin\ActivityPub\Util\Explorer ;
2021-11-30 16:47:31 +00:00
use Plugin\ActivityPub\Util\HTTPSignature ;
2021-12-04 04:07:08 +00:00
use Plugin\ActivityPub\Util\Model ;
2022-01-11 20:28:15 +00:00
use Plugin\ActivityPub\Util\OrderedCollectionController ;
2022-02-23 17:39:11 +00:00
use Plugin\ActivityPub\Util\Response\ActivityResponse ;
2021-10-04 17:00:58 +01:00
use Plugin\ActivityPub\Util\Response\ActorResponse ;
use Plugin\ActivityPub\Util\Response\NoteResponse ;
2021-12-04 04:07:08 +00:00
use Plugin\ActivityPub\Util\TypeResponse ;
use Plugin\ActivityPub\Util\Validator\contentLangModelValidator ;
use Plugin\ActivityPub\Util\Validator\manuallyApprovesFollowersModelValidator ;
2022-02-23 17:39:11 +00:00
use Symfony\Component\HttpFoundation\JsonResponse ;
2021-12-04 04:07:08 +00:00
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface ;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface ;
2021-11-29 23:58:42 +00:00
use Symfony\Contracts\HttpClient\ResponseInterface ;
2021-10-18 13:22:02 +01:00
use XML_XRD ;
use XML_XRD_Element_Link ;
2021-08-24 20:29:26 +01:00
2021-12-04 04:07:08 +00:00
/**
* 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
*/
2021-08-24 20:29:26 +01:00
class ActivityPub extends Plugin
{
2021-10-27 04:14:01 +01:00
// ActivityStreams 2.0 Accept Headers
2021-10-18 13:22:02 +01:00
public static array $accept_headers = [
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ,
'application/activity+json' ,
'application/json' ,
'application/ld+json' ,
];
2021-10-27 04:14:01 +01:00
// So that this isn't hardcoded everywhere
2021-11-27 15:06:46 +00:00
public const PUBLIC_TO = [
'https://www.w3.org/ns/activitystreams#Public' ,
2021-10-27 04:14:01 +01:00
'Public' ,
'as:Public' ,
];
public const HTTP_CLIENT_HEADERS = [
2021-12-24 01:58:41 +00:00
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' ,
2021-10-27 04:14:01 +01:00
'User-Agent' => 'GNUsocialBot ' . GNUSOCIAL_VERSION . ' - ' . GNUSOCIAL_PROJECT_URL ,
];
2022-03-28 21:18:44 +01:00
public static function version () : string
2021-08-24 20:29:26 +01:00
{
return '3.0.0' ;
}
2022-02-23 17:39:11 +00:00
public static array $activity_streams_two_context = [
'https://www.w3.org/ns/activitystreams' ,
'https://w3id.org/security/v1' ,
[ 'gs' => 'https://www.gnu.org/software/social/ns#' ],
[ 'litepub' => 'http://litepub.social/ns#' ],
[ 'chatMessage' => 'litepub:chatMessage' ],
[
'inConversation' => [
'@id' => 'gs:inConversation' ,
'@type' => '@id' ,
],
],
];
2022-04-03 21:40:32 +01:00
public function onInitializePlugin () : EventResult
2022-02-23 17:39:11 +00:00
{
Event :: handle ( 'ActivityStreamsTwoContext' , [ & self :: $activity_streams_two_context ]);
self :: $activity_streams_two_context = array_unique ( self :: $activity_streams_two_context , \SORT_REGULAR );
return Event :: next ;
}
2022-04-03 21:40:32 +01:00
public function onQueueActivitypubInbox ( ActivitypubActor $ap_actor , Actor $actor , string | AbstractObject $type ) : EventResult
2022-03-05 14:23:08 +00:00
{
// TODO: Check if Actor has authority over payload
// Store Activity
$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 ();
2022-03-23 13:18:44 +00:00
if (( $att_targets = $ap_act -> getAttentionTargets ()) !== []) {
if ( Event :: handle ( 'ActivityPubNewNotification' , [ $actor , ( $act = $ap_act -> getActivity ()), $att_targets , _m ( '{actor_id} triggered a notification via ActivityPub.' , [ '{actor_id}' => $actor -> getId ()])]) === Event :: next ) {
Event :: handle ( 'NewNotification' , [ $actor , $act , $att_targets , _m ( '{actor_id} triggered a notification via ActivityPub.' , [ '{nickname}' => $actor -> getId ()])]);
2022-03-13 18:23:19 +00:00
}
2022-03-05 14:23:08 +00:00
}
return Event :: stop ;
}
2021-08-24 20:29:26 +01:00
/**
* This code executes when GNU social creates the page routing , and we hook
2021-10-04 17:00:58 +01:00
* on this event to add our Inbox and Outbox handler for ActivityPub .
2021-08-24 20:29:26 +01:00
*
2022-03-27 16:43:59 +01:00
* @ param Router $r the router that was initialized
2021-08-24 20:29:26 +01:00
*/
2022-04-03 21:40:32 +01:00
public function onAddRoute ( Router $r ) : EventResult
2021-08-24 20:29:26 +01:00
{
2021-09-16 17:04:05 +01:00
$r -> connect (
2021-10-19 23:46:01 +01:00
'activitypub_inbox' ,
'/inbox.json' ,
2022-02-20 05:03:05 +00:00
Inbox :: class ,
2021-12-12 06:40:13 +00:00
options : [ 'format' => self :: $accept_headers [ 0 ]],
2021-09-16 17:04:05 +01:00
);
$r -> connect (
2021-10-19 23:46:01 +01:00
'activitypub_actor_inbox' ,
'/actor/{gsactor_id<\d+>}/inbox.json' ,
2021-09-16 17:04:05 +01:00
[ Inbox :: class , 'handle' ],
2021-12-12 06:40:13 +00:00
options : [ 'format' => self :: $accept_headers [ 0 ]],
2021-09-16 17:04:05 +01:00
);
2021-08-24 20:29:26 +01:00
$r -> connect (
2021-10-19 23:46:01 +01:00
'activitypub_actor_outbox' ,
'/actor/{gsactor_id<\d+>}/outbox.json' ,
2022-01-11 20:28:15 +00:00
[ Outbox :: class , 'viewOutboxByActorId' ],
2021-10-27 04:14:01 +01:00
options : [ 'accept' => self :: $accept_headers , 'format' => self :: $accept_headers [ 0 ]],
2021-08-24 20:29:26 +01:00
);
return Event :: next ;
}
2021-12-04 04:07:08 +00:00
/**
* Fill Actor -> getUrl () calls with correct URL coming from ActivityPub
*/
2022-04-03 21:40:32 +01:00
public function onStartGetActorUri ( Actor $actor , int $type , ? string & $url ) : EventResult
2021-11-27 15:06:46 +00:00
{
if (
// Is remote?
! $actor -> getIsLocal ()
// Is in ActivityPub?
2022-02-18 17:48:06 +00:00
&& ! \is_null ( $ap_actor = DB :: findOneBy ( ActivitypubActor :: class , [ 'actor_id' => $actor -> getId ()], return_null : true ))
2021-11-27 15:06:46 +00:00
// We can only provide a full URL (anything else wouldn't make sense)
&& $type === Router :: ABSOLUTE_URL
) {
2021-11-29 23:58:42 +00:00
$url = $ap_actor -> getUri ();
2021-11-27 15:06:46 +00:00
return Event :: stop ;
}
return Event :: next ;
}
2021-12-28 15:02:03 +00:00
/**
* Fill Actor -> canAdmin () for Actors that came from ActivityPub
*/
2022-04-03 21:40:32 +01:00
public function onFreeNetworkActorCanAdmin ( Actor $actor , Actor $other , bool & $canAdmin ) : EventResult
2021-12-28 15:02:03 +00:00
{
// Are both in AP?
if (
2022-02-18 17:48:06 +00:00
! \is_null ( $ap_actor = DB :: findOneBy ( ActivitypubActor :: class , [ 'actor_id' => $actor -> getId ()], return_null : true ))
&& ! \is_null ( $ap_other = DB :: findOneBy ( ActivitypubActor :: class , [ 'actor_id' => $other -> getId ()], return_null : true ))
2021-12-28 15:02:03 +00:00
) {
// Are they both in the same server?
2022-02-25 01:05:28 +00:00
$canAdmin = parse_url ( $ap_actor -> getUri (), \PHP_URL_HOST ) === parse_url ( $ap_other -> getUri (), \PHP_URL_HOST );
2021-12-28 15:02:03 +00:00
return Event :: stop ;
}
return Event :: next ;
}
2021-12-04 04:07:08 +00:00
/**
* Overload core endpoints to make resources available in ActivityStreams 2.0
*
* @ throws Exception
*/
2022-04-03 21:40:32 +01:00
public function onControllerResponseInFormat ( string $route , array $accept_header , array $vars , ? TypeResponse & $response = null ) : EventResult
2021-12-04 04:07:08 +00:00
{
2021-12-24 01:58:41 +00:00
if ( \count ( array_intersect ( self :: $accept_headers , $accept_header )) === 0 ) {
2021-12-04 04:07:08 +00:00
return Event :: next ;
}
switch ( $route ) {
2022-02-16 18:45:30 +00:00
case 'actor_view_id' :
2022-02-10 16:02:51 +00:00
case 'person_actor_view_id' :
case 'person_actor_view_nickname' :
case 'group_actor_view_id' :
case 'group_actor_view_nickname' :
case 'bot_actor_view_id' :
case 'bot_actor_view_nickname' :
2021-12-04 04:07:08 +00:00
$response = ActorResponse :: handle ( $vars [ 'actor' ]);
2022-01-11 20:28:15 +00:00
break ;
2022-02-11 00:22:22 +00:00
case 'activity_view' :
$response = ActivityResponse :: handle ( $vars [ 'activity' ]);
break ;
2021-12-04 04:07:08 +00:00
case 'note_view' :
$response = NoteResponse :: handle ( $vars [ 'note' ]);
2022-01-11 20:28:15 +00:00
break ;
case 'activitypub_actor_outbox' :
$response = new TypeResponse ( $vars [ 'type' ]);
break ;
2021-12-04 04:07:08 +00:00
default :
2022-01-11 20:28:15 +00:00
if ( Event :: handle ( 'ActivityPubActivityStreamsTwoResponse' , [ $route , $vars , & $response ]) !== Event :: stop ) {
if ( is_subclass_of ( $vars [ 'controller' ][ 0 ], OrderedCollection :: class )) {
$response = new TypeResponse ( OrderedCollectionController :: fromControllerVars ( $vars )[ 'type' ]);
2022-02-11 00:22:22 +00:00
} else {
$response = new JsonResponse ([ 'error' => 'Unknown Object cannot be represented.' ]);
2022-01-11 20:28:15 +00:00
}
2021-12-04 04:07:08 +00:00
}
}
2022-01-11 20:28:15 +00:00
return Event :: stop ;
2021-12-04 04:07:08 +00:00
}
/**
* Add ActivityStreams 2 Extensions
*/
2022-04-03 21:40:32 +01:00
public function onActivityPubValidateActivityStreamsTwoData ( string $type_name , array & $validators ) : EventResult
2021-12-04 04:07:08 +00:00
{
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
*/
2022-04-03 21:40:32 +01:00
public function onAddFreeNetworkProtocol ( array & $protocols ) : EventResult
2021-12-01 20:53:32 +00:00
{
2022-10-21 11:27:53 +01:00
if ( ! \in_array ( '\Plugin\ActivityPub\ActivityPub' , $protocols )) {
$protocols [] = '\Plugin\ActivityPub\ActivityPub' ;
}
2021-11-29 23:58:42 +00:00
return Event :: next ;
}
2022-02-21 04:53:12 +00:00
/**
* The FreeNetwork component will call this function to pull ActivityPub objects by URI
*
* @ param string $uri Query
2022-02-23 17:39:11 +00:00
*
2022-02-21 04:53:12 +00:00
* @ return bool true if imported , false otherwise
*/
public static function freeNetworkGrabRemote ( string $uri ) : bool
{
if ( Common :: isValidHttpUrl ( $uri )) {
try {
$object = self :: getObjectByUri ( $uri );
if ( ! \is_null ( $object )) {
if ( $object instanceof Type\AbstractObject ) {
2022-02-23 17:39:11 +00:00
if ( \in_array ( $object -> get ( 'type' ), array_keys ( Model\Actor :: $_as2_actor_type_to_gs_actor_type ))) {
DB :: wrapInTransaction ( fn () => Model\Actor :: fromJson ( $object ));
2022-02-21 04:53:12 +00:00
} else {
2022-02-23 17:39:11 +00:00
DB :: wrapInTransaction ( fn () => Model\Activity :: fromJson ( $object ));
2022-02-21 04:53:12 +00:00
}
}
return true ;
}
2022-02-23 17:39:11 +00:00
} catch ( Exception | Throwable ) {
2022-02-21 04:53:12 +00:00
// May be invalid input, we can safely ignore in this case
}
}
return false ;
}
2022-03-05 14:23:08 +00:00
public function onQueueActivitypubPostman (
Actor $sender ,
Activity $activity ,
string $inbox ,
array $to_actors ,
array & $retry_args ,
2022-04-03 21:40:32 +01:00
) : EventResult {
2022-03-05 14:23:08 +00:00
try {
2022-03-28 20:58:48 +01:00
$data = Model :: toType ( $activity );
2022-03-28 23:52:48 +01:00
if ( $sender -> isGroup ()) { // When the sender is a group,
if ( $activity -> getVerb () === 'subscribe' ) {
// Regular postman happens
} elseif ( $activity -> getVerb () === 'undo' && $data -> get ( 'object' ) -> get ( 'type' ) === 'Follow' ) {
// Regular postman happens
} else {
// For every other activity sent by a Group, we have to wrap it in a transient Announce activity
$data = Type :: create ( 'Announce' , [
'@context' => 'https:\/\/www.w3.org\/ns\/activitystreams' ,
'actor' => $sender -> getUri ( type : Router :: ABSOLUTE_URL ),
'object' => $data ,
]);
}
2022-03-05 14:23:08 +00:00
}
2022-03-28 20:58:48 +01:00
$res = self :: postman ( $sender , $data -> toJson (), $inbox );
2022-03-05 14:23:08 +00:00
// 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 );
$retry_args [ 'reason' ] ? ? = [];
$retry_args [ 'reason' ][] = $res_body [ 'error' ] ? ? 'An unknown error occurred.' ;
return Event :: next ;
} else {
foreach ( $to_actors as $actor ) {
if ( $actor -> isPerson ()) {
FreeNetworkActorProtocol :: protocolSucceeded (
'activitypub' ,
$actor ,
Discovery :: normalize ( $actor -> getNickname () . '@' . parse_url ( $inbox , \PHP_URL_HOST )),
);
}
}
}
return Event :: stop ;
} catch ( Exception $e ) {
Log :: error ( 'ActivityPub @ freeNetworkDistribute: ' . $e -> getMessage (), [ $e ]);
$retry_args [ 'reason' ] ? ? = [];
$retry_args [ 'reason' ][] = " Got an exception: { $e -> getMessage () } " ;
$retry_args [ 'exception' ] ? ? = [];
$retry_args [ 'exception' ][] = $e ;
return Event :: next ;
}
}
2021-12-04 04:07:08 +00:00
/**
* The FreeNetwork component will call this function to distribute this instance ' s activities
*
* @ throws ClientExceptionInterface
* @ throws RedirectionExceptionInterface
* @ throws ServerExceptionInterface
* @ throws TransportExceptionInterface
*/
2022-03-05 14:23:08 +00:00
public static function freeNetworkDistribute ( Actor $sender , Activity $activity , array $targets , ? string $reason = null ) : void
2021-11-27 15:06:46 +00:00
{
2021-11-29 23:58:42 +00:00
$to_addr = [];
2021-12-02 04:25:58 +00:00
foreach ( $targets as $actor ) {
if ( FreeNetworkActorProtocol :: canIActor ( 'activitypub' , $actor -> getId ())) {
2022-03-05 14:23:08 +00:00
// Sometimes FreeNetwork can allow us to actor even though we don't have an internal representation of
// the actor, that could for example mean that OStatus handled this actor while we were deactivated
// On next interaction this should be resolved, for now continue
2022-02-18 17:48:06 +00:00
if ( \is_null ( $ap_target = DB :: findOneBy ( ActivitypubActor :: class , [ 'actor_id' => $actor -> getId ()], return_null : true ))) {
2022-03-28 20:58:48 +01:00
Log :: info ( 'FreeNetwork wrongly told ActivityPub that it can handle actor id: ' . $actor -> getId () . ' you might want to keep an eye on it.' );
2021-12-02 04:25:58 +00:00
continue ;
}
$to_addr [ $ap_target -> getInboxSharedUri () ? ? $ap_target -> getInboxUri ()][] = $actor ;
} else {
2022-03-05 14:23:08 +00:00
continue ;
2021-11-29 23:58:42 +00:00
}
}
2022-03-05 14:23:08 +00:00
foreach ( $to_addr as $inbox => $to_actors ) {
Queue :: enqueue (
payload : [ $sender , $activity , $inbox , $to_actors ],
2022-03-13 18:23:19 +00:00
queue : 'ActivitypubPostman' ,
2022-03-05 14:23:08 +00:00
priority : false ,
);
2021-11-29 23:58:42 +00:00
}
}
/**
2021-12-04 04:07:08 +00:00
* Internal tool to sign and send activities out
*
* @ throws Exception
2021-11-29 23:58:42 +00:00
*/
2021-12-04 04:07:08 +00:00
private static function postman ( Actor $sender , string $json_activity , string $inbox , string $method = 'post' ) : ResponseInterface
2021-11-29 23:58:42 +00:00
{
2021-12-04 04:07:08 +00:00
Log :: debug ( 'ActivityPub Postman: Delivering ' . $json_activity . ' to ' . $inbox );
2021-11-29 23:58:42 +00:00
2021-12-04 04:07:08 +00:00
$headers = HTTPSignature :: sign ( $sender , $inbox , $json_activity );
2021-11-30 16:47:31 +00:00
Log :: debug ( 'ActivityPub Postman: Delivery headers were: ' . print_r ( $headers , true ));
2021-11-29 23:58:42 +00:00
2021-12-04 04:07:08 +00:00
$response = HTTPClient :: $method ( $inbox , [ 'headers' => $headers , 'body' => $json_activity ]);
2021-12-01 20:53:32 +00:00
Log :: debug ( 'ActivityPub Postman: Delivery result with status code ' . $response -> getStatusCode () . ': ' . $response -> getContent ());
2021-11-29 23:58:42 +00:00
return $response ;
2021-11-27 15:06:46 +00:00
}
2021-12-04 04:07:08 +00:00
// WebFinger Events
2021-11-01 12:16:46 +00:00
2021-10-18 13:22:02 +01:00
/**
2021-12-04 04:07:08 +00:00
* Add activity + json mimetype to WebFinger
2021-10-18 13:22:02 +01:00
*/
2022-04-03 21:40:32 +01:00
public function onEndWebFingerProfileLinks ( XML_XRD $xrd , Actor $object ) : EventResult
2021-10-04 17:00:58 +01:00
{
2021-10-18 13:22:02 +01:00
if ( $object -> isPerson ()) {
$link = new XML_XRD_Element_Link (
2021-10-27 04:14:01 +01:00
rel : 'self' ,
href : $object -> getUri ( Router :: ABSOLUTE_URL ), //Router::url('actor_view_id', ['id' => $object->getId()], Router::ABSOLUTE_URL),
type : 'application/activity+json' ,
2021-10-18 13:22:02 +01:00
);
$xrd -> links [] = clone $link ;
2021-10-04 17:00:58 +01:00
}
2021-10-27 04:14:01 +01:00
return Event :: next ;
2021-10-18 13:22:02 +01:00
}
2021-12-04 04:07:08 +00:00
/**
* When FreeNetwork component asks us to help with identifying Actors from XRDs
*/
2022-04-03 21:40:32 +01:00
public function onFreeNetworkFoundXrd ( XML_XRD $xrd , ? Actor & $actor = null ) : EventResult
2021-11-01 12:16:46 +00:00
{
2021-12-02 04:25:58 +00:00
$addr = null ;
foreach ( $xrd -> aliases as $alias ) {
if ( Discovery :: isAcct ( $alias )) {
$addr = Discovery :: normalize ( $alias );
}
}
2021-12-24 01:58:41 +00:00
if ( \is_null ( $addr )) {
2021-12-02 04:25:58 +00:00
return Event :: next ;
} else {
if ( ! FreeNetworkActorProtocol :: canIAddr ( 'activitypub' , $addr )) {
return Event :: next ;
}
}
2021-12-02 03:34:31 +00:00
try {
2021-12-02 04:25:58 +00:00
$ap_actor = ActivitypubActor :: fromXrd ( $addr , $xrd );
2021-12-24 01:58:41 +00:00
$actor = Actor :: getById ( $ap_actor -> getActorId ());
2021-12-02 04:25:58 +00:00
FreeNetworkActorProtocol :: protocolSucceeded ( 'activitypub' , $actor , $addr );
2021-12-02 03:34:31 +00:00
return Event :: stop ;
} catch ( Exception $e ) {
2021-12-04 04:07:08 +00:00
Log :: error ( 'ActivityPub Actor from URL Mention check failed: ' . $e -> getMessage ());
2021-12-02 03:34:31 +00:00
return Event :: next ;
2021-11-01 12:16:46 +00:00
}
2021-10-04 17:00:58 +01:00
}
2021-11-01 12:16:46 +00:00
2021-12-04 04:07:08 +00:00
// Discovery Events
2021-11-01 12:16:46 +00:00
/**
2021-12-04 04:07:08 +00:00
* When FreeNetwork component asks us to help with identifying Actors from URIs
2021-11-01 12:16:46 +00:00
*/
2022-04-03 21:40:32 +01:00
public function onFreeNetworkFindMentions ( string $target , ? Actor & $actor = null ) : EventResult
2021-12-04 04:07:08 +00:00
{
try {
if ( FreeNetworkActorProtocol :: canIAddr ( 'activitypub' , $addr = Discovery :: normalize ( $target ))) {
2022-02-23 17:39:11 +00:00
$ap_actor = DB :: wrapInTransaction ( fn () => ActivitypubActor :: getByAddr ( $addr ));
2022-10-21 11:27:53 +01:00
$actor = Actor :: getById ( $ap_actor -> getActorId ());
2021-12-04 04:07:08 +00:00
FreeNetworkActorProtocol :: protocolSucceeded ( 'activitypub' , $actor -> getId (), $addr );
return Event :: stop ;
} else {
return Event :: next ;
}
} catch ( Exception $e ) {
2022-02-20 13:59:06 +00:00
Log :: error ( 'ActivityPub WebFinger Mention check failed.' , [ $e ]);
2021-12-04 04:07:08 +00:00
return Event :: next ;
}
}
2021-11-01 12:16:46 +00:00
2021-12-12 06:40:13 +00:00
/**
* @ return string got from URI
*/
public static function getUriByObject ( mixed $object ) : string
{
2022-01-02 20:37:15 +00:00
switch ( $object :: class ) {
case Note :: class :
if ( $object -> getIsLocal ()) {
return $object -> getUrl ();
} else {
// Try known remote objects
2022-02-18 17:48:06 +00:00
$known_object = DB :: findOneBy ( ActivitypubObject :: class , [ 'object_type' => 'note' , 'object_id' => $object -> getId ()], return_null : true );
2022-01-02 20:37:15 +00:00
if ( $known_object instanceof ActivitypubObject ) {
return $known_object -> getObjectUri ();
} else {
throw new BugFoundException ( 'ActivityPub cannot generate an URI for a stored note.' , [ $object , $known_object ]);
}
2021-12-12 06:40:13 +00:00
}
2022-01-02 20:37:15 +00:00
break ;
case Actor :: class :
return $object -> getUri ();
break ;
case Activity :: class :
// Try known remote activities
2022-02-18 17:48:06 +00:00
$known_activity = DB :: findOneBy ( ActivitypubActivity :: class , [ 'activity_id' => $object -> getId ()], return_null : true );
if ( ! \is_null ( $known_activity )) {
2022-01-02 20:37:15 +00:00
return $known_activity -> getActivityUri ();
} else {
return Router :: url ( 'activity_view' , [ 'id' => $object -> getId ()], Router :: ABSOLUTE_URL );
}
break ;
default :
throw new InvalidArgumentException ( 'ActivityPub::getUriByObject found a limitation with: ' . var_export ( $object , true ));
2021-12-24 01:58:41 +00:00
}
2021-12-12 06:40:13 +00:00
}
2021-12-08 22:24:52 +00:00
/**
* Get a Note from ActivityPub URI , if it doesn ' t exist , attempt to fetch it
* This should only be necessary internally .
*
* @ throws ClientExceptionInterface
* @ throws RedirectionExceptionInterface
* @ throws ServerExceptionInterface
* @ throws TransportExceptionInterface
2021-12-24 01:58:41 +00:00
*
2022-02-23 17:39:11 +00:00
* @ return null | Actor | mixed | Note got from URI
2021-12-08 22:24:52 +00:00
*/
2022-03-13 18:23:19 +00:00
public static function getObjectByUri ( string $resource , bool $try_online = true ) : mixed
2021-12-08 22:24:52 +00:00
{
2022-01-02 20:37:15 +00:00
// Try known object
2022-02-18 17:48:06 +00:00
$known_object = DB :: findOneBy ( ActivitypubObject :: class , [ 'object_uri' => $resource ], return_null : true );
if ( ! \is_null ( $known_object )) {
2021-12-08 22:24:52 +00:00
return $known_object -> getObject ();
}
2022-01-02 20:37:15 +00:00
// Try known activity
2022-02-18 17:48:06 +00:00
$known_activity = DB :: findOneBy ( ActivitypubActivity :: class , [ 'activity_uri' => $resource ], return_null : true );
if ( ! \is_null ( $known_activity )) {
2021-12-08 22:24:52 +00:00
return $known_activity -> getActivity ();
}
2022-03-13 18:23:19 +00:00
// Try Actor
try {
return Explorer :: getOneFromUri ( $resource , try_online : false );
} catch ( \Exception ) {
// Ignore, this is brute forcing, it's okay not to find
}
// Is it a HTTP URL?
2021-12-08 22:24:52 +00:00
if ( Common :: isValidHttpUrl ( $resource )) {
$resource_parts = parse_url ( $resource );
2022-03-13 18:23:19 +00:00
// If it is local
2022-02-11 00:17:20 +00:00
if ( $resource_parts [ 'host' ] === Common :: config ( 'site' , 'server' )) {
2022-03-13 18:23:19 +00:00
// Try Local Note
$local_note = DB :: findOneBy ( Note :: class , [ 'url' => $resource ], return_null : true );
2022-02-23 17:39:11 +00:00
if ( ! \is_null ( $local_note )) {
2021-12-08 22:24:52 +00:00
return $local_note ;
}
2022-03-13 18:23:19 +00:00
// Try local Activity
try {
$match = Router :: match ( $resource_parts [ 'path' ]);
$local_activity = DB :: findOneBy ( Activity :: class , [ 'id' => $match [ 'id' ]], return_null : true );
if ( ! \is_null ( $local_activity )) {
return $local_activity ;
} else {
throw new InvalidArgumentException ( 'Tried to retrieve a non-existent local activity.' );
}
} catch ( \Exception ) {
// Ignore, this is brute forcing, it's okay not to find
}
2022-01-02 20:37:15 +00:00
2022-03-13 18:23:19 +00:00
throw new BugFoundException ( 'ActivityPub failed to retrieve local resource: "' . $resource . '". This is a big issue.' );
} else {
// Then it's remote
if ( ! $try_online ) {
throw new Exception ( " Remote resource { $resource } not found without online resources. " );
}
2021-12-08 22:24:52 +00:00
2022-03-13 18:23:19 +00:00
$response = HTTPClient :: get ( $resource , [ 'headers' => self :: HTTP_CLIENT_HEADERS ]);
// If it was deleted
if ( $response -> getStatusCode () == 410 ) {
//$obj = Type::create('Tombstone', ['id' => $resource]);
return null ;
} elseif ( ! HTTPClient :: statusCodeIsOkay ( $response )) { // If it is unavailable
throw new Exception ( 'Non Ok Status Code for given Object id.' );
} else {
return Model :: jsonToType ( $response -> getContent ());
}
}
2021-12-08 22:24:52 +00:00
}
2022-03-13 18:23:19 +00:00
return null ;
2021-12-08 22:24:52 +00:00
}
2021-09-06 23:47:28 +01:00
}