2018-10-03 06:15:07 +00:00
< ? php
/**
2023-01-01 14:36:24 +00:00
* @ copyright Copyright ( C ) 2010 - 2023 , the Friendica project
2020-02-09 15:18:46 +00:00
*
* @ license GNU AGPL version 3 or any later version
*
* 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 < https :// www . gnu . org / licenses />.
*
2018-10-03 06:15:07 +00:00
*/
2020-02-09 15:18:46 +00:00
2018-10-03 06:15:07 +00:00
namespace Friendica\Protocol\ActivityPub ;
2020-05-07 02:41:59 +00:00
use Friendica\Content\Text\BBCode ;
2018-10-03 06:15:07 +00:00
use Friendica\Database\DBA ;
2019-11-13 16:22:20 +00:00
use Friendica\Content\Text\HTML ;
use Friendica\Content\Text\Markdown ;
2018-10-29 21:20:46 +00:00
use Friendica\Core\Logger ;
2018-10-03 06:15:07 +00:00
use Friendica\Core\Protocol ;
2022-04-01 11:20:17 +00:00
use Friendica\Core\System ;
2022-07-27 20:59:42 +00:00
use Friendica\Core\Worker ;
2022-08-25 04:57:41 +00:00
use Friendica\Database\Database ;
2022-04-01 11:20:17 +00:00
use Friendica\DI ;
2018-10-03 06:15:07 +00:00
use Friendica\Model\Contact ;
use Friendica\Model\APContact ;
use Friendica\Model\Item ;
2021-01-16 04:14:58 +00:00
use Friendica\Model\Post ;
2018-10-03 06:15:07 +00:00
use Friendica\Model\User ;
2019-10-23 22:25:43 +00:00
use Friendica\Protocol\Activity ;
2018-10-03 06:15:07 +00:00
use Friendica\Protocol\ActivityPub ;
2022-07-27 20:59:42 +00:00
use Friendica\Util\DateTimeFormat ;
2018-11-08 16:28:29 +00:00
use Friendica\Util\HTTPSignature ;
use Friendica\Util\JsonLD ;
use Friendica\Util\LDSignature ;
2022-04-03 07:21:36 +00:00
use Friendica\Util\Network ;
2018-11-08 16:28:29 +00:00
use Friendica\Util\Strings ;
2018-10-03 06:15:07 +00:00
/**
2020-01-19 06:05:23 +00:00
* ActivityPub Receiver Protocol class
2018-10-03 09:53:12 +00:00
*
* To - Do :
2020-02-09 15:18:46 +00:00
* @ todo Undo Announce
2018-10-03 09:53:12 +00:00
*
* Check what this is meant to do :
* - Add
* - Block
* - Flag
* - Remove
* - Undo Block
2018-10-03 06:15:07 +00:00
*/
class Receiver
{
2018-10-07 13:37:05 +00:00
const PUBLIC_COLLECTION = 'as:Public' ;
const ACCOUNT_TYPES = [ 'as:Person' , 'as:Organization' , 'as:Service' , 'as:Group' , 'as:Application' ];
2022-01-23 04:40:45 +00:00
const CONTENT_TYPES = [ 'as:Note' , 'as:Article' , 'as:Video' , 'as:Image' , 'as:Event' , 'as:Audio' , 'as:Page' , 'as:Question' ];
2022-12-26 15:08:46 +00:00
const ACTIVITY_TYPES = [ 'as:Like' , 'as:Dislike' , 'as:Accept' , 'as:Reject' , 'as:TentativeAccept' , 'as:View' , 'as:Read' , 'litepub:EmojiReact' ];
2018-10-07 13:37:05 +00:00
2020-09-12 12:12:55 +00:00
const TARGET_UNKNOWN = 0 ;
const TARGET_TO = 1 ;
const TARGET_CC = 2 ;
2020-09-12 17:45:04 +00:00
const TARGET_BTO = 3 ;
const TARGET_BCC = 4 ;
const TARGET_FOLLOWER = 5 ;
2020-09-13 14:15:28 +00:00
const TARGET_ANSWER = 6 ;
2020-09-14 17:48:57 +00:00
const TARGET_GLOBAL = 7 ;
2020-09-12 12:12:55 +00:00
2022-03-12 11:17:33 +00:00
const COMPLETION_NONE = 0 ;
const COMPLETION_ANNOUCE = 1 ;
const COMPLETION_RELAY = 2 ;
const COMPLETION_MANUAL = 3 ;
const COMPLETION_AUTO = 4 ;
2018-10-03 06:15:07 +00:00
/**
2018-10-07 13:37:05 +00:00
* Checks incoming message from the inbox
2018-10-03 06:15:07 +00:00
*
2022-06-20 17:04:01 +00:00
* @ param string $body Body string
* @ param array $header Header lines
2018-10-03 06:15:07 +00:00
* @ param integer $uid User ID
2022-06-20 19:21:32 +00:00
* @ return void
2019-01-06 21:06:53 +00:00
* @ throws \Exception
2018-10-03 06:15:07 +00:00
*/
2022-06-20 17:04:01 +00:00
public static function processInbox ( string $body , array $header , int $uid )
2018-10-03 06:15:07 +00:00
{
$activity = json_decode ( $body , true );
if ( empty ( $activity )) {
2019-02-23 04:00:16 +00:00
Logger :: warning ( 'Invalid body.' );
2018-10-03 06:15:07 +00:00
return ;
}
2018-10-07 13:37:05 +00:00
$ldactivity = JsonLD :: compact ( $activity );
2022-06-20 18:59:08 +00:00
$actor = JsonLD :: fetchElement ( $ldactivity , 'as:actor' , '@id' ) ? ? '' ;
2022-07-18 03:31:00 +00:00
2022-06-20 18:59:08 +00:00
$apcontact = APContact :: getByURL ( $actor );
2018-10-07 13:37:05 +00:00
2020-11-24 22:32:52 +00:00
if ( empty ( $apcontact )) {
2021-03-10 14:40:57 +00:00
Logger :: notice ( 'Unable to retrieve AP contact for actor - message is discarded' , [ 'actor' => $actor ]);
return ;
2022-07-20 05:37:40 +00:00
} elseif ( APContact :: isRelay ( $apcontact )) {
2020-09-21 15:17:33 +00:00
self :: processRelayPost ( $ldactivity , $actor );
2020-09-14 20:58:41 +00:00
return ;
2020-11-24 22:32:52 +00:00
} else {
APContact :: unmarkForArchival ( $apcontact );
2020-09-14 20:58:41 +00:00
}
2022-08-27 08:08:58 +00:00
$sig_contact = HTTPSignature :: getKeyIdContact ( $header );
if ( APContact :: isRelay ( $sig_contact )) {
Logger :: info ( 'Message from a relay' , [ 'url' => $sig_contact [ 'url' ]]);
self :: processRelayPost ( $ldactivity , $sig_contact [ 'url' ]);
return ;
}
2022-07-18 14:03:49 +00:00
$http_signer = HTTPSignature :: getSigner ( $body , $header );
if ( $http_signer === false ) {
2022-09-04 07:39:09 +00:00
Logger :: notice ( 'Invalid HTTP signature, message will not be trusted.' , [ 'uid' => $uid , 'actor' => $actor , 'header' => $header , 'body' => $body ]);
$signer = [];
2022-07-18 14:03:49 +00:00
} elseif ( empty ( $http_signer )) {
Logger :: info ( 'Signer is a tombstone. The message will be discarded, the signer account is deleted.' );
return ;
} else {
Logger :: info ( 'Valid HTTP signature' , [ 'signer' => $http_signer ]);
2022-09-04 07:39:09 +00:00
$signer = [ $http_signer ];
2022-07-18 14:03:49 +00:00
}
2019-02-23 04:00:16 +00:00
Logger :: info ( 'Message for user ' . $uid . ' is from actor ' . $actor );
2018-10-07 13:37:05 +00:00
2022-09-04 07:39:09 +00:00
if ( $http_signer === false ) {
$trust_source = false ;
} elseif ( LDSignature :: isSigned ( $activity )) {
2018-10-03 06:15:07 +00:00
$ld_signer = LDSignature :: getSigner ( $activity );
if ( empty ( $ld_signer )) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Invalid JSON-LD signature from ' . $actor );
2020-09-12 12:12:55 +00:00
} elseif ( $ld_signer != $http_signer ) {
$signer [] = $ld_signer ;
2018-10-03 06:15:07 +00:00
}
if ( ! empty ( $ld_signer && ( $actor == $http_signer ))) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'The HTTP and the JSON-LD signature belong to ' . $ld_signer );
2018-10-03 06:15:07 +00:00
$trust_source = true ;
} elseif ( ! empty ( $ld_signer )) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'JSON-LD signature is signed by ' . $ld_signer );
2018-10-03 06:15:07 +00:00
$trust_source = true ;
} elseif ( $actor == $http_signer ) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Bad JSON-LD signature, but HTTP signer fits the actor.' );
2018-10-03 06:15:07 +00:00
$trust_source = true ;
} else {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Invalid JSON-LD signature and the HTTP signer is different.' );
2018-10-03 06:15:07 +00:00
$trust_source = false ;
}
} elseif ( $actor == $http_signer ) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Trusting post without JSON-LD signature, The actor fits the HTTP signer.' );
2018-10-03 06:15:07 +00:00
$trust_source = true ;
} else {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'No JSON-LD signature, different actor.' );
2018-10-03 06:15:07 +00:00
$trust_source = false ;
}
2022-07-21 05:16:14 +00:00
self :: processActivity ( $ldactivity , $body , $uid , $trust_source , true , $signer , $http_signer );
2022-07-18 03:31:00 +00:00
}
2020-09-14 20:58:41 +00:00
/**
* Process incoming posts from relays
*
2020-09-21 15:17:33 +00:00
* @ param array $activity
* @ param string $actor
2020-09-14 20:58:41 +00:00
* @ return void
*/
2020-09-21 15:17:33 +00:00
private static function processRelayPost ( array $activity , string $actor )
2020-09-14 20:58:41 +00:00
{
$type = JsonLD :: fetchElement ( $activity , '@type' );
if ( ! $type ) {
2022-08-27 08:08:58 +00:00
Logger :: notice ( 'Empty type' , [ 'activity' => $activity , 'actor' => $actor ]);
2020-09-14 20:58:41 +00:00
return ;
}
2022-08-27 08:08:58 +00:00
$object_type = JsonLD :: fetchElement ( $activity , 'as:object' , '@type' ) ? ? '' ;
2020-09-14 20:58:41 +00:00
$object_id = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
if ( empty ( $object_id )) {
2022-08-27 08:08:58 +00:00
Logger :: notice ( 'No object id found' , [ 'type' => $type , 'object_type' => $object_type , 'actor' => $actor , 'activity' => $activity ]);
return ;
}
$handle = ( $type == 'as:Announce' );
if ( ! $handle && in_array ( $type , [ 'as:Create' , 'as:Update' ])) {
$handle = in_array ( $object_type , self :: CONTENT_TYPES );
}
if ( ! $handle ) {
$trust_source = false ;
$object_data = self :: prepareObjectData ( $activity , 0 , false , $trust_source );
if ( ! $trust_source ) {
Logger :: notice ( 'Activity trust could not be achieved.' , [ 'type' => $type , 'object_type' => $object_type , 'object_id' => $object_id , 'actor' => $actor , 'activity' => $activity ]);
return ;
}
if ( empty ( $object_data )) {
Logger :: notice ( 'No object data found' , [ 'type' => $type , 'object_type' => $object_type , 'object_id' => $object_id , 'actor' => $actor , 'activity' => $activity ]);
return ;
}
2022-12-08 03:35:37 +00:00
2022-08-27 08:08:58 +00:00
if ( self :: routeActivities ( $object_data , $type , true )) {
Logger :: debug ( 'Handled activity' , [ 'type' => $type , 'object_type' => $object_type , 'object_id' => $object_id , 'actor' => $actor ]);
} else {
Logger :: info ( 'Unhandled activity' , [ 'type' => $type , 'object_type' => $object_type , 'object_id' => $object_id , 'actor' => $actor , 'activity' => $activity ]);
}
2020-09-15 17:45:19 +00:00
return ;
2020-09-14 20:58:41 +00:00
}
2020-09-29 05:06:37 +00:00
$contact = Contact :: getByURL ( $actor );
if ( empty ( $contact )) {
Logger :: info ( 'Relay contact not found' , [ 'actor' => $actor ]);
return ;
}
if ( ! in_array ( $contact [ 'rel' ], [ Contact :: SHARING , Contact :: FRIEND ])) {
Logger :: notice ( 'Relay is no sharer' , [ 'actor' => $actor ]);
return ;
}
2022-08-27 08:08:58 +00:00
Logger :: debug ( 'Got relayed message id' , [ 'id' => $object_id , 'actor' => $actor ]);
2020-09-14 20:58:41 +00:00
$item_id = Item :: searchByLink ( $object_id );
if ( $item_id ) {
2022-07-20 05:37:40 +00:00
Logger :: info ( 'Relayed message already exists' , [ 'id' => $object_id , 'item' => $item_id , 'actor' => $actor ]);
2020-09-14 20:58:41 +00:00
return ;
}
2022-07-21 05:16:14 +00:00
$id = Processor :: fetchMissingActivity ( $object_id , [], $actor , self :: COMPLETION_RELAY );
2020-09-22 15:48:44 +00:00
if ( empty ( $id )) {
2022-07-20 05:37:40 +00:00
Logger :: notice ( 'Relayed message had not been fetched' , [ 'id' => $object_id , 'actor' => $actor ]);
2020-09-22 15:48:44 +00:00
return ;
}
2020-09-14 20:58:41 +00:00
}
2018-10-07 20:36:15 +00:00
/**
* Fetches the object type for a given object id
*
2018-11-03 21:37:08 +00:00
* @ param array $activity
* @ param string $object_id Object ID of the the provided object
2019-01-06 21:06:53 +00:00
* @ param integer $uid User ID
2018-10-07 20:36:15 +00:00
*
2022-06-16 12:59:29 +00:00
* @ return string with object type or NULL
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-07 20:36:15 +00:00
*/
2023-02-13 21:27:11 +00:00
public static function fetchObjectType ( array $activity , string $object_id , int $uid = 0 )
2018-10-07 20:36:15 +00:00
{
2018-10-27 06:17:17 +00:00
if ( ! empty ( $activity [ 'as:object' ])) {
$object_type = JsonLD :: fetchElement ( $activity [ 'as:object' ], '@type' );
if ( ! empty ( $object_type )) {
return $object_type ;
}
2018-10-07 20:36:15 +00:00
}
2022-09-12 21:12:11 +00:00
if ( Post :: exists ([ 'uri' => $object_id , 'gravity' => [ Item :: GRAVITY_PARENT , Item :: GRAVITY_COMMENT ]])) {
2018-10-07 20:36:15 +00:00
// We just assume "note" since it doesn't make a difference for the further processing
return 'as:Note' ;
}
$profile = APContact :: getByURL ( $object_id );
if ( ! empty ( $profile [ 'type' ])) {
2020-11-24 22:32:52 +00:00
APContact :: unmarkForArchival ( $profile );
2018-10-07 20:36:15 +00:00
return 'as:' . $profile [ 'type' ];
}
2022-07-28 19:05:04 +00:00
$data = Processor :: fetchCachedActivity ( $object_id , $uid );
2018-10-07 20:36:15 +00:00
if ( ! empty ( $data )) {
$object = JsonLD :: compact ( $data );
$type = JsonLD :: fetchElement ( $object , '@type' );
if ( ! empty ( $type )) {
return $type ;
}
}
return null ;
}
2018-10-03 06:15:07 +00:00
/**
2018-10-07 18:41:45 +00:00
* Prepare the object array
2018-10-03 06:15:07 +00:00
*
2020-03-03 08:01:04 +00:00
* @ param array $activity Array with activity data
* @ param integer $uid User ID
* @ param boolean $push Message had been pushed to our system
* @ param boolean $trust_source Do we trust the source ?
2018-10-03 06:15:07 +00:00
*
2018-10-07 18:41:45 +00:00
* @ return array with object data
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 06:15:07 +00:00
*/
2022-06-16 12:59:29 +00:00
public static function prepareObjectData ( array $activity , int $uid , bool $push , bool & $trust_source ) : array
2018-10-03 06:15:07 +00:00
{
2022-07-27 17:39:00 +00:00
$id = JsonLD :: fetchElement ( $activity , '@id' );
$type = JsonLD :: fetchElement ( $activity , '@type' );
2022-07-24 19:31:31 +00:00
$object_id = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
2022-07-24 21:58:09 +00:00
2022-07-27 17:39:00 +00:00
if ( ! empty ( $object_id ) && in_array ( $type , [ 'as:Create' , 'as:Update' ])) {
$fetch_id = $object_id ;
} else {
$fetch_id = $id ;
}
if ( ! empty ( $activity [ 'as:object' ])) {
$object_type = JsonLD :: fetchElement ( $activity [ 'as:object' ], '@type' );
}
2022-12-28 14:56:12 +00:00
$fetched = false ;
2020-09-12 12:12:55 +00:00
if ( ! empty ( $id ) && ! $trust_source ) {
2022-02-13 16:42:43 +00:00
$fetch_uid = $uid ? : self :: getBestUserForActivity ( $activity );
2022-02-13 05:45:06 +00:00
2022-07-28 19:05:04 +00:00
$fetched_activity = Processor :: fetchCachedActivity ( $fetch_id , $fetch_uid );
2020-09-12 12:12:55 +00:00
if ( ! empty ( $fetched_activity )) {
2022-12-28 14:56:12 +00:00
$fetched = true ;
$object = JsonLD :: compact ( $fetched_activity );
2022-07-27 17:39:00 +00:00
$fetched_id = JsonLD :: fetchElement ( $object , '@id' );
$fetched_type = JsonLD :: fetchElement ( $object , '@type' );
if (( $fetched_id == $id ) && ! empty ( $fetched_type ) && ( $fetched_type == $type )) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Activity had been fetched successfully' , [ 'id' => $id ]);
$trust_source = true ;
2022-07-27 17:39:00 +00:00
$activity = $object ;
} elseif (( $fetched_id == $object_id ) && ! empty ( $fetched_type ) && ( $fetched_type == $object_type )) {
Logger :: info ( 'Fetched data is the object instead of the activity' , [ 'id' => $id ]);
$trust_source = true ;
unset ( $object [ '@context' ]);
$activity [ 'as:object' ] = $object ;
2020-09-12 12:12:55 +00:00
} else {
Logger :: info ( 'Activity id is not equal' , [ 'id' => $id , 'fetched' => $fetched_id ]);
}
} else {
Logger :: info ( 'Activity could not been fetched' , [ 'id' => $id ]);
}
}
2019-04-26 06:17:37 +00:00
$actor = JsonLD :: fetchElement ( $activity , 'as:actor' , '@id' );
2018-10-03 06:15:07 +00:00
if ( empty ( $actor )) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Empty actor' , [ 'activity' => $activity ]);
2018-10-03 06:15:07 +00:00
return [];
}
2018-10-07 18:41:45 +00:00
$type = JsonLD :: fetchElement ( $activity , '@type' );
2018-10-07 13:37:05 +00:00
2018-10-03 06:15:07 +00:00
// Fetch all receivers from to, cc, bto and bcc
2022-12-28 14:56:12 +00:00
$receiverdata = self :: getReceivers ( $activity , $actor , [], false , $push || $fetched );
2020-09-12 17:45:04 +00:00
$receivers = $reception_types = [];
foreach ( $receiverdata as $key => $data ) {
$receivers [ $key ] = $data [ 'uid' ];
2020-09-25 06:47:07 +00:00
$reception_types [ $data [ 'uid' ]] = $data [ 'type' ] ? ? self :: TARGET_UNKNOWN ;
2020-09-12 17:45:04 +00:00
}
2018-10-03 06:15:07 +00:00
2022-02-19 13:31:49 +00:00
$urls = self :: getReceiverURL ( $activity );
2018-10-03 06:15:07 +00:00
// When it is a delivery to a personal inbox we add that user to the receivers
if ( ! empty ( $uid )) {
2020-09-25 06:47:07 +00:00
$additional = [ $uid => $uid ];
$receivers = array_replace ( $receivers , $additional );
if ( empty ( $activity [ 'thread-completion' ]) && ( empty ( $reception_types [ $uid ]) || in_array ( $reception_types [ $uid ], [ self :: TARGET_UNKNOWN , self :: TARGET_FOLLOWER , self :: TARGET_ANSWER , self :: TARGET_GLOBAL ]))) {
2020-09-12 17:45:04 +00:00
$reception_types [ $uid ] = self :: TARGET_BCC ;
2022-02-19 13:31:49 +00:00
$owner = User :: getOwnerDataById ( $uid );
if ( ! empty ( $owner [ 'url' ])) {
$urls [ 'as:bcc' ][] = $owner [ 'url' ];
}
2020-09-12 17:45:04 +00:00
}
2018-10-03 06:15:07 +00:00
}
2022-02-13 16:42:43 +00:00
// We possibly need some user to fetch private content,
// so we fetch one out of the receivers if no uid is provided.
$fetch_uid = $uid ? : self :: getBestUserForActivity ( $activity );
2019-04-26 06:17:37 +00:00
$object_id = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
2018-10-03 06:15:07 +00:00
if ( empty ( $object_id )) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'No object found' );
2018-10-03 06:15:07 +00:00
return [];
}
2019-06-14 02:58:40 +00:00
if ( ! is_string ( $object_id )) {
Logger :: info ( 'Invalid object id' , [ 'object' => $object_id ]);
return [];
}
2022-02-13 16:42:43 +00:00
$object_type = self :: fetchObjectType ( $activity , $object_id , $fetch_uid );
2018-10-07 20:36:15 +00:00
2022-01-22 15:24:51 +00:00
// Fetch the activity on Lemmy "Announce" messages (announces of activities)
2022-01-23 04:40:45 +00:00
if (( $type == 'as:Announce' ) && in_array ( $object_type , array_merge ( self :: ACTIVITY_TYPES , [ 'as:Delete' , 'as:Undo' , 'as:Update' ]))) {
2022-08-31 05:01:22 +00:00
Logger :: debug ( 'Fetch announced activity' , [ 'object' => $object_id ]);
2022-07-28 19:05:04 +00:00
$data = Processor :: fetchCachedActivity ( $object_id , $fetch_uid );
2022-01-22 15:24:51 +00:00
if ( ! empty ( $data )) {
$type = $object_type ;
$activity = JsonLD :: compact ( $data );
// Some variables need to be refetched since the activity changed
$actor = JsonLD :: fetchElement ( $activity , 'as:actor' , '@id' );
$object_id = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
2022-02-13 16:42:43 +00:00
$object_type = self :: fetchObjectType ( $activity , $object_id , $fetch_uid );
2022-01-22 15:24:51 +00:00
}
}
2022-01-23 04:40:45 +00:00
// Any activities on account types must not be altered
2022-12-23 22:11:50 +00:00
if ( in_array ( $type , [ 'as:Flag' ])) {
$object_data = [];
$object_data [ 'id' ] = JsonLD :: fetchElement ( $activity , '@id' );
$object_data [ 'object_id' ] = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
$object_data [ 'object_ids' ] = JsonLD :: fetchElementArray ( $activity , 'as:object' , '@id' );
$object_data [ 'content' ] = JsonLD :: fetchElement ( $activity , 'as:content' , '@type' );
} elseif ( in_array ( $object_type , self :: ACCOUNT_TYPES )) {
2022-01-23 04:40:45 +00:00
$object_data = [];
$object_data [ 'id' ] = JsonLD :: fetchElement ( $activity , '@id' );
$object_data [ 'object_id' ] = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
$object_data [ 'object_actor' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:actor' , '@id' );
$object_data [ 'object_object' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:object' );
$object_data [ 'object_type' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], '@type' );
2022-07-27 17:39:00 +00:00
if ( ! $trust_source && ( $type == 'as:Delete' )) {
2022-07-24 21:58:09 +00:00
$apcontact = APContact :: getByURL ( $object_data [ 'object_id' ], true );
2022-07-27 17:39:00 +00:00
$trust_source = empty ( $apcontact ) || ( $apcontact [ 'type' ] == 'Tombstone' ) || $apcontact [ 'suspended' ];
2022-07-24 21:58:09 +00:00
}
2023-01-28 14:57:04 +00:00
} elseif ( in_array ( $type , [ 'as:Create' , 'as:Update' , 'as:Invite' ]) || strpos ( $type , '#emojiReaction' )) {
2022-04-01 11:20:17 +00:00
// Fetch the content only on activities where this matters
// We can receive "#emojiReaction" when fetching content from Hubzilla systems
2023-01-28 14:57:04 +00:00
$object_data = self :: fetchObject ( $object_id , $activity [ 'as:object' ], $trust_source , $fetch_uid );
2018-10-03 06:15:07 +00:00
if ( empty ( $object_data )) {
2021-11-03 23:19:24 +00:00
Logger :: info ( " Object data couldn't be processed " );
2018-10-03 06:15:07 +00:00
return [];
}
2020-02-27 05:01:43 +00:00
2019-04-02 21:10:49 +00:00
$object_data [ 'object_id' ] = $object_id ;
2020-03-04 06:04:27 +00:00
2019-05-18 07:00:57 +00:00
// Test if it is an answer to a mail
if ( DBA :: exists ( 'mail' , [ 'uri' => $object_data [ 'reply-to-id' ]])) {
$object_data [ 'directmessage' ] = true ;
} else {
$object_data [ 'directmessage' ] = JsonLD :: fetchElement ( $activity , 'litepub:directMessage' );
}
2023-01-28 14:57:04 +00:00
} elseif ( in_array ( $type , array_merge ( self :: ACTIVITY_TYPES , [ 'as:Announce' , 'as:Follow' ])) && in_array ( $object_type , self :: CONTENT_TYPES )) {
2018-10-03 06:15:07 +00:00
// Create a mostly empty array out of the activity data (instead of the object).
2022-01-23 04:40:45 +00:00
// This way we later don't have to check for the existence of each individual array element.
2023-02-13 15:32:14 +00:00
$object_data = self :: processObject ( $activity );
2018-10-07 13:37:05 +00:00
$object_data [ 'name' ] = $type ;
2019-04-26 06:17:37 +00:00
$object_data [ 'author' ] = JsonLD :: fetchElement ( $activity , 'as:actor' , '@id' );
2018-10-07 19:42:04 +00:00
$object_data [ 'object_id' ] = $object_id ;
2018-10-03 06:15:07 +00:00
$object_data [ 'object_type' ] = '' ; // Since we don't fetch the object, we don't know the type
2022-12-22 21:58:51 +00:00
} elseif ( in_array ( $type , [ 'as:Add' , 'as:Remove' , 'as:Move' ])) {
2019-05-26 11:20:03 +00:00
$object_data = [];
$object_data [ 'id' ] = JsonLD :: fetchElement ( $activity , '@id' );
$object_data [ 'target_id' ] = JsonLD :: fetchElement ( $activity , 'as:target' , '@id' );
$object_data [ 'object_id' ] = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
$object_data [ 'object_type' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], '@type' );
$object_data [ 'object_content' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:content' , '@type' );
2018-10-03 06:15:07 +00:00
} else {
$object_data = [];
2018-10-07 18:41:45 +00:00
$object_data [ 'id' ] = JsonLD :: fetchElement ( $activity , '@id' );
2019-04-26 06:17:37 +00:00
$object_data [ 'object_id' ] = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
$object_data [ 'object_actor' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:actor' , '@id' );
2018-10-07 18:41:45 +00:00
$object_data [ 'object_object' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:object' );
$object_data [ 'object_type' ] = JsonLD :: fetchElement ( $activity [ 'as:object' ], '@type' );
2018-10-27 06:17:17 +00:00
// An Undo is done on the object of an object, so we need that type as well
2020-05-14 04:53:56 +00:00
if (( $type == 'as:Undo' ) && ! empty ( $object_data [ 'object_object' ])) {
2022-02-13 16:42:43 +00:00
$object_data [ 'object_object_type' ] = self :: fetchObjectType ([], $object_data [ 'object_object' ], $fetch_uid );
2018-10-27 06:17:17 +00:00
}
2022-07-24 21:58:09 +00:00
2022-07-27 17:39:00 +00:00
if ( ! $trust_source && ( $type == 'as:Delete' ) && in_array ( $object_data [ 'object_type' ], array_merge ([ 'as:Tombstone' , '' ], self :: CONTENT_TYPES ))) {
2022-07-24 21:58:09 +00:00
$trust_source = Processor :: isActivityGone ( $object_data [ 'object_id' ]);
2022-07-27 17:39:00 +00:00
if ( ! $trust_source ) {
$trust_source = ! empty ( APContact :: getByURL ( $object_data [ 'object_id' ], false ));
}
2022-07-24 21:58:09 +00:00
}
2018-10-03 06:15:07 +00:00
}
2023-01-28 14:57:04 +00:00
$object_data [ 'push' ] = $push ;
2018-10-07 18:41:45 +00:00
$object_data = self :: addActivityFields ( $object_data , $activity );
2018-10-03 06:15:07 +00:00
2018-10-07 20:36:15 +00:00
if ( empty ( $object_data [ 'object_type' ])) {
$object_data [ 'object_type' ] = $object_type ;
}
2022-02-19 13:31:49 +00:00
foreach ([ 'as:to' , 'as:cc' , 'as:bto' , 'as:bcc' ] as $element ) {
2022-02-23 20:18:37 +00:00
if (( empty ( $object_data [ 'receiver_urls' ][ $element ]) || in_array ( $element , [ 'as:bto' , 'as:bcc' ])) && ! empty ( $urls [ $element ])) {
2022-02-19 13:31:49 +00:00
$object_data [ 'receiver_urls' ][ $element ] = array_unique ( array_merge ( $object_data [ 'receiver_urls' ][ $element ] ? ? [], $urls [ $element ]));
}
}
2018-10-07 15:34:51 +00:00
$object_data [ 'type' ] = $type ;
2018-10-07 17:35:43 +00:00
$object_data [ 'actor' ] = $actor ;
2019-01-10 22:51:03 +00:00
$object_data [ 'item_receiver' ] = $receivers ;
2020-09-25 06:47:07 +00:00
$object_data [ 'receiver' ] = array_replace ( $object_data [ 'receiver' ] ? ? [], $receivers );
$object_data [ 'reception_type' ] = array_replace ( $object_data [ 'reception_type' ] ? ? [], $reception_types );
2020-09-12 12:12:55 +00:00
2022-07-23 12:50:15 +00:00
// This check here interferes with Hubzilla posts where the author host differs from the host the post was created
// $author = $object_data['author'] ?? $actor;
// if (!empty($author) && !empty($object_data['id'])) {
// $author_host = parse_url($author, PHP_URL_HOST);
// $id_host = parse_url($object_data['id'], PHP_URL_HOST);
// if ($author_host == $id_host) {
// Logger::info('Valid hosts', ['type' => $type, 'host' => $id_host]);
// } else {
// Logger::notice('Differing hosts on author and id', ['type' => $type, 'author' => $author_host, 'id' => $id_host]);
// $trust_source = false;
// }
// }
2018-10-03 06:15:07 +00:00
2022-10-09 21:16:36 +00:00
$account = Contact :: selectFirstAccount ([ 'platform' ], [ 'nurl' => Strings :: normaliseLink ( $actor )]);
$platform = $account [ 'platform' ] ? ? '' ;
Logger :: info ( 'Processing' , [ 'type' => $object_data [ 'type' ], 'object_type' => $object_data [ 'object_type' ], 'id' => $object_data [ 'id' ], 'actor' => $actor , 'platform' => $platform ]);
2018-10-03 06:15:07 +00:00
return $object_data ;
}
2018-11-03 21:37:08 +00:00
/**
2018-11-04 10:51:01 +00:00
* Fetches the first user id from the receiver array
2018-11-03 21:37:08 +00:00
*
* @ param array $receivers Array with receivers
* @ return integer user id ;
*/
2022-06-16 12:59:29 +00:00
public static function getFirstUserFromReceivers ( array $receivers ) : int
2018-11-03 21:37:08 +00:00
{
foreach ( $receivers as $receiver ) {
if ( ! empty ( $receiver )) {
return $receiver ;
}
}
return 0 ;
}
2018-10-03 06:15:07 +00:00
/**
2018-10-07 17:17:06 +00:00
* Processes the activity object
2018-10-03 06:15:07 +00:00
*
2022-06-25 03:48:49 +00:00
* @ param array $activity Array with activity data
* @ param string $body The unprocessed body
* @ param int | null $uid User ID
* @ param boolean $trust_source Do we trust the source ?
* @ param boolean $push Message had been pushed to our system
* @ param array $signer The signer of the post
2022-08-03 03:38:03 +00:00
*
* @ return bool
*
2022-06-25 03:48:49 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 06:15:07 +00:00
*/
2022-08-03 03:38:03 +00:00
public static function processActivity ( array $activity , string $body = '' , int $uid = null , bool $trust_source = false , bool $push = false , array $signer = [], string $http_signer = '' , int $completion = Receiver :: COMPLETION_AUTO ) : bool
2018-10-03 06:15:07 +00:00
{
2018-10-07 18:41:45 +00:00
$type = JsonLD :: fetchElement ( $activity , '@type' );
2018-10-07 13:37:05 +00:00
if ( ! $type ) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Empty type' , [ 'activity' => $activity ]);
2022-08-03 03:38:03 +00:00
return true ;
2018-10-03 06:15:07 +00:00
}
2023-02-20 06:41:28 +00:00
if ( ! DI :: config () -> get ( 'system' , 'process_view' ) && ( $type == 'as:View' )) {
2022-11-17 20:01:32 +00:00
Logger :: info ( 'View activities are ignored.' , [ 'signer' => $signer , 'http_signer' => $http_signer ]);
return true ;
}
2019-04-26 06:17:37 +00:00
if ( ! JsonLD :: fetchElement ( $activity , 'as:object' , '@id' )) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Empty object' , [ 'activity' => $activity ]);
2022-08-03 03:38:03 +00:00
return true ;
2018-10-03 06:15:07 +00:00
}
2020-09-12 12:12:55 +00:00
$actor = JsonLD :: fetchElement ( $activity , 'as:actor' , '@id' );
if ( empty ( $actor )) {
Logger :: info ( 'Empty actor' , [ 'activity' => $activity ]);
2022-08-03 03:38:03 +00:00
return true ;
2018-10-03 06:15:07 +00:00
}
2020-09-12 12:12:55 +00:00
if ( is_array ( $activity [ 'as:object' ])) {
2019-04-26 06:17:37 +00:00
$attributed_to = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:attributedTo' , '@id' );
2020-09-12 12:12:55 +00:00
} else {
$attributed_to = '' ;
}
// Test the provided signatures against the actor and "attributedTo"
if ( $trust_source ) {
if ( ! empty ( $attributed_to ) && ! empty ( $actor )) {
$trust_source = ( in_array ( $actor , $signer ) && in_array ( $attributed_to , $signer ));
} else {
$trust_source = in_array ( $actor , $signer );
2018-11-20 20:40:47 +00:00
}
}
2018-10-03 06:15:07 +00:00
// $trust_source is called by reference and is set to true if the content was retrieved successfully
2020-02-27 05:01:43 +00:00
$object_data = self :: prepareObjectData ( $activity , $uid , $push , $trust_source );
2018-10-03 06:15:07 +00:00
if ( empty ( $object_data )) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'No object data found' , [ 'activity' => $activity ]);
2022-08-03 03:38:03 +00:00
return true ;
2018-10-03 06:15:07 +00:00
}
2022-01-22 15:24:51 +00:00
// Lemmy is announcing activities.
// We are changing the announces into regular activities.
2022-01-23 04:40:45 +00:00
if (( $type == 'as:Announce' ) && in_array ( $object_data [ 'type' ] ? ? '' , array_merge ( self :: ACTIVITY_TYPES , [ 'as:Delete' , 'as:Undo' , 'as:Update' ]))) {
2022-08-31 05:01:22 +00:00
Logger :: debug ( 'Change type of announce to activity' , [ 'type' => $object_data [ 'type' ]]);
2022-01-22 15:24:51 +00:00
$type = $object_data [ 'type' ];
}
2020-03-04 06:04:27 +00:00
if ( ! empty ( $body ) && empty ( $object_data [ 'raw' ])) {
2020-02-28 09:21:40 +00:00
$object_data [ 'raw' ] = $body ;
2019-01-19 16:44:15 +00:00
}
2018-10-11 20:08:04 +00:00
2018-10-09 05:04:24 +00:00
// Internal flag for thread completion. See Processor.php
if ( ! empty ( $activity [ 'thread-completion' ])) {
$object_data [ 'thread-completion' ] = $activity [ 'thread-completion' ];
}
2022-07-21 12:42:26 +00:00
2022-03-12 11:17:33 +00:00
if ( ! empty ( $activity [ 'completion-mode' ])) {
$object_data [ 'completion-mode' ] = $activity [ 'completion-mode' ];
}
2022-02-12 13:05:56 +00:00
if ( ! empty ( $activity [ 'thread-children-type' ])) {
$object_data [ 'thread-children-type' ] = $activity [ 'thread-children-type' ];
}
2018-10-09 05:04:24 +00:00
2020-09-21 12:31:20 +00:00
// Internal flag for posts that arrived via relay
if ( ! empty ( $activity [ 'from-relay' ])) {
$object_data [ 'from-relay' ] = $activity [ 'from-relay' ];
}
2021-07-09 19:30:41 +00:00
2022-07-21 05:16:14 +00:00
if ( $type == 'as:Announce' ) {
$object_data [ 'object_activity' ] = $activity ;
}
2022-08-06 17:06:55 +00:00
if (( $type == 'as:Create' ) && $trust_source ) {
if ( self :: hasArrived ( $object_data [ 'object_id' ])) {
Logger :: info ( 'The activity already arrived.' , [ 'id' => $object_data [ 'object_id' ]]);
return true ;
}
self :: addArrivedId ( $object_data [ 'object_id' ]);
2022-08-03 03:38:03 +00:00
2022-08-06 17:06:55 +00:00
if ( Queue :: exists ( $object_data [ 'object_id' ], $type )) {
Logger :: info ( 'The activity is already added.' , [ 'id' => $object_data [ 'object_id' ]]);
return true ;
}
}
2022-08-07 19:24:50 +00:00
2023-01-28 14:57:04 +00:00
$decouple = DI :: config () -> get ( 'system' , 'decoupled_receiver' ) && ! in_array ( $completion , [ self :: COMPLETION_MANUAL , self :: COMPLETION_ANNOUCE ]);
if ( $decouple && ( $trust_source || DI :: config () -> get ( 'debug' , 'ap_inbox_store_untrusted' ))) {
2022-07-24 19:31:31 +00:00
$object_data = Queue :: add ( $object_data , $type , $uid , $http_signer , $push , $trust_source );
}
2022-07-24 14:26:06 +00:00
if ( ! $trust_source ) {
Logger :: info ( 'Activity trust could not be achieved.' , [ 'id' => $object_data [ 'object_id' ], 'type' => $type , 'signer' => $signer , 'actor' => $actor , 'attributedTo' => $attributed_to ]);
2022-08-03 03:38:03 +00:00
return true ;
2022-07-24 14:26:06 +00:00
}
2022-07-18 14:03:49 +00:00
2023-01-28 14:57:04 +00:00
if ( ! empty ( $object_data [ 'entry-id' ]) && $decouple && ( $push || ( $completion == self :: COMPLETION_RELAY ))) {
2022-08-07 19:24:50 +00:00
if ( Queue :: isProcessable ( $object_data [ 'entry-id' ])) {
// We delay by 5 seconds to allow to accumulate all receivers
$delayed = date ( DateTimeFormat :: MYSQL , time () + 5 );
Logger :: debug ( 'Initiate processing' , [ 'id' => $object_data [ 'entry-id' ], 'uri' => $object_data [ 'object_id' ]]);
2022-10-17 05:49:55 +00:00
$wid = Worker :: add ([ 'priority' => Worker :: PRIORITY_HIGH , 'delayed' => $delayed ], 'ProcessQueue' , $object_data [ 'entry-id' ]);
2022-08-07 19:24:50 +00:00
Queue :: setWorkerId ( $object_data [ 'entry-id' ], $wid );
} else {
Logger :: debug ( 'Other queue entries need to be processed first.' , [ 'id' => $object_data [ 'entry-id' ]]);
}
2022-08-03 03:38:03 +00:00
return false ;
2022-07-27 20:59:42 +00:00
}
2022-07-24 09:26:52 +00:00
if ( ! empty ( $activity [ 'recursion-depth' ])) {
$object_data [ 'recursion-depth' ] = $activity [ 'recursion-depth' ];
}
2023-01-28 14:57:04 +00:00
if ( ! self :: routeActivities ( $object_data , $type , $push , true , $uid )) {
2022-07-21 05:16:14 +00:00
self :: storeUnhandledActivity ( true , $type , $object_data , $activity , $body , $uid , $trust_source , $push , $signer );
2022-07-21 12:42:26 +00:00
Queue :: remove ( $object_data );
2022-07-21 05:16:14 +00:00
}
2022-08-03 03:38:03 +00:00
return true ;
2022-07-21 05:16:14 +00:00
}
2022-07-23 12:50:15 +00:00
/**
* Route activities
*
2022-08-03 04:51:57 +00:00
* @ param array $object_data
* @ param string $type
* @ param bool $push
* @ param bool $fetch_parents
2023-01-28 14:57:04 +00:00
* @ param int $uid
2022-07-23 12:50:15 +00:00
*
* @ return boolean Could the activity be routed ?
*/
2023-01-28 14:57:04 +00:00
public static function routeActivities ( array $object_data , string $type , bool $push , bool $fetch_parents = true , int $uid = 0 ) : bool
2022-07-21 05:16:14 +00:00
{
2018-10-07 13:37:05 +00:00
switch ( $type ) {
case 'as:Create' :
2019-04-02 21:10:49 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-08-03 04:51:57 +00:00
$item = ActivityPub\Processor :: createItem ( $object_data , $fetch_parents );
2020-07-20 04:37:43 +00:00
ActivityPub\Processor :: postItem ( $object_data , $item );
2022-04-01 11:20:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'pt:CacheFile' ])) {
// Unhandled Peertube activity
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-12-22 21:58:51 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
ActivityPub\Processor :: updatePerson ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2022-04-01 11:20:17 +00:00
}
break ;
case 'as:Invite' :
if ( in_array ( $object_data [ 'object_type' ], [ 'as:Event' ])) {
2022-08-03 04:51:57 +00:00
$item = ActivityPub\Processor :: createItem ( $object_data , $fetch_parents );
2022-04-01 11:20:17 +00:00
ActivityPub\Processor :: postItem ( $object_data , $item );
} else {
2022-07-21 05:16:14 +00:00
return false ;
2019-04-02 21:10:49 +00:00
}
break ;
2019-05-26 11:20:03 +00:00
case 'as:Add' :
if ( $object_data [ 'object_type' ] == 'as:tag' ) {
ActivityPub\Processor :: addTag ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-04-04 23:07:44 +00:00
ActivityPub\Processor :: addToFeaturedCollection ( $object_data );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2019-05-26 11:20:03 +00:00
}
break ;
2018-10-07 13:37:05 +00:00
case 'as:Announce' :
2018-10-27 06:17:17 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2023-01-28 14:57:04 +00:00
if ( ! Item :: searchByLink ( $object_data [ 'object_id' ], $uid )) {
if ( ActivityPub\Processor :: fetchMissingActivity ( $object_data [ 'object_id' ], [], $object_data [ 'actor' ], self :: COMPLETION_ANNOUCE , $uid )) {
Logger :: debug ( 'Created announced id' , [ 'uid' => $uid , 'id' => $object_data [ 'object_id' ]]);
Queue :: remove ( $object_data );
} else {
Logger :: debug ( 'Announced id was not created' , [ 'uid' => $uid , 'id' => $object_data [ 'object_id' ]]);
2022-12-22 21:58:51 +00:00
Queue :: remove ( $object_data );
return true ;
2022-08-07 19:24:50 +00:00
}
} else {
2023-01-28 14:57:04 +00:00
Logger :: info ( 'Announced id already exists' , [ 'uid' => $uid , 'id' => $object_data [ 'object_id' ]]);
2022-08-07 19:24:50 +00:00
Queue :: remove ( $object_data );
2020-09-20 04:49:48 +00:00
}
2023-01-28 14:57:04 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: ANNOUNCE );
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-27 06:17:17 +00:00
}
2018-10-03 06:15:07 +00:00
break ;
2018-10-07 13:37:05 +00:00
case 'as:Like' :
2018-10-27 06:17:17 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: LIKE );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-27 06:17:17 +00:00
}
2018-10-03 06:15:07 +00:00
break ;
2018-10-07 13:37:05 +00:00
case 'as:Dislike' :
2018-10-27 06:17:17 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: DISLIKE );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-27 06:17:17 +00:00
}
break ;
case 'as:TentativeAccept' :
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: ATTENDMAYBE );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-27 06:17:17 +00:00
}
2018-10-03 06:15:07 +00:00
break ;
2018-10-07 13:37:05 +00:00
case 'as:Update' :
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: updateItem ( $object_data );
2018-10-07 13:37:05 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
2019-01-07 17:09:10 +00:00
ActivityPub\Processor :: updatePerson ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'pt:CacheFile' ])) {
// Unhandled Peertube activity
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
break ;
2018-10-07 13:37:05 +00:00
case 'as:Delete' :
2022-04-01 11:20:17 +00:00
if ( in_array ( $object_data [ 'object_type' ], array_merge ([ 'as:Tombstone' ], self :: CONTENT_TYPES ))) {
2019-01-07 17:09:10 +00:00
ActivityPub\Processor :: deleteItem ( $object_data );
2018-10-07 13:37:05 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
2019-01-07 17:09:10 +00:00
ActivityPub\Processor :: deletePerson ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif ( $object_data [ 'object_type' ] == '' ) {
// The object type couldn't be determined. Most likely we don't have it here. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2022-04-01 11:20:17 +00:00
}
break ;
2022-12-22 21:58:51 +00:00
case 'as:Move' :
if ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
ActivityPub\Processor :: movePerson ( $object_data );
} else {
return false ;
}
break ;
2023-01-01 14:36:24 +00:00
2022-04-01 11:20:17 +00:00
case 'as:Block' :
if ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
2022-04-05 20:06:04 +00:00
ActivityPub\Processor :: blockAccount ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2022-04-01 11:20:17 +00:00
}
break ;
2022-12-23 22:11:50 +00:00
case 'as:Flag' :
if ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
ActivityPub\Processor :: ReportAccount ( $object_data );
} else {
return false ;
}
break ;
2023-01-01 14:36:24 +00:00
2022-04-01 11:20:17 +00:00
case 'as:Remove' :
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 12:42:26 +00:00
ActivityPub\Processor :: removeFromFeaturedCollection ( $object_data );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
break ;
2018-10-07 13:37:05 +00:00
case 'as:Follow' :
2018-10-27 06:17:17 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: ACCOUNT_TYPES )) {
ActivityPub\Processor :: followUser ( $object_data );
2019-01-30 16:30:01 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
$object_data [ 'reply-to-id' ] = $object_data [ 'object_id' ];
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: FOLLOW );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-27 06:17:17 +00:00
}
2018-10-03 06:15:07 +00:00
break ;
2018-10-07 13:37:05 +00:00
case 'as:Accept' :
if ( $object_data [ 'object_type' ] == 'as:Follow' ) {
2022-09-28 16:32:17 +00:00
if ( ! empty ( $object_data [ 'object_actor' ])) {
ActivityPub\Processor :: acceptFollowUser ( $object_data );
} else {
2022-09-28 16:45:18 +00:00
Logger :: notice ( 'Unhandled "accept follow" message.' , [ 'object_data' => $object_data ]);
2022-09-28 16:32:17 +00:00
}
2018-10-27 06:17:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: ATTEND );
2022-12-22 21:58:51 +00:00
} elseif ( ! empty ( $object_data [ 'object_id' ]) && empty ( $object_data [ 'object_actor' ]) && empty ( $object_data [ 'object_type' ])) {
// Follow acceptances from gup.pe only contain the object id
ActivityPub\Processor :: acceptFollowUser ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
break ;
2018-10-07 13:37:05 +00:00
case 'as:Reject' :
if ( $object_data [ 'object_type' ] == 'as:Follow' ) {
2018-10-03 09:15:38 +00:00
ActivityPub\Processor :: rejectFollowUser ( $object_data );
2018-10-27 06:17:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: ATTENDNO );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
break ;
2018-10-07 13:37:05 +00:00
case 'as:Undo' :
2018-10-27 06:17:17 +00:00
if (( $object_data [ 'object_type' ] == 'as:Follow' ) &&
in_array ( $object_data [ 'object_object_type' ], self :: ACCOUNT_TYPES )) {
2018-10-03 09:15:38 +00:00
ActivityPub\Processor :: undoFollowUser ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif (( $object_data [ 'object_type' ] == 'as:Follow' ) &&
in_array ( $object_data [ 'object_object_type' ], self :: CONTENT_TYPES )) {
ActivityPub\Processor :: undoActivity ( $object_data );
2018-10-27 06:17:17 +00:00
} elseif (( $object_data [ 'object_type' ] == 'as:Accept' ) &&
in_array ( $object_data [ 'object_object_type' ], self :: ACCOUNT_TYPES )) {
ActivityPub\Processor :: rejectFollowUser ( $object_data );
2022-04-05 19:14:29 +00:00
} elseif (( $object_data [ 'object_type' ] == 'as:Block' ) &&
in_array ( $object_data [ 'object_object_type' ], self :: ACCOUNT_TYPES )) {
2022-04-05 20:06:04 +00:00
ActivityPub\Processor :: unblockAccount ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], array_merge ( self :: ACTIVITY_TYPES , [ 'as:Announce' , 'as:Create' , '' ])) &&
2022-04-03 07:45:15 +00:00
empty ( $object_data [ 'object_object_type' ])) {
2022-04-01 11:20:17 +00:00
// We cannot detect the target object. So we can ignore it.
2022-07-21 11:47:23 +00:00
Queue :: remove ( $object_data );
2022-10-03 20:01:28 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], array_merge ( self :: ACTIVITY_TYPES , [ 'as:Announce' ])) &&
in_array ( $object_data [ 'object_object_type' ], array_merge ([ 'as:Tombstone' ], self :: CONTENT_TYPES ))) {
ActivityPub\Processor :: undoActivity ( $object_data );
2022-04-01 11:20:17 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Create' ]) &&
in_array ( $object_data [ 'object_object_type' ], [ 'pt:CacheFile' ])) {
// Unhandled Peertube activity
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-12-22 21:58:51 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Delete' ])) {
// We cannot undo deletions, so we just ignore this
Queue :: remove ( $object_data );
2022-12-26 08:41:40 +00:00
} elseif ( in_array ( $object_data [ 'object_object_type' ], [ 'as:Tombstone' ])) {
// The object is a tombstone, we ignore any actions on it.
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
break ;
2022-04-01 11:20:17 +00:00
case 'as:View' :
2022-04-05 07:48:38 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: VIEW );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2022-04-01 11:20:17 +00:00
}
break ;
2022-12-26 08:41:40 +00:00
case 'as:Read' :
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
ActivityPub\Processor :: createActivity ( $object_data , Activity :: READ );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-12-26 08:41:40 +00:00
Queue :: remove ( $object_data );
} else {
return false ;
}
break ;
2022-04-01 11:20:17 +00:00
case 'litepub:EmojiReact' :
2022-04-04 16:03:53 +00:00
if ( in_array ( $object_data [ 'object_type' ], self :: CONTENT_TYPES )) {
2022-07-21 05:16:14 +00:00
ActivityPub\Processor :: createActivity ( $object_data , Activity :: EMOJIREACT );
2022-12-26 15:08:46 +00:00
} elseif ( in_array ( $object_data [ 'object_type' ], [ 'as:Tombstone' , '' ])) {
// We don't have the object here or it is deleted. We ignore this activity.
2022-07-21 05:16:14 +00:00
Queue :: remove ( $object_data );
2022-04-01 11:20:17 +00:00
} else {
2022-07-21 05:16:14 +00:00
return false ;
2022-04-01 11:20:17 +00:00
}
break ;
2022-07-21 12:42:26 +00:00
2018-10-03 06:15:07 +00:00
default :
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Unknown activity: ' . $type . ' ' . $object_data [ 'object_type' ]);
2022-07-21 05:16:14 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
2022-07-21 05:16:14 +00:00
return true ;
2018-10-03 06:15:07 +00:00
}
2022-04-01 11:20:17 +00:00
/**
2022-04-02 21:04:44 +00:00
* Stores unhandled or unknown Activities as a file
2022-04-01 11:20:17 +00:00
*
* @ param boolean $unknown " true " if the activity is unknown , " false " if it is unhandled
* @ param string $type Activity type
* @ param array $object_data Preprocessed array that is generated out of the received activity
* @ param array $activity Array with activity data
* @ param string $body The unprocessed body
* @ param integer $uid User ID
* @ param boolean $trust_source Do we trust the source ?
* @ param boolean $push Message had been pushed to our system
* @ param array $signer The signer of the post
* @ return void
*/
private static function storeUnhandledActivity ( bool $unknown , string $type , array $object_data , array $activity , string $body = '' , int $uid = null , bool $trust_source = false , bool $push = false , array $signer = [])
{
2022-07-21 12:42:26 +00:00
if ( ! DI :: config () -> get ( 'debug' , 'ap_log_unknown' )) {
return ;
}
2022-04-04 16:03:53 +00:00
$file = ( $unknown ? 'unknown-' : 'unhandled-' ) . str_replace ( ':' , '-' , $type ) . '-' ;
2022-07-21 12:42:26 +00:00
2022-04-04 16:03:53 +00:00
if ( ! empty ( $object_data [ 'object_type' ])) {
$file .= str_replace ( ':' , '-' , $object_data [ 'object_type' ]) . '-' ;
}
if ( ! empty ( $object_data [ 'object_object_type' ])) {
$file .= str_replace ( ':' , '-' , $object_data [ 'object_object_type' ]) . '-' ;
}
$tempfile = tempnam ( System :: getTempPath (), $file );
2022-04-09 11:58:01 +00:00
file_put_contents ( $tempfile , json_encode ([ 'activity' => $activity , 'body' => $body , 'uid' => $uid , 'trust_source' => $trust_source , 'push' => $push , 'signer' => $signer , 'object_data' => $object_data ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT ));
2022-12-22 21:58:51 +00:00
Logger :: notice ( 'Unknown activity stored' , [ 'type' => $type , 'object_type' => $object_data [ 'object_type' ], 'object_object_type' => $object_data [ 'object_object_type' ] ? ? '' , 'file' => $tempfile ]);
2022-04-01 11:20:17 +00:00
}
2022-02-13 05:45:06 +00:00
/**
* Fetch a user id from an activity array
*
* @ param array $activity
* @ param string $actor
*
* @ return int user id
*/
2022-06-16 12:59:29 +00:00
public static function getBestUserForActivity ( array $activity ) : int
2022-02-13 05:45:06 +00:00
{
$uid = 0 ;
2022-02-13 16:42:43 +00:00
$actor = JsonLD :: fetchElement ( $activity , 'as:actor' , '@id' ) ? ? '' ;
2022-12-28 14:56:12 +00:00
$receivers = self :: getReceivers ( $activity , $actor , [], false , false );
2022-02-13 05:45:06 +00:00
foreach ( $receivers as $receiver ) {
if ( $receiver [ 'type' ] == self :: TARGET_GLOBAL ) {
return 0 ;
}
if ( empty ( $uid ) || ( $receiver [ 'type' ] == self :: TARGET_TO )) {
$uid = $receiver [ 'uid' ];
}
}
2022-02-16 22:56:55 +00:00
// When we haven't found any user yet, we just chose a user who most likely could have access to the content
if ( empty ( $uid )) {
$contact = Contact :: selectFirst ([ 'uid' ], [ 'nurl' => Strings :: normaliseLink ( $actor ), 'rel' => [ Contact :: SHARING , Contact :: FRIEND ]]);
if ( ! empty ( $contact [ 'uid' ])) {
$uid = $contact [ 'uid' ];
}
}
2022-02-13 05:45:06 +00:00
return $uid ;
}
2022-06-16 12:59:29 +00:00
// @TODO Missing documentation
public static function getReceiverURL ( array $activity ) : array
2022-02-19 13:31:49 +00:00
{
$urls = [];
foreach ([ 'as:to' , 'as:cc' , 'as:bto' , 'as:bcc' ] as $element ) {
$receiver_list = JsonLD :: fetchElementArray ( $activity , $element , '@id' );
if ( empty ( $receiver_list )) {
continue ;
}
foreach ( $receiver_list as $receiver ) {
2023-01-27 05:55:45 +00:00
if ( $receiver == 'Public' ) {
2023-01-28 14:57:04 +00:00
Logger :: warning ( 'Not compacted public collection found' , [ 'activity' => $activity , 'callstack' => System :: callstack ( 20 )]);
2023-01-27 05:55:45 +00:00
$receiver = ActivityPub :: PUBLIC_COLLECTION ;
}
2022-02-19 13:31:49 +00:00
if ( $receiver == self :: PUBLIC_COLLECTION ) {
$receiver = ActivityPub :: PUBLIC_COLLECTION ;
}
$urls [ $element ][] = $receiver ;
}
}
return $urls ;
}
2018-10-03 06:15:07 +00:00
/**
2018-10-07 13:37:05 +00:00
* Fetch the receiver list from an activity array
2018-10-03 06:15:07 +00:00
*
2020-03-02 07:57:23 +00:00
* @ param array $activity
2022-12-28 14:56:12 +00:00
* @ param string $actor
* @ param array $tags
* @ param bool $fetch_unlisted
* @ param bool $push
2018-10-03 06:15:07 +00:00
*
2018-10-07 13:37:05 +00:00
* @ return array with receivers ( user id )
2019-01-06 21:06:53 +00:00
* @ throws \Exception
2018-10-03 06:15:07 +00:00
*/
2022-12-28 14:56:12 +00:00
private static function getReceivers ( array $activity , string $actor , array $tags , bool $fetch_unlisted , bool $push ) : array
2018-10-03 06:15:07 +00:00
{
2022-06-17 07:51:11 +00:00
$reply = $receivers = $profile = [];
2018-10-03 06:15:07 +00:00
// When it is an answer, we inherite the receivers from the parent
2019-04-26 06:17:37 +00:00
$replyto = JsonLD :: fetchElement ( $activity , 'as:inReplyTo' , '@id' );
2018-10-03 06:15:07 +00:00
if ( ! empty ( $replyto )) {
2020-09-13 14:15:28 +00:00
$reply = [ $replyto ];
2020-02-02 19:59:14 +00:00
// Fix possibly wrong item URI (could be an answer to a plink uri)
$fixedReplyTo = Item :: getURIByLink ( $replyto );
2020-09-13 14:15:28 +00:00
if ( ! empty ( $fixedReplyTo )) {
$reply [] = $fixedReplyTo ;
}
}
// Fetch all posts that refer to the object id
$object_id = JsonLD :: fetchElement ( $activity , 'as:object' , '@id' );
if ( ! empty ( $object_id )) {
$reply [] = $object_id ;
}
2020-02-02 19:59:14 +00:00
2020-09-13 14:15:28 +00:00
if ( ! empty ( $reply )) {
2022-07-29 21:28:22 +00:00
$parents = Post :: select ([ 'uid' ], DBA :: mergeConditions ([ 'uri' => $reply ], [ " `uid` != ? " , 0 ]));
2021-01-16 04:14:58 +00:00
while ( $parent = Post :: fetch ( $parents )) {
2020-09-25 06:47:07 +00:00
$receivers [ $parent [ 'uid' ]] = [ 'uid' => $parent [ 'uid' ], 'type' => self :: TARGET_ANSWER ];
2018-10-03 06:15:07 +00:00
}
2021-01-16 04:14:58 +00:00
DBA :: close ( $parents );
2018-10-03 06:15:07 +00:00
}
if ( ! empty ( $actor )) {
2021-06-06 19:28:47 +00:00
$profile = APContact :: getByURL ( $actor );
2019-10-16 12:35:14 +00:00
$followers = $profile [ 'followers' ] ? ? '' ;
2021-06-07 10:21:48 +00:00
$is_forum = ( $actor [ 'type' ] ? ? '' ) == 'Group' ;
2022-12-28 14:56:12 +00:00
if ( $push ) {
Contact :: updateByUrlIfNeeded ( $actor );
}
2021-06-06 19:28:47 +00:00
Logger :: info ( 'Got actor and followers' , [ 'actor' => $actor , 'followers' => $followers ]);
2018-10-03 06:15:07 +00:00
} else {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Empty actor' , [ 'activity' => $activity ]);
2018-10-03 06:15:07 +00:00
$followers = '' ;
2021-06-06 19:28:47 +00:00
$is_forum = false ;
2018-10-03 06:15:07 +00:00
}
2020-09-25 06:47:07 +00:00
// We have to prevent false follower assumptions upon thread completions
$follower_target = empty ( $activity [ 'thread-completion' ]) ? self :: TARGET_FOLLOWER : self :: TARGET_UNKNOWN ;
2018-10-07 13:37:05 +00:00
foreach ([ 'as:to' , 'as:cc' , 'as:bto' , 'as:bcc' ] as $element ) {
2019-04-26 06:17:37 +00:00
$receiver_list = JsonLD :: fetchElementArray ( $activity , $element , '@id' );
2018-10-07 13:37:05 +00:00
if ( empty ( $receiver_list )) {
2018-10-03 06:15:07 +00:00
continue ;
}
2018-10-07 13:37:05 +00:00
foreach ( $receiver_list as $receiver ) {
if ( $receiver == self :: PUBLIC_COLLECTION ) {
2020-09-25 06:47:07 +00:00
$receivers [ 0 ] = [ 'uid' => 0 , 'type' => self :: TARGET_GLOBAL ];
2018-10-03 06:15:07 +00:00
}
2021-07-09 19:30:41 +00:00
// Add receiver "-1" for unlisted posts
2020-03-02 07:57:23 +00:00
if ( $fetch_unlisted && ( $receiver == self :: PUBLIC_COLLECTION ) && ( $element == 'as:cc' )) {
2020-09-25 06:47:07 +00:00
$receivers [ - 1 ] = [ 'uid' => - 1 , 'type' => self :: TARGET_GLOBAL ];
2020-03-02 07:57:23 +00:00
}
2020-09-09 16:55:14 +00:00
// Fetch the receivers for the public and the followers collection
2021-06-06 19:28:47 +00:00
if ((( $receiver == $followers ) || (( $receiver == self :: PUBLIC_COLLECTION ) && ! $is_forum )) && ! empty ( $actor )) {
2022-05-03 08:19:35 +00:00
$receivers = self :: getReceiverForActor ( $actor , $tags , $receivers , $follower_target , $profile );
2018-10-03 06:15:07 +00:00
continue ;
}
2018-11-03 21:37:08 +00:00
// Fetching all directly addressed receivers
2018-11-08 16:28:29 +00:00
$condition = [ 'self' => true , 'nurl' => Strings :: normaliseLink ( $receiver )];
2018-11-03 21:37:08 +00:00
$contact = DBA :: selectFirst ( 'contact' , [ 'uid' , 'contact-type' ], $condition );
2018-10-03 06:15:07 +00:00
if ( ! DBA :: isResult ( $contact )) {
continue ;
}
2018-11-03 21:37:08 +00:00
// Check if the potential receiver is following the actor
// Exception: The receiver is targetted via "to" or this is a comment
2019-01-06 22:08:35 +00:00
if ((( $element != 'as:to' ) && empty ( $replyto )) || ( $contact [ 'contact-type' ] == Contact :: TYPE_COMMUNITY )) {
2019-07-01 18:00:55 +00:00
$networks = Protocol :: FEDERATED ;
2018-11-08 16:28:29 +00:00
$condition = [ 'nurl' => Strings :: normaliseLink ( $actor ), 'rel' => [ Contact :: SHARING , Contact :: FRIEND ],
2018-11-03 21:37:08 +00:00
'network' => $networks , 'archive' => false , 'pending' => false , 'uid' => $contact [ 'uid' ]];
// Forum posts are only accepted from forum contacts
2019-01-06 22:08:35 +00:00
if ( $contact [ 'contact-type' ] == Contact :: TYPE_COMMUNITY ) {
2018-11-03 21:37:08 +00:00
$condition [ 'rel' ] = [ Contact :: SHARING , Contact :: FRIEND , Contact :: FOLLOWER ];
}
if ( ! DBA :: exists ( 'contact' , $condition )) {
continue ;
}
}
2020-09-25 06:47:07 +00:00
$type = $receivers [ $contact [ 'uid' ]][ 'type' ] ? ? self :: TARGET_UNKNOWN ;
2020-09-14 17:48:57 +00:00
if ( in_array ( $type , [ self :: TARGET_UNKNOWN , self :: TARGET_FOLLOWER , self :: TARGET_ANSWER , self :: TARGET_GLOBAL ])) {
2020-09-12 17:45:04 +00:00
switch ( $element ) {
case 'as:to' :
$type = self :: TARGET_TO ;
break ;
case 'as:cc' :
$type = self :: TARGET_CC ;
break ;
case 'as:bto' :
$type = self :: TARGET_BTO ;
break ;
case 'as:bcc' :
$type = self :: TARGET_BCC ;
break ;
}
2020-09-25 06:47:07 +00:00
$receivers [ $contact [ 'uid' ]] = [ 'uid' => $contact [ 'uid' ], 'type' => $type ];
2020-09-12 17:45:04 +00:00
}
2018-10-03 06:15:07 +00:00
}
}
2018-10-03 09:15:38 +00:00
self :: switchContacts ( $receivers , $actor );
2018-10-03 06:15:07 +00:00
2022-10-09 21:16:36 +00:00
// "birdsitelive" is a service that mirrors tweets into the fediverse
// These posts can be fetched without authentification, but are not marked as public
// We treat them as unlisted posts to be able to handle them.
if ( empty ( $receivers ) && $fetch_unlisted && Contact :: isPlatform ( $actor , 'birdsitelive' )) {
$receivers [ 0 ] = [ 'uid' => 0 , 'type' => self :: TARGET_GLOBAL ];
$receivers [ - 1 ] = [ 'uid' => - 1 , 'type' => self :: TARGET_GLOBAL ];
Logger :: notice ( 'Post from "birdsitelive" is set to "unlisted"' , [ 'id' => JsonLD :: fetchElement ( $activity , '@id' )]);
} elseif ( empty ( $receivers )) {
Logger :: notice ( 'Post has got no receivers' , [ 'fetch_unlisted' => $fetch_unlisted , 'actor' => $actor , 'id' => JsonLD :: fetchElement ( $activity , '@id' ), 'type' => JsonLD :: fetchElement ( $activity , '@type' )]);
}
2018-10-03 06:15:07 +00:00
return $receivers ;
}
2018-11-03 21:37:08 +00:00
/**
* Fetch the receiver list of a given actor
*
2020-09-25 06:47:07 +00:00
* @ param string $actor
* @ param array $tags
* @ param array $receivers
* @ param integer $target_type
2022-05-03 08:19:35 +00:00
* @ param array $profile
2018-11-03 21:37:08 +00:00
*
* @ return array with receivers ( user id )
2019-01-06 21:06:53 +00:00
* @ throws \Exception
2018-11-03 21:37:08 +00:00
*/
2022-06-16 12:59:29 +00:00
private static function getReceiverForActor ( string $actor , array $tags , array $receivers , int $target_type , array $profile ) : array
2018-11-03 21:37:08 +00:00
{
2020-09-09 16:55:14 +00:00
$basecondition = [ 'rel' => [ Contact :: SHARING , Contact :: FRIEND , Contact :: FOLLOWER ],
'network' => Protocol :: FEDERATED , 'archive' => false , 'pending' => false ];
2022-05-03 08:19:35 +00:00
if ( ! empty ( $profile [ 'uri-id' ])) {
$condition = DBA :: mergeConditions ( $basecondition , [ " `uri-id` = ? AND `uid` != ? " , $profile [ 'uri-id' ], 0 ]);
$contacts = DBA :: select ( 'contact' , [ 'uid' , 'rel' ], $condition );
while ( $contact = DBA :: fetch ( $contacts )) {
if ( empty ( $receivers [ $contact [ 'uid' ]]) && self :: isValidReceiverForActor ( $contact , $tags )) {
$receivers [ $contact [ 'uid' ]] = [ 'uid' => $contact [ 'uid' ], 'type' => $target_type ];
}
2020-09-09 16:55:14 +00:00
}
2022-05-03 08:19:35 +00:00
DBA :: close ( $contacts );
} else {
// This part will only be called while post update 1426 wasn't finished
$condition = DBA :: mergeConditions ( $basecondition , [ " `nurl` = ? AND `uid` != ? " , Strings :: normaliseLink ( $actor ), 0 ]);
$contacts = DBA :: select ( 'contact' , [ 'uid' , 'rel' ], $condition );
while ( $contact = DBA :: fetch ( $contacts )) {
if ( empty ( $receivers [ $contact [ 'uid' ]]) && self :: isValidReceiverForActor ( $contact , $tags )) {
$receivers [ $contact [ 'uid' ]] = [ 'uid' => $contact [ 'uid' ], 'type' => $target_type ];
}
}
DBA :: close ( $contacts );
// The queries are split because of performance issues
$condition = DBA :: mergeConditions ( $basecondition , [ " `alias` IN (?, ?) AND `uid` != ? " , Strings :: normaliseLink ( $actor ), $actor , 0 ]);
$contacts = DBA :: select ( 'contact' , [ 'uid' , 'rel' ], $condition );
while ( $contact = DBA :: fetch ( $contacts )) {
if ( empty ( $receivers [ $contact [ 'uid' ]]) && self :: isValidReceiverForActor ( $contact , $tags )) {
$receivers [ $contact [ 'uid' ]] = [ 'uid' => $contact [ 'uid' ], 'type' => $target_type ];
}
2018-11-03 21:37:08 +00:00
}
2022-05-03 08:19:35 +00:00
DBA :: close ( $contacts );
2018-11-03 21:37:08 +00:00
}
return $receivers ;
}
/**
* Tests if the contact is a valid receiver for this actor
*
2019-01-06 21:06:53 +00:00
* @ param array $contact
* @ param array $tags
2018-11-03 21:37:08 +00:00
*
2019-01-06 21:06:53 +00:00
* @ return bool with receivers ( user id )
* @ throws \Exception
2018-11-03 21:37:08 +00:00
*/
2022-06-16 17:17:25 +00:00
private static function isValidReceiverForActor ( array $contact , array $tags ) : bool
2018-11-03 21:37:08 +00:00
{
// Are we following the contact? Then this is a valid receiver
if ( in_array ( $contact [ 'rel' ], [ Contact :: SHARING , Contact :: FRIEND ])) {
return true ;
}
// When the possible receiver isn't a community, then it is no valid receiver
$owner = User :: getOwnerDataById ( $contact [ 'uid' ]);
2019-01-06 22:08:35 +00:00
if ( empty ( $owner ) || ( $owner [ 'contact-type' ] != Contact :: TYPE_COMMUNITY )) {
2018-11-03 21:37:08 +00:00
return false ;
}
// Is the community account tagged?
foreach ( $tags as $tag ) {
if ( $tag [ 'type' ] != 'Mention' ) {
continue ;
}
2020-09-24 10:26:28 +00:00
if ( Strings :: compareLink ( $tag [ 'href' ], $owner [ 'url' ])) {
2018-11-03 21:37:08 +00:00
return true ;
}
}
return false ;
}
2018-10-03 06:15:07 +00:00
/**
2018-10-06 04:18:40 +00:00
* Switches existing contacts to ActivityPub
2018-10-03 06:15:07 +00:00
*
2018-10-05 19:48:48 +00:00
* @ param integer $cid Contact ID
2018-10-03 06:15:07 +00:00
* @ param integer $uid User ID
2019-01-06 21:06:53 +00:00
* @ param string $url Profile URL
2022-06-24 02:42:35 +00:00
* @ return void
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 06:15:07 +00:00
*/
2022-06-16 12:59:29 +00:00
public static function switchContact ( int $cid , int $uid , string $url )
2018-10-03 06:15:07 +00:00
{
2019-09-11 16:54:13 +00:00
if ( DBA :: exists ( 'contact' , [ 'id' => $cid , 'network' => Protocol :: ACTIVITYPUB ])) {
Logger :: info ( 'Contact is already ActivityPub' , [ 'id' => $cid , 'uid' => $uid , 'url' => $url ]);
return ;
}
2018-10-03 06:15:07 +00:00
2020-08-06 18:53:45 +00:00
if ( Contact :: updateFromProbe ( $cid )) {
2019-09-11 16:54:13 +00:00
Logger :: info ( 'Update was successful' , [ 'id' => $cid , 'uid' => $uid , 'url' => $url ]);
}
2018-10-04 12:57:42 +00:00
2018-10-05 19:48:48 +00:00
// Send a new follow request to be sure that the connection still exists
2019-09-11 16:54:13 +00:00
if (( $uid != 0 ) && DBA :: exists ( 'contact' , [ 'id' => $cid , 'rel' => [ Contact :: SHARING , Contact :: FRIEND ], 'network' => Protocol :: ACTIVITYPUB ])) {
Logger :: info ( 'Contact had been switched to ActivityPub. Sending a new follow request.' , [ 'uid' => $uid , 'url' => $url ]);
2019-07-11 20:11:51 +00:00
ActivityPub\Transmitter :: sendActivity ( 'Follow' , $url , $uid );
2018-10-05 19:48:48 +00:00
}
2018-10-03 06:15:07 +00:00
}
/**
2022-06-16 12:59:29 +00:00
* @ TODO Fix documentation and type - hints
2018-10-03 06:15:07 +00:00
*
* @ param $receivers
* @ param $actor
2022-06-24 02:42:35 +00:00
* @ return void
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 06:15:07 +00:00
*/
private static function switchContacts ( $receivers , $actor )
{
if ( empty ( $actor )) {
return ;
}
foreach ( $receivers as $receiver ) {
2020-09-12 17:45:04 +00:00
$contact = DBA :: selectFirst ( 'contact' , [ 'id' ], [ 'uid' => $receiver [ 'uid' ], 'network' => Protocol :: OSTATUS , 'nurl' => Strings :: normaliseLink ( $actor )]);
2018-10-03 06:15:07 +00:00
if ( DBA :: isResult ( $contact )) {
2020-09-12 17:45:04 +00:00
self :: switchContact ( $contact [ 'id' ], $receiver [ 'uid' ], $actor );
2018-10-03 06:15:07 +00:00
}
2020-09-12 17:45:04 +00:00
$contact = DBA :: selectFirst ( 'contact' , [ 'id' ], [ 'uid' => $receiver [ 'uid' ], 'network' => Protocol :: OSTATUS , 'alias' => [ Strings :: normaliseLink ( $actor ), $actor ]]);
2018-10-03 06:15:07 +00:00
if ( DBA :: isResult ( $contact )) {
2020-09-12 17:45:04 +00:00
self :: switchContact ( $contact [ 'id' ], $receiver [ 'uid' ], $actor );
2018-10-03 06:15:07 +00:00
}
}
}
/**
2022-06-16 12:59:29 +00:00
* @ TODO Fix documentation and type - hints
2018-10-03 06:15:07 +00:00
*
2019-01-06 21:06:53 +00:00
* @ param $object_data
2018-10-03 06:15:07 +00:00
* @ param array $activity
*
2019-01-06 21:06:53 +00:00
* @ return mixed
2018-10-03 06:15:07 +00:00
*/
2022-06-16 12:59:29 +00:00
private static function addActivityFields ( $object_data , array $activity )
2018-10-03 06:15:07 +00:00
{
if ( ! empty ( $activity [ 'published' ]) && empty ( $object_data [ 'published' ])) {
2018-10-13 21:37:39 +00:00
$object_data [ 'published' ] = JsonLD :: fetchElement ( $activity , 'as:published' , '@value' );
2018-10-03 06:15:07 +00:00
}
2018-10-06 14:02:23 +00:00
if ( ! empty ( $activity [ 'diaspora:guid' ]) && empty ( $object_data [ 'diaspora:guid' ])) {
2019-04-26 06:17:37 +00:00
$object_data [ 'diaspora:guid' ] = JsonLD :: fetchElement ( $activity , 'diaspora:guid' , '@value' );
2018-10-06 14:02:23 +00:00
}
2018-10-13 21:37:39 +00:00
$object_data [ 'service' ] = JsonLD :: fetchElement ( $activity , 'as:instrument' , 'as:name' , '@type' , 'as:Service' );
2019-04-26 06:17:37 +00:00
$object_data [ 'service' ] = JsonLD :: fetchElement ( $object_data , 'service' , '@value' );
2018-10-03 06:15:07 +00:00
2020-02-02 19:59:14 +00:00
if ( ! empty ( $object_data [ 'object_id' ])) {
// Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
$objectId = Item :: getURIByLink ( $object_data [ 'object_id' ]);
if ( ! empty ( $objectId ) && ( $object_data [ 'object_id' ] != $objectId )) {
Logger :: notice ( 'Fix wrong object-id' , [ 'received' => $object_data [ 'object_id' ], 'correct' => $objectId ]);
$object_data [ 'object_id' ] = $objectId ;
}
}
2018-10-03 06:15:07 +00:00
return $object_data ;
}
/**
2018-10-07 17:17:06 +00:00
* Fetches the object data from external ressources if needed
2018-10-03 06:15:07 +00:00
*
2018-10-07 17:17:06 +00:00
* @ param string $object_id Object ID of the the provided object
* @ param array $object The provided object array
* @ param boolean $trust_source Do we trust the provided object ?
2018-11-03 21:37:08 +00:00
* @ param integer $uid User ID for the signature that we use to fetch data
2018-10-03 06:15:07 +00:00
*
2019-06-13 23:07:39 +00:00
* @ return array | false with trusted and valid object data
2019-01-06 21:06:53 +00:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 06:15:07 +00:00
*/
2019-06-13 23:07:39 +00:00
private static function fetchObject ( string $object_id , array $object = [], bool $trust_source = false , int $uid = 0 )
2018-10-03 06:15:07 +00:00
{
2018-10-07 17:17:06 +00:00
// By fetching the type we check if the object is complete.
$type = JsonLD :: fetchElement ( $object , '@type' );
if ( ! $trust_source || empty ( $type )) {
2022-07-28 19:05:04 +00:00
$data = Processor :: fetchCachedActivity ( $object_id , $uid );
2018-10-07 15:34:51 +00:00
if ( ! empty ( $data )) {
$object = JsonLD :: compact ( $data );
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Fetched content for ' . $object_id );
2018-10-07 15:34:51 +00:00
} else {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Empty content for ' . $object_id . ', check if content is available locally.' );
2018-10-07 15:34:51 +00:00
2021-02-13 19:56:03 +00:00
$item = Post :: selectFirst ( Item :: DELIVER_FIELDLIST , [ 'uri' => $object_id ]);
2018-10-07 15:34:51 +00:00
if ( ! DBA :: isResult ( $item )) {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Object with url ' . $object_id . ' was not found locally.' );
2018-10-07 15:34:51 +00:00
return false ;
}
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Using already stored item for url ' . $object_id );
2018-10-07 15:34:51 +00:00
$data = ActivityPub\Transmitter :: createNote ( $item );
$object = JsonLD :: compact ( $data );
2018-10-03 06:15:07 +00:00
}
2020-09-12 12:12:55 +00:00
$id = JsonLD :: fetchElement ( $object , '@id' );
if ( empty ( $id )) {
Logger :: info ( 'Empty id' );
return false ;
}
2021-07-09 19:30:41 +00:00
2020-09-12 12:12:55 +00:00
if ( $id != $object_id ) {
Logger :: info ( 'Fetched id differs from provided id' , [ 'provided' => $object_id , 'fetched' => $id ]);
return false ;
}
2018-10-03 06:15:07 +00:00
} else {
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Using original object for url ' . $object_id );
2018-10-03 06:15:07 +00:00
}
2018-10-07 15:34:51 +00:00
$type = JsonLD :: fetchElement ( $object , '@type' );
if ( empty ( $type )) {
2020-09-12 12:12:55 +00:00
Logger :: info ( 'Empty type' );
2018-10-03 06:15:07 +00:00
return false ;
}
2022-01-22 15:24:51 +00:00
// Lemmy is resharing "create" activities instead of content
// We fetch the content from the activity.
if ( in_array ( $type , [ 'as:Create' ])) {
$object = $object [ 'as:object' ];
$type = JsonLD :: fetchElement ( $object , '@type' );
if ( empty ( $type )) {
Logger :: info ( 'Empty type' );
return false ;
}
2023-02-13 15:32:14 +00:00
$object_data = self :: processObject ( $object );
2022-01-22 15:24:51 +00:00
}
2020-09-12 12:12:55 +00:00
// We currently don't handle 'pt:CacheFile', but with this step we avoid logging
if ( in_array ( $type , self :: CONTENT_TYPES ) || ( $type == 'pt:CacheFile' )) {
2023-02-13 15:32:14 +00:00
$object_data = self :: processObject ( $object );
2020-03-03 22:43:19 +00:00
if ( ! empty ( $data )) {
2022-07-27 17:39:00 +00:00
$object_data [ 'raw-object' ] = json_encode ( $data );
2020-03-03 22:43:19 +00:00
}
return $object_data ;
2018-10-03 06:15:07 +00:00
}
2021-11-03 23:19:24 +00:00
Logger :: info ( 'Unhandled object type: ' . $type );
2019-06-13 23:07:39 +00:00
return false ;
2018-10-03 06:15:07 +00:00
}
2021-03-06 08:43:25 +00:00
/**
* Converts the language element ( Used by Peertube )
*
* @ param array $languages
* @ return array Languages
*/
2022-06-16 12:59:29 +00:00
public static function processLanguages ( array $languages ) : array
2021-03-06 08:43:25 +00:00
{
if ( empty ( $languages )) {
return [];
}
$language_list = [];
foreach ( $languages as $language ) {
if ( ! empty ( $language [ '_:identifier' ]) && ! empty ( $language [ 'as:name' ])) {
$language_list [ $language [ '_:identifier' ]] = $language [ 'as:name' ];
}
}
return $language_list ;
}
2018-10-03 06:15:07 +00:00
/**
2018-10-07 13:37:05 +00:00
* Convert tags from JSON - LD format into a simplified format
2018-10-03 06:15:07 +00:00
*
2018-10-07 13:37:05 +00:00
* @ param array $tags Tags in JSON - LD format
2018-10-03 06:15:07 +00:00
*
2018-10-07 13:37:05 +00:00
* @ return array with tags in a simplified format
*/
2022-06-16 12:59:29 +00:00
public static function processTags ( array $tags ) : array
2018-10-07 13:37:05 +00:00
{
$taglist = [];
2018-10-07 15:34:51 +00:00
2018-10-07 13:37:05 +00:00
foreach ( $tags as $tag ) {
2018-10-07 15:34:51 +00:00
if ( empty ( $tag )) {
continue ;
}
2022-12-08 03:35:37 +00:00
$element = [ 'type' => str_replace ( 'as:' , '' , JsonLD :: fetchElement ( $tag , '@type' ) ? ? '' ),
2019-04-26 06:17:37 +00:00
'href' => JsonLD :: fetchElement ( $tag , 'as:href' , '@id' ),
'name' => JsonLD :: fetchElement ( $tag , 'as:name' , '@value' )];
2018-11-07 20:34:03 +00:00
if ( empty ( $element [ 'type' ])) {
continue ;
}
2020-03-24 23:12:53 +00:00
if ( empty ( $element [ 'href' ])) {
$element [ 'href' ] = $element [ 'name' ];
}
2018-11-07 20:34:03 +00:00
$taglist [] = $element ;
2018-10-07 13:37:05 +00:00
}
return $taglist ;
}
2018-11-07 20:34:03 +00:00
/**
* Convert emojis from JSON - LD format into a simplified format
*
2020-06-04 19:51:14 +00:00
* @ param array $emojis
2018-11-07 20:34:03 +00:00
* @ return array with emojis in a simplified format
*/
2022-06-16 12:59:29 +00:00
private static function processEmojis ( array $emojis ) : array
2018-11-07 20:34:03 +00:00
{
$emojilist = [];
foreach ( $emojis as $emoji ) {
if ( empty ( $emoji ) || ( JsonLD :: fetchElement ( $emoji , '@type' ) != 'toot:Emoji' ) || empty ( $emoji [ 'as:icon' ])) {
continue ;
}
2019-04-26 06:17:37 +00:00
$url = JsonLD :: fetchElement ( $emoji [ 'as:icon' ], 'as:url' , '@id' );
$element = [ 'name' => JsonLD :: fetchElement ( $emoji , 'as:name' , '@value' ),
2018-11-07 20:34:03 +00:00
'href' => $url ];
$emojilist [] = $element ;
}
2020-06-04 19:51:14 +00:00
2018-11-07 20:34:03 +00:00
return $emojilist ;
}
2018-10-07 13:37:05 +00:00
/**
* Convert attachments from JSON - LD format into a simplified format
*
* @ param array $attachments Attachments in JSON - LD format
*
2020-07-20 04:26:42 +00:00
* @ return array Attachments in a simplified format
2018-10-07 13:37:05 +00:00
*/
2022-06-16 12:59:29 +00:00
private static function processAttachments ( array $attachments ) : array
2018-10-07 13:37:05 +00:00
{
$attachlist = [];
2018-10-07 15:34:51 +00:00
2020-06-04 19:51:14 +00:00
// Removes empty values
$attachments = array_filter ( $attachments );
2018-10-07 17:17:06 +00:00
2018-10-07 13:37:05 +00:00
foreach ( $attachments as $attachment ) {
2020-06-04 19:51:14 +00:00
switch ( JsonLD :: fetchElement ( $attachment , '@type' )) {
case 'as:Page' :
$pageUrl = null ;
$pageImage = null ;
$urls = JsonLD :: fetchElementArray ( $attachment , 'as:url' );
foreach ( $urls as $url ) {
// Single scalar URL case
if ( is_string ( $url )) {
$pageUrl = $url ;
continue ;
}
$href = JsonLD :: fetchElement ( $url , 'as:href' , '@id' );
$mediaType = JsonLD :: fetchElement ( $url , 'as:mediaType' , '@value' );
if ( Strings :: startsWith ( $mediaType , 'image' )) {
$pageImage = $href ;
} else {
$pageUrl = $href ;
}
}
2018-10-07 15:34:51 +00:00
2020-06-04 19:51:14 +00:00
$attachlist [] = [
'type' => 'link' ,
'title' => JsonLD :: fetchElement ( $attachment , 'as:name' , '@value' ),
'desc' => JsonLD :: fetchElement ( $attachment , 'as:summary' , '@value' ),
'url' => $pageUrl ,
'image' => $pageImage ,
];
break ;
2020-07-20 04:26:42 +00:00
case 'as:Image' :
$mediaType = JsonLD :: fetchElement ( $attachment , 'as:mediaType' , '@value' );
$imageFullUrl = JsonLD :: fetchElement ( $attachment , 'as:url' , '@id' );
$imagePreviewUrl = null ;
// Multiple URLs?
if ( ! $imageFullUrl && ( $urls = JsonLD :: fetchElementArray ( $attachment , 'as:url' ))) {
$imageVariants = [];
$previewVariants = [];
foreach ( $urls as $url ) {
// Scalar URL, no discrimination possible
if ( is_string ( $url )) {
$imageFullUrl = $url ;
continue ;
}
// Not sure what to do with a different Link media type than the base Image, we skip
if ( $mediaType != JsonLD :: fetchElement ( $url , 'as:mediaType' , '@value' )) {
continue ;
}
$href = JsonLD :: fetchElement ( $url , 'as:href' , '@id' );
// Default URL choice if no discriminating width is provided
$imageFullUrl = $href ? ? $imageFullUrl ;
$width = intval ( JsonLD :: fetchElement ( $url , 'as:width' , '@value' ) ? ? 1 );
if ( $href && $width ) {
$imageVariants [ $width ] = $href ;
// 632 is the ideal width for full screen frio posts, we compute the absolute distance to it
$previewVariants [ abs ( 632 - $width )] = $href ;
}
}
if ( $imageVariants ) {
// Taking the maximum size image
ksort ( $imageVariants );
$imageFullUrl = array_pop ( $imageVariants );
// Taking the minimum number distance to the target distance
ksort ( $previewVariants );
$imagePreviewUrl = array_shift ( $previewVariants );
}
unset ( $imageVariants );
unset ( $previewVariants );
}
$attachlist [] = [
'type' => str_replace ( 'as:' , '' , JsonLD :: fetchElement ( $attachment , '@type' )),
'mediaType' => $mediaType ,
'name' => JsonLD :: fetchElement ( $attachment , 'as:name' , '@value' ),
'url' => $imageFullUrl ,
'image' => $imagePreviewUrl !== $imageFullUrl ? $imagePreviewUrl : null ,
];
break ;
2020-06-04 19:51:14 +00:00
default :
$attachlist [] = [
'type' => str_replace ( 'as:' , '' , JsonLD :: fetchElement ( $attachment , '@type' )),
'mediaType' => JsonLD :: fetchElement ( $attachment , 'as:mediaType' , '@value' ),
'name' => JsonLD :: fetchElement ( $attachment , 'as:name' , '@value' ),
2021-07-04 06:30:54 +00:00
'url' => JsonLD :: fetchElement ( $attachment , 'as:url' , '@id' ),
'height' => JsonLD :: fetchElement ( $attachment , 'as:height' , '@value' ),
'width' => JsonLD :: fetchElement ( $attachment , 'as:width' , '@value' ),
'image' => JsonLD :: fetchElement ( $attachment , 'as:image' , '@id' )
2020-06-04 19:51:14 +00:00
];
}
2018-10-07 13:37:05 +00:00
}
2020-06-04 19:51:14 +00:00
2018-10-07 13:37:05 +00:00
return $attachlist ;
}
2022-04-15 09:11:50 +00:00
/**
* Convert questions from JSON - LD format into a simplified format
*
* @ param array $object
*
* @ return array Questions in a simplified format
*/
2022-06-16 12:59:29 +00:00
private static function processQuestion ( array $object ) : array
2022-04-15 09:11:50 +00:00
{
$question = [];
if ( ! empty ( $object [ 'as:oneOf' ])) {
$question [ 'multiple' ] = false ;
$options = JsonLD :: fetchElementArray ( $object , 'as:oneOf' ) ? ? [];
} elseif ( ! empty ( $object [ 'as:anyOf' ])) {
$question [ 'multiple' ] = true ;
$options = JsonLD :: fetchElementArray ( $object , 'as:anyOf' ) ? ? [];
} else {
return [];
}
2022-04-20 06:28:02 +00:00
$closed = JsonLD :: fetchElement ( $object , 'as:closed' , '@value' );
if ( ! empty ( $closed )) {
$question [ 'end-time' ] = $closed ;
} else {
$question [ 'end-time' ] = JsonLD :: fetchElement ( $object , 'as:endTime' , '@value' );
}
2022-04-15 09:11:50 +00:00
$question [ 'voters' ] = ( int ) JsonLD :: fetchElement ( $object , 'toot:votersCount' , '@value' );
$question [ 'options' ] = [];
$voters = 0 ;
foreach ( $options as $option ) {
if ( JsonLD :: fetchElement ( $option , '@type' ) != 'as:Note' ) {
continue ;
}
$name = JsonLD :: fetchElement ( $option , 'as:name' , '@value' );
if ( empty ( $option [ 'as:replies' ])) {
continue ;
}
$replies = JsonLD :: fetchElement ( $option [ 'as:replies' ], 'as:totalItems' , '@value' );
$question [ 'options' ][] = [ 'name' => $name , 'replies' => $replies ];
$voters += ( int ) $replies ;
}
// For single choice question we can count the number of voters if not provided (like with Misskey)
if ( empty ( $question [ 'voters' ]) && ! $question [ 'multiple' ]) {
$question [ 'voters' ] = $voters ;
}
return $question ;
}
2019-11-13 16:22:20 +00:00
/**
* Fetch the original source or content with the " language " Markdown or HTML
*
* @ param array $object
* @ param array $object_data
*
2022-06-16 12:59:29 +00:00
* @ return array Object data ( ? )
2019-11-13 16:22:20 +00:00
* @ throws \Exception
*/
2022-06-16 12:59:29 +00:00
private static function getSource ( array $object , array $object_data ) : array
2019-11-13 16:22:20 +00:00
{
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object , 'as:source' , 'as:content' , 'as:mediaType' , 'text/bbcode' );
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object_data , 'source' , '@value' );
if ( ! empty ( $object_data [ 'source' ])) {
return $object_data ;
}
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object , 'as:source' , 'as:content' , 'as:mediaType' , 'text/markdown' );
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object_data , 'source' , '@value' );
if ( ! empty ( $object_data [ 'source' ])) {
$object_data [ 'source' ] = Markdown :: toBBCode ( $object_data [ 'source' ]);
return $object_data ;
}
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object , 'as:source' , 'as:content' , 'as:mediaType' , 'text/html' );
$object_data [ 'source' ] = JsonLD :: fetchElement ( $object_data , 'source' , '@value' );
if ( ! empty ( $object_data [ 'source' ])) {
$object_data [ 'source' ] = HTML :: toBBCode ( $object_data [ 'source' ]);
return $object_data ;
}
return $object_data ;
}
2020-03-23 04:43:06 +00:00
/**
2021-08-05 14:51:42 +00:00
* Extracts a potential alternate URL from a list of additional URL elements
2020-03-23 04:43:06 +00:00
*
2021-08-05 14:51:42 +00:00
* @ param array $urls
* @ return string
2020-03-23 04:43:06 +00:00
*/
2021-08-05 14:51:42 +00:00
private static function extractAlternateUrl ( array $urls ) : string
{
$alternateUrl = '' ;
foreach ( $urls as $key => $url ) {
// Not a list but a single URL element
if ( ! is_numeric ( $key )) {
continue ;
}
2021-07-09 19:30:41 +00:00
2021-08-05 14:51:42 +00:00
if ( empty ( $url [ '@type' ]) || ( $url [ '@type' ] != 'as:Link' )) {
continue ;
}
$href = JsonLD :: fetchElement ( $url , 'as:href' , '@id' );
if ( empty ( $href )) {
continue ;
}
$mediatype = JsonLD :: fetchElement ( $url , 'as:mediaType' );
if ( empty ( $mediatype )) {
continue ;
}
if ( $mediatype == 'text/html' ) {
$alternateUrl = $href ;
}
2020-03-23 04:43:06 +00:00
}
2021-08-05 14:51:42 +00:00
return $alternateUrl ;
}
/**
* Check if the " as:url " element is an array with multiple links
* This is the case with audio and video posts .
* Then the links are added as attachments
*
* @ param array $urls The object URL list
* @ return array an array of attachments
*/
private static function processAttachmentUrls ( array $urls ) : array
{
2020-03-23 04:43:06 +00:00
$attachments = [];
2021-08-05 14:51:42 +00:00
foreach ( $urls as $key => $url ) {
// Not a list but a single URL element
if ( ! is_numeric ( $key )) {
continue ;
}
2020-03-23 04:43:06 +00:00
if ( empty ( $url [ '@type' ]) || ( $url [ '@type' ] != 'as:Link' )) {
continue ;
}
$href = JsonLD :: fetchElement ( $url , 'as:href' , '@id' );
if ( empty ( $href )) {
continue ;
}
$mediatype = JsonLD :: fetchElement ( $url , 'as:mediaType' );
if ( empty ( $mediatype )) {
continue ;
}
$filetype = strtolower ( substr ( $mediatype , 0 , strpos ( $mediatype , '/' )));
if ( $filetype == 'audio' ) {
2021-08-06 16:43:47 +00:00
$attachments [] = [ 'type' => $filetype , 'mediaType' => $mediatype , 'url' => $href , 'height' => null , 'size' => null , 'name' => '' ];
2020-03-23 04:43:06 +00:00
} elseif ( $filetype == 'video' ) {
$height = ( int ) JsonLD :: fetchElement ( $url , 'as:height' , '@value' );
2021-08-05 14:51:42 +00:00
// PeerTube audio-only track
if ( $height === 0 ) {
continue ;
}
2020-10-29 05:20:26 +00:00
$size = ( int ) JsonLD :: fetchElement ( $url , 'pt:size' , '@value' );
2021-08-06 16:43:47 +00:00
$attachments [] = [ 'type' => $filetype , 'mediaType' => $mediatype , 'url' => $href , 'height' => $height , 'size' => $size , 'name' => '' ];
2020-10-29 05:20:26 +00:00
} elseif ( in_array ( $mediatype , [ 'application/x-bittorrent' , 'application/x-bittorrent;x-scheme-handler/magnet' ])) {
$height = ( int ) JsonLD :: fetchElement ( $url , 'as:height' , '@value' );
// For Torrent links we always store the highest resolution
if ( ! empty ( $attachments [ $mediatype ][ 'height' ]) && ( $height < $attachments [ $mediatype ][ 'height' ])) {
continue ;
}
2021-08-06 16:43:47 +00:00
$attachments [ $mediatype ] = [ 'type' => $mediatype , 'mediaType' => $mediatype , 'url' => $href , 'height' => $height , 'size' => null , 'name' => '' ];
2021-08-05 14:51:42 +00:00
} elseif ( $mediatype == 'application/x-mpegURL' ) {
// PeerTube exception, actual video link is in the tags of this URL element
$attachments = array_merge ( $attachments , self :: processAttachmentUrls ( $url [ 'as:tag' ]));
2020-03-23 04:43:06 +00:00
}
}
2021-08-06 16:43:47 +00:00
return array_values ( $attachments );
2020-03-23 04:43:06 +00:00
}
2018-10-07 13:37:05 +00:00
/**
* Fetches data from the object part of an activity
*
* @ param array $object
*
2022-06-16 12:59:29 +00:00
* @ return array | bool Object data or FALSE if $object does not contain @ id element
2019-01-06 21:06:53 +00:00
* @ throws \Exception
2018-10-03 06:15:07 +00:00
*/
2023-02-13 15:32:14 +00:00
private static function processObject ( array $object )
2018-10-03 06:15:07 +00:00
{
2023-02-13 15:32:14 +00:00
if ( ! JsonLD :: fetchElement ( $object , '@id' )) {
2018-10-03 06:15:07 +00:00
return false ;
}
2023-02-13 15:32:14 +00:00
$object_data = self :: getObjectDataFromActivity ( $object );
$receiverdata = self :: getReceivers ( $object , $object_data [ 'actor' ] ? ? '' , $object_data [ 'tags' ], true , false );
$receivers = $reception_types = [];
foreach ( $receiverdata as $key => $data ) {
$receivers [ $key ] = $data [ 'uid' ];
$reception_types [ $data [ 'uid' ]] = $data [ 'type' ] ? ? 0 ;
}
$object_data [ 'receiver_urls' ] = self :: getReceiverURL ( $object );
$object_data [ 'receiver' ] = $receivers ;
$object_data [ 'reception_type' ] = $reception_types ;
$object_data [ 'unlisted' ] = in_array ( - 1 , $object_data [ 'receiver' ]);
unset ( $object_data [ 'receiver' ][ - 1 ]);
unset ( $object_data [ 'reception_type' ][ - 1 ]);
return $object_data ;
}
/**
* Create an object data array from a given activity
*
* @ param array $object
*
* @ return array Object data
*/
2023-02-13 21:27:11 +00:00
public static function getObjectDataFromActivity ( array $object ) : array
2023-02-13 15:32:14 +00:00
{
2018-10-03 06:15:07 +00:00
$object_data = [];
2018-10-07 13:37:05 +00:00
$object_data [ 'object_type' ] = JsonLD :: fetchElement ( $object , '@type' );
$object_data [ 'id' ] = JsonLD :: fetchElement ( $object , '@id' );
2019-04-26 06:17:37 +00:00
$object_data [ 'reply-to-id' ] = JsonLD :: fetchElement ( $object , 'as:inReplyTo' , '@id' );
2018-10-07 13:37:05 +00:00
2019-01-13 09:38:01 +00:00
// An empty "id" field is translated to "./" by the compactor, so we have to check for this content
if ( empty ( $object_data [ 'reply-to-id' ]) || ( $object_data [ 'reply-to-id' ] == './' )) {
2018-10-03 06:15:07 +00:00
$object_data [ 'reply-to-id' ] = $object_data [ 'id' ];
2022-01-22 15:24:51 +00:00
// On activities the "reply to" is the id of the object it refers to
2023-01-28 14:57:04 +00:00
if ( in_array ( $object_data [ 'object_type' ], array_merge ( self :: ACTIVITY_TYPES , [ 'as:Announce' ]))) {
2022-01-22 15:24:51 +00:00
$object_id = JsonLD :: fetchElement ( $object , 'as:object' , '@id' );
if ( ! empty ( $object_id )) {
$object_data [ 'reply-to-id' ] = $object_id ;
}
}
2020-02-02 19:59:14 +00:00
} else {
// Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field.
$replyToId = Item :: getURIByLink ( $object_data [ 'reply-to-id' ]);
if ( ! empty ( $replyToId ) && ( $object_data [ 'reply-to-id' ] != $replyToId )) {
Logger :: notice ( 'Fix wrong reply-to' , [ 'received' => $object_data [ 'reply-to-id' ], 'correct' => $replyToId ]);
$object_data [ 'reply-to-id' ] = $replyToId ;
}
2018-10-03 06:15:07 +00:00
}
2018-10-07 13:37:05 +00:00
$object_data [ 'published' ] = JsonLD :: fetchElement ( $object , 'as:published' , '@value' );
$object_data [ 'updated' ] = JsonLD :: fetchElement ( $object , 'as:updated' , '@value' );
if ( empty ( $object_data [ 'updated' ])) {
$object_data [ 'updated' ] = $object_data [ 'published' ];
}
2018-10-03 06:15:07 +00:00
if ( empty ( $object_data [ 'published' ]) && ! empty ( $object_data [ 'updated' ])) {
$object_data [ 'published' ] = $object_data [ 'updated' ];
}
2019-04-26 06:17:37 +00:00
$actor = JsonLD :: fetchElement ( $object , 'as:attributedTo' , '@id' );
2018-10-03 06:15:07 +00:00
if ( empty ( $actor )) {
2019-04-26 06:17:37 +00:00
$actor = JsonLD :: fetchElement ( $object , 'as:actor' , '@id' );
2018-10-03 06:15:07 +00:00
}
2020-05-07 02:41:59 +00:00
$location = JsonLD :: fetchElement ( $object , 'as:location' , 'as:name' , '@type' , 'as:Place' );
$location = JsonLD :: fetchElement ( $location , 'location' , '@value' );
2020-05-07 13:17:16 +00:00
if ( $location ) {
// Some AP software allow formatted text in post location, so we run all the text converters we have to boil
// down to HTML and then finally format to plaintext.
$location = Markdown :: convert ( $location );
2021-07-10 12:58:48 +00:00
$location = BBCode :: toPlaintext ( $location );
2020-05-07 13:17:16 +00:00
}
2020-05-07 02:41:59 +00:00
2020-03-05 08:06:19 +00:00
$object_data [ 'sc:identifier' ] = JsonLD :: fetchElement ( $object , 'sc:identifier' , '@value' );
2019-04-26 06:17:37 +00:00
$object_data [ 'diaspora:guid' ] = JsonLD :: fetchElement ( $object , 'diaspora:guid' , '@value' );
$object_data [ 'diaspora:comment' ] = JsonLD :: fetchElement ( $object , 'diaspora:comment' , '@value' );
$object_data [ 'diaspora:like' ] = JsonLD :: fetchElement ( $object , 'diaspora:like' , '@value' );
2018-10-07 17:35:43 +00:00
$object_data [ 'actor' ] = $object_data [ 'author' ] = $actor ;
2022-09-06 14:08:25 +00:00
$element = JsonLD :: fetchElement ( $object , 'as:context' , '@id' );
$object_data [ 'context' ] = $element != './' ? $element : null ;
$element = JsonLD :: fetchElement ( $object , 'ostatus:conversation' , '@id' );
$object_data [ 'conversation' ] = $element != './' ? $element : null ;
2018-10-07 13:37:05 +00:00
$object_data [ 'sensitive' ] = JsonLD :: fetchElement ( $object , 'as:sensitive' );
2019-04-26 06:17:37 +00:00
$object_data [ 'name' ] = JsonLD :: fetchElement ( $object , 'as:name' , '@value' );
$object_data [ 'summary' ] = JsonLD :: fetchElement ( $object , 'as:summary' , '@value' );
$object_data [ 'content' ] = JsonLD :: fetchElement ( $object , 'as:content' , '@value' );
2021-03-06 08:43:25 +00:00
$object_data [ 'mediatype' ] = JsonLD :: fetchElement ( $object , 'as:mediaType' , '@value' );
2019-11-13 16:22:20 +00:00
$object_data = self :: getSource ( $object , $object_data );
2018-10-26 04:13:26 +00:00
$object_data [ 'start-time' ] = JsonLD :: fetchElement ( $object , 'as:startTime' , '@value' );
$object_data [ 'end-time' ] = JsonLD :: fetchElement ( $object , 'as:endTime' , '@value' );
2020-05-07 02:41:59 +00:00
$object_data [ 'location' ] = $location ;
2018-10-13 21:37:39 +00:00
$object_data [ 'latitude' ] = JsonLD :: fetchElement ( $object , 'as:location' , 'as:latitude' , '@type' , 'as:Place' );
$object_data [ 'latitude' ] = JsonLD :: fetchElement ( $object_data , 'latitude' , '@value' );
$object_data [ 'longitude' ] = JsonLD :: fetchElement ( $object , 'as:location' , 'as:longitude' , '@type' , 'as:Place' );
$object_data [ 'longitude' ] = JsonLD :: fetchElement ( $object_data , 'longitude' , '@value' );
2020-06-04 19:51:14 +00:00
$object_data [ 'attachments' ] = self :: processAttachments ( JsonLD :: fetchElementArray ( $object , 'as:attachment' ) ? ? []);
$object_data [ 'tags' ] = self :: processTags ( JsonLD :: fetchElementArray ( $object , 'as:tag' ) ? ? []);
2020-11-11 16:19:08 +00:00
$object_data [ 'emojis' ] = self :: processEmojis ( JsonLD :: fetchElementArray ( $object , 'as:tag' , null , '@type' , 'toot:Emoji' ) ? ? []);
2021-03-06 08:43:25 +00:00
$object_data [ 'languages' ] = self :: processLanguages ( JsonLD :: fetchElementArray ( $object , 'sc:inLanguage' ) ? ? []);
2018-10-13 21:37:39 +00:00
$object_data [ 'generator' ] = JsonLD :: fetchElement ( $object , 'as:generator' , 'as:name' , '@type' , 'as:Application' );
2019-04-26 06:17:37 +00:00
$object_data [ 'generator' ] = JsonLD :: fetchElement ( $object_data , 'generator' , '@value' );
$object_data [ 'alternate-url' ] = JsonLD :: fetchElement ( $object , 'as:url' , '@id' );
2018-10-07 13:37:05 +00:00
// Special treatment for Hubzilla links
if ( is_array ( $object_data [ 'alternate-url' ])) {
2019-04-26 06:17:37 +00:00
$object_data [ 'alternate-url' ] = JsonLD :: fetchElement ( $object_data [ 'alternate-url' ], 'as:href' , '@id' );
2018-10-14 17:57:44 +00:00
if ( ! is_string ( $object_data [ 'alternate-url' ])) {
2019-04-26 06:17:37 +00:00
$object_data [ 'alternate-url' ] = JsonLD :: fetchElement ( $object [ 'as:url' ], 'as:href' , '@id' );
2018-10-07 13:37:05 +00:00
}
}
2022-04-03 07:21:36 +00:00
if ( ! empty ( $object_data [ 'alternate-url' ]) && ! Network :: isValidHttpUrl ( $object_data [ 'alternate-url' ])) {
$object_data [ 'alternate-url' ] = null ;
}
2020-03-23 04:43:06 +00:00
if ( in_array ( $object_data [ 'object_type' ], [ 'as:Audio' , 'as:Video' ])) {
2021-08-05 14:51:42 +00:00
$object_data [ 'alternate-url' ] = self :: extractAlternateUrl ( $object [ 'as:url' ] ? ? []) ? : $object_data [ 'alternate-url' ];
2021-08-06 16:43:47 +00:00
$object_data [ 'attachments' ] = array_merge ( $object_data [ 'attachments' ], self :: processAttachmentUrls ( $object [ 'as:url' ] ? ? []));
2020-03-23 04:43:06 +00:00
}
2022-09-29 16:04:33 +00:00
// Support for quoted posts (Pleroma, Fedibird and Misskey)
$object_data [ 'quote-url' ] = JsonLD :: fetchElement ( $object , 'as:quoteUrl' , '@value' );
if ( empty ( $object_data [ 'quote-url' ])) {
$object_data [ 'quote-url' ] = JsonLD :: fetchElement ( $object , 'fedibird:quoteUri' , '@value' );
}
if ( empty ( $object_data [ 'quote-url' ])) {
$object_data [ 'quote-url' ] = JsonLD :: fetchElement ( $object , 'misskey:_misskey_quote' , '@value' );
}
// Misskey adds some data to the standard "content" value for quoted posts for backwards compatibility.
// Their own "_misskey_content" value does then contain the content without this extra data.
if ( ! empty ( $object_data [ 'quote-url' ])) {
$misskey_content = JsonLD :: fetchElement ( $object , 'misskey:_misskey_content' , '@value' );
if ( ! empty ( $misskey_content )) {
$object_data [ 'content' ] = $misskey_content ;
}
}
2022-01-27 17:51:23 +00:00
// For page types we expect that the alternate url posts to some page.
// So we add this to the attachments if it differs from the id.
// Currently only Lemmy is using the page type.
if (( $object_data [ 'object_type' ] == 'as:Page' ) && ! empty ( $object_data [ 'alternate-url' ]) && ! Strings :: compareLink ( $object_data [ 'alternate-url' ], $object_data [ 'id' ])) {
$object_data [ 'attachments' ][] = [ 'url' => $object_data [ 'alternate-url' ]];
$object_data [ 'alternate-url' ] = null ;
}
2022-04-15 09:11:50 +00:00
if ( $object_data [ 'object_type' ] == 'as:Question' ) {
$object_data [ 'question' ] = self :: processQuestion ( $object );
}
2018-10-03 06:15:07 +00:00
return $object_data ;
}
2022-08-06 17:06:55 +00:00
/**
* Add an object id to the list of arrived activities
*
* @ param string $id
*
* @ return void
*/
private static function addArrivedId ( string $id )
{
DBA :: delete ( 'arrived-activity' , [ " `received` < ? " , DateTimeFormat :: utc ( 'now - 5 minutes' )]);
2022-08-25 04:57:41 +00:00
DBA :: insert ( 'arrived-activity' , [ 'object-id' => $id , 'received' => DateTimeFormat :: utcNow ()], Database :: INSERT_IGNORE );
2022-08-06 17:06:55 +00:00
}
/**
* Checks if the given object already arrived before
*
* @ param string $id
*
* @ return boolean
*/
private static function hasArrived ( string $id ) : bool
{
return DBA :: exists ( 'arrived-activity' , [ 'object-id' => $id ]);
}
2018-10-03 06:15:07 +00:00
}