2018-10-03 08:15:07 +02:00
< ? php
/**
2020-02-09 16:18:46 +01:00
* @ copyright Copyright ( C ) 2020 , Friendica
*
* @ 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 08:15:07 +02:00
*/
2020-02-09 16:18:46 +01:00
2018-10-03 08:15:07 +02:00
namespace Friendica\Protocol\ActivityPub ;
2020-07-18 01:39:12 +02:00
use Friendica\Content\PageInfo ;
2019-05-16 07:44:59 +02:00
use Friendica\Content\Text\BBCode ;
2018-11-08 17:28:29 +01:00
use Friendica\Content\Text\HTML ;
2018-10-29 22:20:46 +01:00
use Friendica\Core\Logger ;
2018-10-03 08:15:07 +02:00
use Friendica\Core\Protocol ;
2019-10-25 00:10:20 +02:00
use Friendica\Database\DBA ;
2020-01-18 16:50:57 +01:00
use Friendica\DI ;
2018-10-03 08:15:07 +02:00
use Friendica\Model\APContact ;
2019-10-25 00:10:20 +02:00
use Friendica\Model\Contact ;
2020-02-28 10:46:53 +01:00
use Friendica\Model\Conversation ;
2018-10-26 06:13:26 +02:00
use Friendica\Model\Event ;
2019-10-25 00:10:20 +02:00
use Friendica\Model\Item ;
2020-04-14 01:54:28 +02:00
use Friendica\Model\ItemURI ;
2019-10-25 00:10:20 +02:00
use Friendica\Model\Mail ;
2020-09-22 17:48:44 +02:00
use Friendica\Model\Search ;
2020-04-17 08:35:20 +02:00
use Friendica\Model\Tag ;
2018-10-03 08:15:07 +02:00
use Friendica\Model\User ;
2019-10-24 00:25:43 +02:00
use Friendica\Protocol\Activity ;
2018-10-03 08:15:07 +02:00
use Friendica\Protocol\ActivityPub ;
2018-10-27 08:17:17 +02:00
use Friendica\Util\DateTimeFormat ;
2018-11-08 17:28:29 +01:00
use Friendica\Util\JsonLD ;
use Friendica\Util\Strings ;
2020-09-23 11:00:09 +02:00
use Text_LanguageDetect ;
2018-10-03 08:15:07 +02:00
/**
2018-10-24 06:51:37 +02:00
* ActivityPub Processor Protocol class
2018-10-03 08:15:07 +02:00
*/
class Processor
{
/**
2018-10-06 06:18:40 +02:00
* Converts mentions from Pleroma into the Friendica format
2018-10-03 08:15:07 +02:00
*
* @ param string $body
*
2019-01-06 22:06:53 +01:00
* @ return string converted body
2018-10-03 08:15:07 +02:00
*/
private static function convertMentions ( $body )
{
$URLSearchString = " ^ \ [ \ ] " ;
$body = preg_replace ( " / \ [url \ =([ $URLSearchString ]*) \ ]([#@!])(.*?) \ [ \ /url \ ]/ism " , '$2[url=$1]$3[/url]' , $body );
return $body ;
}
2018-11-07 21:34:03 +01:00
/**
* Replaces emojis in the body
*
* @ param array $emojis
* @ param string $body
*
* @ return string with replaced emojis
*/
2019-03-17 23:13:17 +01:00
private static function replaceEmojis ( $body , array $emojis )
2018-11-07 21:34:03 +01:00
{
foreach ( $emojis as $emoji ) {
2018-11-08 08:42:19 +01:00
$replace = '[class=emoji mastodon][img=' . $emoji [ 'href' ] . ']' . $emoji [ 'name' ] . '[/img][/class]' ;
$body = str_replace ( $emoji [ 'name' ], $replace , $body );
2018-11-07 21:34:03 +01:00
}
return $body ;
}
2018-10-03 08:15:07 +02:00
/**
2018-10-07 20:41:45 +02:00
* Add attachment data to the item array
2018-10-03 08:15:07 +02:00
*
2019-11-28 07:34:35 +01:00
* @ param array $activity
2019-05-13 21:56:46 +02:00
* @ param array $item
2018-10-03 08:15:07 +02:00
*
2019-01-06 22:06:53 +01:00
* @ return array array
2018-10-03 08:15:07 +02:00
*/
2019-11-28 07:34:35 +01:00
private static function constructAttachList ( $activity , $item )
2018-10-03 08:15:07 +02:00
{
2019-11-28 07:34:35 +01:00
if ( empty ( $activity [ 'attachments' ])) {
2018-10-03 08:15:07 +02:00
return $item ;
}
2019-11-28 07:34:35 +01:00
foreach ( $activity [ 'attachments' ] as $attach ) {
2020-06-04 21:51:14 +02:00
switch ( $attach [ 'type' ]) {
case 'link' :
2020-07-18 01:39:12 +02:00
$data = [
'url' => $attach [ 'url' ],
'type' => $attach [ 'type' ],
'title' => $attach [ 'title' ] ? ? '' ,
'text' => $attach [ 'desc' ] ? ? '' ,
'image' => $attach [ 'image' ] ? ? '' ,
'images' => [],
'keywords' => [],
];
$item [ 'body' ] = PageInfo :: appendDataToBody ( $item [ 'body' ], $data );
2020-06-04 21:51:14 +02:00
break ;
default :
$filetype = strtolower ( substr ( $attach [ 'mediaType' ], 0 , strpos ( $attach [ 'mediaType' ], '/' )));
if ( $filetype == 'image' ) {
if ( ! empty ( $activity [ 'source' ]) && strpos ( $activity [ 'source' ], $attach [ 'url' ])) {
2020-06-09 18:40:36 +02:00
continue 2 ;
2020-06-04 21:51:14 +02:00
}
2019-05-13 21:56:46 +02:00
2020-07-20 06:27:36 +02:00
$item [ 'body' ] .= " \n " ;
// image is the preview/thumbnail URL
if ( ! empty ( $attach [ 'image' ])) {
$item [ 'body' ] .= '[url=' . $attach [ 'url' ] . ']' ;
$attach [ 'url' ] = $attach [ 'image' ];
}
2020-06-04 21:51:14 +02:00
if ( empty ( $attach [ 'name' ])) {
2020-07-20 06:27:36 +02:00
$item [ 'body' ] .= '[img]' . $attach [ 'url' ] . '[/img]' ;
2020-06-04 21:51:14 +02:00
} else {
2020-07-20 06:27:36 +02:00
$item [ 'body' ] .= '[img=' . $attach [ 'url' ] . ']' . $attach [ 'name' ] . '[/img]' ;
}
if ( ! empty ( $attach [ 'image' ])) {
$item [ 'body' ] .= '[/url]' ;
2020-06-04 21:51:14 +02:00
}
} elseif ( $filetype == 'audio' ) {
if ( ! empty ( $activity [ 'source' ]) && strpos ( $activity [ 'source' ], $attach [ 'url' ])) {
2020-06-09 18:40:36 +02:00
continue 2 ;
2020-06-04 21:51:14 +02:00
}
2020-03-23 05:43:06 +01:00
2020-06-04 21:51:14 +02:00
$item [ 'body' ] .= " \n [audio] " . $attach [ 'url' ] . '[/audio]' ;
} elseif ( $filetype == 'video' ) {
if ( ! empty ( $activity [ 'source' ]) && strpos ( $activity [ 'source' ], $attach [ 'url' ])) {
2020-06-09 18:40:36 +02:00
continue 2 ;
2020-06-04 21:51:14 +02:00
}
2020-03-23 05:43:06 +01:00
2020-06-04 21:51:14 +02:00
$item [ 'body' ] .= " \n [video] " . $attach [ 'url' ] . '[/video]' ;
} else {
if ( ! empty ( $item [ " attach " ])) {
$item [ " attach " ] .= ',' ;
} else {
$item [ " attach " ] = '' ;
}
$item [ " attach " ] .= '[attach]href="' . $attach [ 'url' ] . '" length="' . ( $attach [ 'length' ] ? ? '0' ) . '" type="' . $attach [ 'mediaType' ] . '" title="' . ( $attach [ 'name' ] ? ? '' ) . '"[/attach]' ;
}
2018-10-03 08:15:07 +02:00
}
}
return $item ;
}
2018-10-27 08:17:17 +02:00
/**
* Updates a message
*
2019-01-06 22:06:53 +01:00
* @ param array $activity Activity array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-27 08:17:17 +02:00
*/
public static function updateItem ( $activity )
{
2020-04-15 07:10:40 +02:00
$item = Item :: selectFirst ([ 'uri' , 'uri-id' , 'thr-parent' , 'gravity' ], [ 'uri' => $activity [ 'id' ]]);
2019-02-09 04:57:35 +01:00
if ( ! DBA :: isResult ( $item )) {
2020-04-23 21:57:20 +02:00
Logger :: warning ( 'No existing item, item will be created' , [ 'uri' => $activity [ 'id' ]]);
2020-07-20 06:37:43 +02:00
$item = self :: createItem ( $activity );
self :: postItem ( $activity , $item );
2019-02-09 04:57:35 +01:00
return ;
}
2018-10-27 08:17:17 +02:00
$item [ 'changed' ] = DateTimeFormat :: utcNow ();
2019-06-13 03:02:37 +02:00
$item [ 'edited' ] = DateTimeFormat :: utc ( $activity [ 'updated' ]);
2019-02-09 04:57:35 +01:00
2019-03-17 14:50:14 +01:00
$item = self :: processContent ( $activity , $item );
if ( empty ( $item )) {
return ;
2019-02-09 04:57:35 +01:00
}
2018-10-27 08:17:17 +02:00
Item :: update ( $item , [ 'uri' => $activity [ 'id' ]]);
}
2018-10-03 08:15:07 +02:00
/**
2018-10-07 20:41:45 +02:00
* Prepares data for a message
2018-10-03 08:15:07 +02:00
*
2019-01-06 22:06:53 +01:00
* @ param array $activity Activity array
2020-07-20 06:37:43 +02:00
* @ return array Internal item
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-11 22:08:04 +02:00
public static function createItem ( $activity )
2018-10-03 08:15:07 +02:00
{
$item = [];
2019-10-24 00:25:43 +02:00
$item [ 'verb' ] = Activity :: POST ;
2019-02-23 05:42:04 +01:00
$item [ 'thr-parent' ] = $activity [ 'reply-to-id' ];
2018-10-03 08:15:07 +02:00
if ( $activity [ 'reply-to-id' ] == $activity [ 'id' ]) {
$item [ 'gravity' ] = GRAVITY_PARENT ;
2019-10-25 00:10:20 +02:00
$item [ 'object-type' ] = Activity\ObjectType :: NOTE ;
2018-10-03 08:15:07 +02:00
} else {
$item [ 'gravity' ] = GRAVITY_COMMENT ;
2019-10-25 00:10:20 +02:00
$item [ 'object-type' ] = Activity\ObjectType :: COMMENT ;
2018-10-03 08:15:07 +02:00
}
2019-05-18 09:00:57 +02:00
if ( empty ( $activity [ 'directmessage' ]) && ( $activity [ 'id' ] != $activity [ 'reply-to-id' ]) && ! Item :: exists ([ 'uri' => $activity [ 'reply-to-id' ]])) {
2020-02-02 20:59:14 +01:00
Logger :: notice ( 'Parent not found. Try to refetch it.' , [ 'parent' => $activity [ 'reply-to-id' ]]);
2018-10-03 08:15:07 +02:00
self :: fetchMissingActivity ( $activity [ 'reply-to-id' ], $activity );
}
2019-10-16 14:35:14 +02:00
$item [ 'diaspora_signed_text' ] = $activity [ 'diaspora:comment' ] ? ? '' ;
2018-10-27 16:35:22 +02:00
2020-07-20 06:37:43 +02:00
/// @todo What to do with $activity['context']?
if ( empty ( $activity [ 'directmessage' ]) && ( $item [ 'gravity' ] != GRAVITY_PARENT ) && ! Item :: exists ([ 'uri' => $item [ 'thr-parent' ]])) {
Logger :: info ( 'Parent not found, message will be discarded.' , [ 'thr-parent' => $item [ 'thr-parent' ]]);
return [];
}
$item [ 'network' ] = Protocol :: ACTIVITYPUB ;
$item [ 'author-link' ] = $activity [ 'author' ];
2020-08-07 15:49:59 +02:00
$item [ 'author-id' ] = Contact :: getIdForURL ( $activity [ 'author' ]);
2020-07-20 06:37:43 +02:00
$item [ 'owner-link' ] = $activity [ 'actor' ];
2020-08-07 15:49:59 +02:00
$item [ 'owner-id' ] = Contact :: getIdForURL ( $activity [ 'actor' ]);
2020-07-20 06:37:43 +02:00
if ( in_array ( 0 , $activity [ 'receiver' ]) && ! empty ( $activity [ 'unlisted' ])) {
$item [ 'private' ] = Item :: UNLISTED ;
} elseif ( in_array ( 0 , $activity [ 'receiver' ])) {
$item [ 'private' ] = Item :: PUBLIC ;
} else {
$item [ 'private' ] = Item :: PRIVATE ;
}
if ( ! empty ( $activity [ 'raw' ])) {
$item [ 'source' ] = $activity [ 'raw' ];
$item [ 'protocol' ] = Conversation :: PARCEL_ACTIVITYPUB ;
$item [ 'conversation-href' ] = $activity [ 'context' ] ? ? '' ;
$item [ 'conversation-uri' ] = $activity [ 'conversation' ] ? ? '' ;
if ( isset ( $activity [ 'push' ])) {
$item [ 'direction' ] = $activity [ 'push' ] ? Conversation :: PUSH : Conversation :: PULL ;
}
}
$item [ 'isForum' ] = false ;
if ( ! empty ( $activity [ 'thread-completion' ])) {
2020-09-25 14:16:08 +02:00
if ( $activity [ 'thread-completion' ] != $item [ 'owner-id' ]) {
$actor = Contact :: getById ( $activity [ 'thread-completion' ], [ 'url' ]);
$item [ 'causer-link' ] = $actor [ 'url' ];
$item [ 'causer-id' ] = $activity [ 'thread-completion' ];
Logger :: info ( 'Use inherited actor as causer.' , [ 'id' => $item [ 'owner-id' ], 'activity' => $activity [ 'thread-completion' ], 'owner' => $item [ 'owner-link' ], 'actor' => $actor [ 'url' ]]);
} else {
// Store the original actor in the "causer" fields to enable the check for ignored or blocked contacts
$item [ 'causer-link' ] = $item [ 'owner-link' ];
$item [ 'causer-id' ] = $item [ 'owner-id' ];
Logger :: info ( 'Use actor as causer.' , [ 'id' => $item [ 'owner-id' ], 'actor' => $item [ 'owner-link' ]]);
}
2020-07-20 06:37:43 +02:00
$item [ 'owner-link' ] = $item [ 'author-link' ];
$item [ 'owner-id' ] = $item [ 'author-id' ];
} else {
$actor = APContact :: getByURL ( $item [ 'owner-link' ], false );
$item [ 'isForum' ] = ( $actor [ 'type' ] == 'Group' );
}
$item [ 'uri' ] = $activity [ 'id' ];
$item [ 'created' ] = DateTimeFormat :: utc ( $activity [ 'published' ]);
$item [ 'edited' ] = DateTimeFormat :: utc ( $activity [ 'updated' ]);
$guid = $activity [ 'sc:identifier' ] ? : self :: getGUIDByURL ( $item [ 'uri' ]);
$item [ 'guid' ] = $activity [ 'diaspora:guid' ] ? : $guid ;
$item [ 'uri-id' ] = ItemURI :: insert ([ 'uri' => $item [ 'uri' ], 'guid' => $item [ 'guid' ]]);
$item = self :: processContent ( $activity , $item );
if ( empty ( $item )) {
2020-09-20 09:46:23 +02:00
Logger :: info ( 'Message was not processed' );
2020-07-20 06:37:43 +02:00
return [];
}
$item [ 'plink' ] = $activity [ 'alternate-url' ] ? ? $item [ 'uri' ];
$item = self :: constructAttachList ( $activity , $item );
return $item ;
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Delete items
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function deleteItem ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 19:35:43 +02:00
$owner = Contact :: getIdForURL ( $activity [ 'actor' ]);
2018-10-07 20:41:45 +02:00
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Deleting item' , [ 'object' => $activity [ 'object_id' ], 'owner' => $owner ]);
2020-03-03 07:47:28 +01:00
Item :: markForDeletion ([ 'uri' => $activity [ 'object_id' ], 'owner-id' => $owner ]);
2018-10-03 08:15:07 +02:00
}
2019-05-26 13:20:03 +02:00
/**
* Prepare the item array for an activity
*
* @ param array $activity Activity array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
*/
public static function addTag ( $activity )
{
if ( empty ( $activity [ 'object_content' ]) || empty ( $activity [ 'object_id' ])) {
return ;
}
foreach ( $activity [ 'receiver' ] as $receiver ) {
2020-04-17 15:34:29 +02:00
$item = Item :: selectFirst ([ 'id' , 'uri-id' , 'tag' , 'origin' , 'author-link' ], [ 'uri' => $activity [ 'target_id' ], 'uid' => $receiver ]);
2019-05-26 13:20:03 +02:00
if ( ! DBA :: isResult ( $item )) {
// We don't fetch missing content for this purpose
continue ;
}
if (( $item [ 'author-link' ] != $activity [ 'actor' ]) && ! $item [ 'origin' ]) {
Logger :: info ( 'Not origin, not from the author, skipping update' , [ 'id' => $item [ 'id' ], 'author' => $item [ 'author-link' ], 'actor' => $activity [ 'actor' ]]);
continue ;
}
2020-04-17 15:34:29 +02:00
Tag :: store ( $item [ 'uri-id' ], Tag :: HASHTAG , $activity [ 'object_content' ], $activity [ 'object_id' ]);
2020-05-05 07:11:59 +02:00
Logger :: info ( 'Tagged item' , [ 'id' => $item [ 'id' ], 'tag' => $activity [ 'object_content' ], 'uri' => $activity [ 'target_id' ], 'actor' => $activity [ 'actor' ]]);
2019-05-26 13:20:03 +02:00
}
}
2018-10-03 08:15:07 +02:00
/**
2018-10-27 08:17:17 +02:00
* Prepare the item array for an activity
2018-10-03 08:15:07 +02:00
*
2018-10-07 20:41:45 +02:00
* @ param array $activity Activity array
2018-10-27 08:17:17 +02:00
* @ param string $verb Activity verb
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-27 08:17:17 +02:00
public static function createActivity ( $activity , $verb )
2018-10-03 08:15:07 +02:00
{
2020-07-20 06:37:43 +02:00
$item = self :: createItem ( $activity );
2018-10-27 08:17:17 +02:00
$item [ 'verb' ] = $verb ;
2019-02-23 05:42:04 +01:00
$item [ 'thr-parent' ] = $activity [ 'object_id' ];
2018-10-03 08:15:07 +02:00
$item [ 'gravity' ] = GRAVITY_ACTIVITY ;
2019-10-25 00:10:20 +02:00
$item [ 'object-type' ] = Activity\ObjectType :: NOTE ;
2018-10-03 08:15:07 +02:00
2019-10-16 14:35:14 +02:00
$item [ 'diaspora_signed_text' ] = $activity [ 'diaspora:like' ] ? ? '' ;
2018-10-27 16:35:22 +02:00
2018-10-11 22:08:04 +02:00
self :: postItem ( $activity , $item );
2018-10-03 08:15:07 +02:00
}
2018-10-26 06:13:26 +02:00
/**
* Create an event
*
2018-10-27 08:17:17 +02:00
* @ param array $activity Activity array
* @ param array $item
2019-01-06 22:06:53 +01:00
* @ throws \Exception
2018-10-26 06:13:26 +02:00
*/
public static function createEvent ( $activity , $item )
{
2018-12-25 04:52:21 +01:00
$event [ 'summary' ] = HTML :: toBBCode ( $activity [ 'name' ]);
$event [ 'desc' ] = HTML :: toBBCode ( $activity [ 'content' ]);
$event [ 'start' ] = $activity [ 'start-time' ];
$event [ 'finish' ] = $activity [ 'end-time' ];
2018-10-26 06:13:26 +02:00
$event [ 'nofinish' ] = empty ( $event [ 'finish' ]);
$event [ 'location' ] = $activity [ 'location' ];
2018-12-25 04:52:21 +01:00
$event [ 'adjust' ] = true ;
$event [ 'cid' ] = $item [ 'contact-id' ];
$event [ 'uid' ] = $item [ 'uid' ];
$event [ 'uri' ] = $item [ 'uri' ];
$event [ 'edited' ] = $item [ 'edited' ];
$event [ 'private' ] = $item [ 'private' ];
$event [ 'guid' ] = $item [ 'guid' ];
$event [ 'plink' ] = $item [ 'plink' ];
2018-10-26 06:13:26 +02:00
$condition = [ 'uri' => $item [ 'uri' ], 'uid' => $item [ 'uid' ]];
$ev = DBA :: selectFirst ( 'event' , [ 'id' ], $condition );
if ( DBA :: isResult ( $ev )) {
$event [ 'id' ] = $ev [ 'id' ];
}
$event_id = Event :: store ( $event );
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Event was stored' , [ 'id' => $event_id ]);
2018-10-26 06:13:26 +02:00
}
2019-03-17 14:50:14 +01:00
/**
* Process the content
*
* @ param array $activity Activity array
* @ param array $item
2019-03-17 16:49:21 +01:00
* @ return array | bool Returns the item array or false if there was an unexpected occurrence
2019-03-17 14:50:14 +01:00
* @ throws \Exception
*/
private static function processContent ( $activity , $item )
{
$item [ 'title' ] = HTML :: toBBCode ( $activity [ 'name' ]);
if ( ! empty ( $activity [ 'source' ])) {
$item [ 'body' ] = $activity [ 'source' ];
} else {
$content = HTML :: toBBCode ( $activity [ 'content' ]);
2019-03-17 23:13:17 +01:00
if ( ! empty ( $activity [ 'emojis' ])) {
$content = self :: replaceEmojis ( $content , $activity [ 'emojis' ]);
}
2019-03-17 14:50:14 +01:00
$content = self :: convertMentions ( $content );
2019-05-16 07:44:59 +02:00
if ( empty ( $activity [ 'directmessage' ]) && ( $item [ 'thr-parent' ] != $item [ 'uri' ]) && ( $item [ 'gravity' ] == GRAVITY_COMMENT )) {
2019-03-17 14:50:14 +01:00
$item_private = ! in_array ( 0 , $activity [ 'item_receiver' ]);
2020-05-02 07:14:30 +02:00
$parent = Item :: selectFirst ([ 'id' , 'uri-id' , 'private' , 'author-link' , 'alias' ], [ 'uri' => $item [ 'thr-parent' ]]);
2019-03-17 14:50:14 +01:00
if ( ! DBA :: isResult ( $parent )) {
2019-03-17 14:56:47 +01:00
Logger :: warning ( 'Unknown parent item.' , [ 'uri' => $item [ 'thr-parent' ]]);
2019-03-17 14:50:14 +01:00
return false ;
}
2020-06-27 12:35:45 +02:00
if ( $item_private && ( $parent [ 'private' ] != Item :: PRIVATE )) {
2019-03-17 14:50:14 +01:00
Logger :: warning ( 'Item is private but the parent is not. Dropping.' , [ 'item-uri' => $item [ 'uri' ], 'thr-parent' => $item [ 'thr-parent' ]]);
return false ;
}
2020-05-09 10:55:10 +02:00
$content = self :: removeImplicitMentionsFromBody ( $content , $parent );
2019-03-17 14:50:14 +01:00
}
$item [ 'content-warning' ] = HTML :: toBBCode ( $activity [ 'summary' ]);
$item [ 'body' ] = $content ;
}
2020-04-20 11:47:26 +02:00
self :: storeFromBody ( $item );
2020-04-14 19:18:48 +02:00
self :: storeTags ( $item [ 'uri-id' ], $activity [ 'tags' ]);
2020-04-14 01:54:28 +02:00
2019-03-17 14:50:14 +01:00
$item [ 'location' ] = $activity [ 'location' ];
2020-06-28 10:46:27 +02:00
if ( ! empty ( $activity [ 'latitude' ]) && ! empty ( $activity [ 'longitude' ])) {
$item [ 'coord' ] = $activity [ 'latitude' ] . ' ' . $activity [ 'longitude' ];
2019-03-17 14:50:14 +01:00
}
$item [ 'app' ] = $activity [ 'generator' ];
return $item ;
}
2020-04-20 14:19:26 +02:00
/**
* Store hashtags and mentions
*
* @ param array $item
*/
private static function storeFromBody ( array $item )
2020-04-20 11:47:26 +02:00
{
// Make sure to delete all existing tags (can happen when called via the update functionality)
2020-04-20 14:19:26 +02:00
DBA :: delete ( 'post-tag' , [ 'uri-id' => $item [ 'uri-id' ]]);
2020-04-20 11:47:26 +02:00
Tag :: storeFromBody ( $item [ 'uri-id' ], $item [ 'body' ], '@!' );
}
2020-01-19 15:33:16 +01:00
/**
* Generate a GUID out of an URL
*
* @ param string $url message URL
* @ return string with GUID
*/
private static function getGUIDByURL ( string $url )
{
$parsed = parse_url ( $url );
$host_hash = hash ( 'crc32' , $parsed [ 'host' ]);
unset ( $parsed [ " scheme " ]);
unset ( $parsed [ " host " ]);
$path = implode ( " / " , $parsed );
return $host_hash . '-' . hash ( 'fnv164' , $path ) . '-' . hash ( 'joaat' , $path );
}
2018-10-03 08:15:07 +02:00
/**
2018-10-07 20:41:45 +02:00
* Creates an item post
2018-10-03 08:15:07 +02:00
*
2019-01-06 22:06:53 +01:00
* @ param array $activity Activity data
* @ param array $item item array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2020-07-20 06:37:43 +02:00
public static function postItem ( array $activity , array $item )
2018-10-03 08:15:07 +02:00
{
2020-07-21 21:55:24 +02:00
if ( empty ( $item )) {
return ;
}
2019-02-10 19:42:51 +01:00
$stored = false ;
2020-09-25 08:47:07 +02:00
ksort ( $activity [ 'receiver' ]);
2019-02-10 19:42:51 +01:00
2018-10-03 08:15:07 +02:00
foreach ( $activity [ 'receiver' ] as $receiver ) {
2020-03-02 08:57:23 +01:00
if ( $receiver == - 1 ) {
continue ;
}
2018-10-03 08:15:07 +02:00
$item [ 'uid' ] = $receiver ;
2019-07-16 07:07:26 +02:00
2020-09-12 19:45:04 +02:00
$type = $activity [ 'reception_type' ][ $receiver ] ? ? Receiver :: TARGET_UNKNOWN ;
switch ( $type ) {
case Receiver :: TARGET_TO :
$item [ 'post-type' ] = Item :: PT_TO ;
break ;
case Receiver :: TARGET_CC :
$item [ 'post-type' ] = Item :: PT_CC ;
break ;
case Receiver :: TARGET_BTO :
$item [ 'post-type' ] = Item :: PT_BTO ;
break ;
case Receiver :: TARGET_BCC :
$item [ 'post-type' ] = Item :: PT_BCC ;
break ;
case Receiver :: TARGET_FOLLOWER :
$item [ 'post-type' ] = Item :: PT_FOLLOWER ;
break ;
2020-09-13 16:15:28 +02:00
case Receiver :: TARGET_ANSWER :
$item [ 'post-type' ] = Item :: PT_COMMENT ;
break ;
2020-09-14 19:48:57 +02:00
case Receiver :: TARGET_GLOBAL :
$item [ 'post-type' ] = Item :: PT_GLOBAL ;
break ;
2020-09-12 19:45:04 +02:00
default :
$item [ 'post-type' ] = Item :: PT_ARTICLE ;
}
2020-09-25 08:47:07 +02:00
if ( ! empty ( $activity [ 'from-relay' ])) {
$item [ 'post-type' ] = Item :: PT_RELAY ;
} elseif ( ! empty ( $activity [ 'thread-completion' ])) {
$item [ 'post-type' ] = Item :: PT_FETCHED ;
2020-09-21 14:31:20 +02:00
}
2020-07-20 06:37:43 +02:00
if ( $item [ 'isForum' ] ? ? false ) {
2020-08-07 15:49:59 +02:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'actor' ], $receiver );
2019-07-30 15:08:14 +02:00
} else {
2020-08-07 15:49:59 +02:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'author' ], $receiver );
2019-07-16 07:07:26 +02:00
}
2018-10-03 08:15:07 +02:00
2019-08-02 02:39:42 +02:00
if (( $receiver != 0 ) && empty ( $item [ 'contact-id' ])) {
2020-08-07 15:49:59 +02:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'author' ]);
2018-10-03 08:15:07 +02:00
}
2019-05-16 07:44:59 +02:00
if ( ! empty ( $activity [ 'directmessage' ])) {
self :: postMail ( $activity , $item );
continue ;
}
2020-01-18 16:50:57 +01:00
if ( DI :: pConfig () -> get ( $receiver , 'system' , 'accept_only_sharer' , false ) && ( $receiver != 0 ) && ( $item [ 'gravity' ] == GRAVITY_PARENT )) {
2019-07-17 23:37:13 +02:00
$skip = ! Contact :: isSharingByURL ( $activity [ 'author' ], $receiver );
2020-07-20 06:37:43 +02:00
if ( $skip && (( $activity [ 'type' ] == 'as:Announce' ) || ( $item [ 'isForum' ] ? ? false ))) {
2019-07-17 23:37:13 +02:00
$skip = ! Contact :: isSharingByURL ( $activity [ 'actor' ], $receiver );
}
if ( $skip ) {
Logger :: info ( 'Skipping post' , [ 'uid' => $receiver , 'url' => $item [ 'uri' ]]);
continue ;
}
Logger :: info ( 'Accepting post' , [ 'uid' => $receiver , 'url' => $item [ 'uri' ]]);
}
2020-03-28 15:02:49 +01:00
if (( $item [ 'gravity' ] != GRAVITY_ACTIVITY ) && ( $activity [ 'object_type' ] == 'as:Event' )) {
2018-10-26 06:13:26 +02:00
self :: createEvent ( $activity , $item );
}
2018-10-03 08:15:07 +02:00
$item_id = Item :: insert ( $item );
2019-02-23 05:00:16 +01:00
if ( $item_id ) {
Logger :: info ( 'Item insertion successful' , [ 'user' => $item [ 'uid' ], 'item_id' => $item_id ]);
} else {
Logger :: notice ( 'Item insertion aborted' , [ 'user' => $item [ 'uid' ]]);
}
2019-02-10 19:42:51 +01:00
2019-02-10 19:59:05 +01:00
if ( $item [ 'uid' ] == 0 ) {
$stored = $item_id ;
2019-02-10 19:42:51 +01:00
}
2018-10-03 08:15:07 +02:00
}
2019-01-30 22:33:23 +01:00
2019-02-10 19:42:51 +01:00
// Store send a follow request for every reshare - but only when the item had been stored
2020-03-02 08:57:23 +01:00
if ( $stored && ( $item [ 'private' ] != Item :: PRIVATE ) && ( $item [ 'gravity' ] == GRAVITY_PARENT ) && ( $item [ 'author-link' ] != $item [ 'owner-link' ])) {
2019-01-30 22:33:23 +01:00
$author = APContact :: getByURL ( $item [ 'owner-link' ], false );
// We send automatic follow requests for reshared messages. (We don't need though for forum posts)
if ( $author [ 'type' ] != 'Group' ) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Send follow request' , [ 'uri' => $item [ 'uri' ], 'stored' => $stored , 'to' => $item [ 'author-link' ]]);
2019-01-30 22:33:23 +01:00
ActivityPub\Transmitter :: sendFollowObject ( $item [ 'uri' ], $item [ 'author-link' ]);
}
}
2018-10-03 08:15:07 +02:00
}
2020-04-15 22:59:45 +02:00
/**
* Store tags and mentions into the tag table
*
* @ param integer $uriid
* @ param array $tags
*/
2020-04-14 19:18:48 +02:00
private static function storeTags ( int $uriid , array $tags = null )
2020-04-14 01:54:28 +02:00
{
foreach ( $tags as $tag ) {
if ( empty ( $tag [ 'name' ]) || empty ( $tag [ 'type' ]) || ! in_array ( $tag [ 'type' ], [ 'Mention' , 'Hashtag' ])) {
continue ;
}
2020-04-17 08:35:20 +02:00
$hash = substr ( $tag [ 'name' ], 0 , 1 );
2020-04-14 01:54:28 +02:00
if ( $tag [ 'type' ] == 'Mention' ) {
2020-04-17 08:35:20 +02:00
if ( in_array ( $hash , [ Tag :: TAG_CHARACTER [ Tag :: MENTION ],
Tag :: TAG_CHARACTER [ Tag :: EXCLUSIVE_MENTION ],
Tag :: TAG_CHARACTER [ Tag :: IMPLICIT_MENTION ]])) {
$tag [ 'name' ] = substr ( $tag [ 'name' ], 1 );
2020-04-14 01:54:28 +02:00
}
2020-04-20 07:43:13 +02:00
$type = Tag :: IMPLICIT_MENTION ;
2020-04-17 08:35:20 +02:00
2020-04-15 22:45:04 +02:00
if ( ! empty ( $tag [ 'href' ])) {
$apcontact = APContact :: getByURL ( $tag [ 'href' ]);
2020-04-15 22:52:30 +02:00
if ( ! empty ( $apcontact [ 'name' ]) || ! empty ( $apcontact [ 'nick' ])) {
2020-04-17 08:35:20 +02:00
$tag [ 'name' ] = $apcontact [ 'name' ] ? : $apcontact [ 'nick' ];
2020-04-15 22:45:04 +02:00
}
}
2020-04-14 01:54:28 +02:00
} elseif ( $tag [ 'type' ] == 'Hashtag' ) {
2020-04-20 07:43:13 +02:00
if ( $hash == Tag :: TAG_CHARACTER [ Tag :: HASHTAG ]) {
2020-04-17 08:35:20 +02:00
$tag [ 'name' ] = substr ( $tag [ 'name' ], 1 );
2020-04-14 01:54:28 +02:00
}
2020-04-20 07:43:13 +02:00
$type = Tag :: HASHTAG ;
2020-04-14 01:54:28 +02:00
}
2020-04-17 08:35:20 +02:00
if ( empty ( $tag [ 'name' ])) {
2020-04-14 01:54:28 +02:00
continue ;
}
2020-04-20 11:47:26 +02:00
2020-04-20 07:43:13 +02:00
Tag :: store ( $uriid , $type , $tag [ 'name' ], $tag [ 'href' ]);
2020-04-14 01:54:28 +02:00
}
}
2019-05-16 07:44:59 +02:00
/**
* Creates an mail post
*
* @ param array $activity Activity data
* @ param array $item item array
2019-06-13 03:01:44 +02:00
* @ return int | bool New mail table row id or false on error
2019-05-16 07:44:59 +02:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function postMail ( $activity , $item )
{
if (( $item [ 'gravity' ] != GRAVITY_PARENT ) && ! DBA :: exists ( 'mail' , [ 'uri' => $item [ 'thr-parent' ], 'uid' => $item [ 'uid' ]])) {
Logger :: info ( 'Parent not found, mail will be discarded.' , [ 'uid' => $item [ 'uid' ], 'uri' => $item [ 'thr-parent' ]]);
return false ;
}
Logger :: info ( 'Direct Message' , $item );
$msg = [];
$msg [ 'uid' ] = $item [ 'uid' ];
$msg [ 'contact-id' ] = $item [ 'contact-id' ];
$contact = Contact :: getById ( $item [ 'contact-id' ], [ 'name' , 'url' , 'photo' ]);
$msg [ 'from-name' ] = $contact [ 'name' ];
$msg [ 'from-url' ] = $contact [ 'url' ];
$msg [ 'from-photo' ] = $contact [ 'photo' ];
$msg [ 'uri' ] = $item [ 'uri' ];
$msg [ 'created' ] = $item [ 'created' ];
$parent = DBA :: selectFirst ( 'mail' , [ 'parent-uri' , 'title' ], [ 'uri' => $item [ 'thr-parent' ]]);
if ( DBA :: isResult ( $parent )) {
$msg [ 'parent-uri' ] = $parent [ 'parent-uri' ];
$msg [ 'title' ] = $parent [ 'title' ];
} else {
$msg [ 'parent-uri' ] = $item [ 'thr-parent' ];
if ( ! empty ( $item [ 'title' ])) {
$msg [ 'title' ] = $item [ 'title' ];
} elseif ( ! empty ( $item [ 'content-warning' ])) {
$msg [ 'title' ] = $item [ 'content-warning' ];
} else {
// Trying to generate a title out of the body
$title = $item [ 'body' ];
while ( preg_match ( '#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is' , $title , $matches )) {
$title = $matches [ 3 ];
}
2020-05-16 18:28:15 +02:00
$title = trim ( HTML :: toPlaintext ( BBCode :: convert ( $title , false , BBCode :: API , true ), 0 ));
2019-05-16 07:44:59 +02:00
if ( strlen ( $title ) > 20 ) {
$title = substr ( $title , 0 , 20 ) . '...' ;
}
$msg [ 'title' ] = $title ;
}
}
$msg [ 'body' ] = $item [ 'body' ];
2019-06-13 03:01:44 +02:00
return Mail :: insert ( $msg );
2019-05-16 07:44:59 +02:00
}
2018-10-03 08:15:07 +02:00
/**
2018-10-07 20:41:45 +02:00
* Fetches missing posts
2018-10-03 08:15:07 +02:00
*
2020-09-22 07:36:01 +02:00
* @ param string $url message URL
* @ param array $child activity array with the child of this message
* @ param string $relay_actor Relay actor
2020-01-20 23:30:34 +01:00
* @ return string fetched message URL
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-03 08:15:07 +02:00
*/
2020-09-22 07:36:01 +02:00
public static function fetchMissingActivity ( string $url , array $child = [], string $relay_actor = '' )
2018-10-03 08:15:07 +02:00
{
2019-07-17 21:36:32 +02:00
if ( ! empty ( $child [ 'receiver' ])) {
$uid = ActivityPub\Receiver :: getFirstUserFromReceivers ( $child [ 'receiver' ]);
} else {
$uid = 0 ;
}
2018-11-03 22:37:08 +01:00
$object = ActivityPub :: fetchContent ( $url , $uid );
2018-10-03 08:15:07 +02:00
if ( empty ( $object )) {
2018-10-29 22:20:46 +01:00
Logger :: log ( 'Activity ' . $url . ' was not fetchable, aborting.' );
2020-01-20 23:30:34 +01:00
return '' ;
2018-10-03 08:15:07 +02:00
}
2018-12-29 10:53:31 +01:00
if ( empty ( $object [ 'id' ])) {
Logger :: log ( 'Activity ' . $url . ' has got not id, aborting. ' . json_encode ( $object ));
2020-01-20 23:30:34 +01:00
return '' ;
2018-12-29 10:53:31 +01:00
}
2020-09-14 22:58:41 +02:00
if ( ! empty ( $object [ 'actor' ])) {
$object_actor = $object [ 'actor' ];
2019-07-17 21:36:32 +02:00
} elseif ( ! empty ( $object [ 'attributedTo' ])) {
2020-09-14 22:58:41 +02:00
$object_actor = $object [ 'attributedTo' ];
2019-07-17 21:36:32 +02:00
} else {
// Shouldn't happen
2020-09-14 22:58:41 +02:00
$object_actor = '' ;
}
$signer = [ $object_actor ];
if ( ! empty ( $child [ 'author' ])) {
$actor = $child [ 'author' ];
$signer [] = $actor ;
} else {
$actor = $object_actor ;
2019-07-17 21:36:32 +02:00
}
if ( ! empty ( $object [ 'published' ])) {
$published = $object [ 'published' ];
} elseif ( ! empty ( $child [ 'published' ])) {
$published = $child [ 'published' ];
} else {
$published = DateTimeFormat :: utcNow ();
}
2018-10-03 08:15:07 +02:00
$activity = [];
$activity [ '@context' ] = $object [ '@context' ];
unset ( $object [ '@context' ]);
$activity [ 'id' ] = $object [ 'id' ];
2019-10-16 14:35:14 +02:00
$activity [ 'to' ] = $object [ 'to' ] ? ? [];
$activity [ 'cc' ] = $object [ 'cc' ] ? ? [];
2019-07-17 21:36:32 +02:00
$activity [ 'actor' ] = $actor ;
2018-10-03 08:15:07 +02:00
$activity [ 'object' ] = $object ;
2019-07-17 21:36:32 +02:00
$activity [ 'published' ] = $published ;
2018-10-03 08:15:07 +02:00
$activity [ 'type' ] = 'Create' ;
2018-10-07 17:34:51 +02:00
$ldactivity = JsonLD :: compact ( $activity );
2018-10-09 07:04:24 +02:00
2020-09-25 14:16:08 +02:00
if ( ! empty ( $relay_actor )) {
$ldactivity [ 'thread-completion' ] = $ldactivity [ 'from-relay' ] = Contact :: getIdForURL ( $relay_actor );
} elseif ( ! empty ( $child [ 'thread-completion' ])) {
$ldactivity [ 'thread-completion' ] = $child [ 'thread-completion' ];
} else {
$ldactivity [ 'thread-completion' ] = Contact :: getIdForURL ( $actor );
}
2018-10-09 07:04:24 +02:00
2020-09-22 17:48:44 +02:00
if ( ! empty ( $relay_actor ) && ! self :: acceptIncomingMessage ( $ldactivity , $object [ 'id' ])) {
return '' ;
}
2020-09-14 22:58:41 +02:00
ActivityPub\Receiver :: processActivity ( $ldactivity , json_encode ( $activity ), $uid , true , false , $signer );
2020-03-03 23:43:19 +01:00
2020-01-20 23:30:34 +01:00
Logger :: notice ( 'Activity had been fetched and processed.' , [ 'url' => $url , 'object' => $activity [ 'id' ]]);
2019-07-21 09:37:50 +02:00
2020-01-20 23:30:34 +01:00
return $activity [ 'id' ];
2018-10-03 08:15:07 +02:00
}
2020-09-22 17:48:44 +02:00
/**
* Test if incoming relay messages should be accepted
*
* @ param array $activity activity array
* @ param string $id object ID
* @ return boolean true if message is accepted
*/
private static function acceptIncomingMessage ( array $activity , string $id )
{
if ( empty ( $activity [ 'as:object' ])) {
Logger :: info ( 'No object field in activity - accepted' , [ 'id' => $id ]);
return true ;
}
$config = DI :: config ();
$subscribe = $config -> get ( 'system' , 'relay_subscribe' , false );
if ( $subscribe ) {
$scope = $config -> get ( 'system' , 'relay_scope' , SR_SCOPE_ALL );
} else {
$scope = SR_SCOPE_NONE ;
}
$replyto = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:inReplyTo' , '@id' );
if ( Item :: exists ([ 'uri' => $replyto ])) {
Logger :: info ( 'Post is a reply to an existing post - accepted' , [ 'id' => $id , 'replyto' => $replyto ]);
return true ;
}
if ( $scope == SR_SCOPE_NONE ) {
Logger :: info ( 'Server does not accept relay posts - rejected' , [ 'id' => $id ]);
return false ;
}
$messageTags = [];
$tags = Receiver :: processTags ( JsonLD :: fetchElementArray ( $activity [ 'as:object' ], 'as:tag' ) ? ? []);
if ( ! empty ( $tags )) {
foreach ( $tags as $tag ) {
if ( $tag [ 'type' ] != 'Hashtag' ) {
continue ;
}
$messageTags [] = ltrim ( mb_strtolower ( $tag [ 'name' ]), '#' );
}
}
$systemTags = [];
$userTags = [];
2020-09-29 21:48:26 +02:00
$denyTags = [];
2020-09-22 17:48:44 +02:00
if ( $scope == SR_SCOPE_TAGS ) {
2020-09-29 21:48:26 +02:00
$server_tags = $config -> get ( 'system' , 'relay_server_tags' );
2020-09-22 17:48:44 +02:00
$tagitems = explode ( ',' , mb_strtolower ( $server_tags ));
foreach ( $tagitems AS $tag ) {
$systemTags [] = trim ( $tag , '# ' );
}
if ( $config -> get ( 'system' , 'relay_user_tags' )) {
$userTags = Search :: getUserTags ();
}
}
$tagList = array_unique ( array_merge ( $systemTags , $userTags ));
2020-09-29 21:48:26 +02:00
$deny_tags = $config -> get ( 'system' , 'relay_deny_tags' );
$tagitems = explode ( ',' , mb_strtolower ( $deny_tags ));
foreach ( $tagitems AS $tag ) {
$tag = trim ( $tag , '# ' );
$denyTags [] = $tag ;
}
if ( ! empty ( $tagList ) || ! empty ( $denyTags )) {
$content = mb_strtolower ( BBCode :: toPlaintext ( HTML :: toBBCode ( JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:content' , '@value' )), false ));
foreach ( $messageTags as $tag ) {
if ( in_array ( $tag , $denyTags )) {
Logger :: info ( 'Unwanted hashtag found - rejected' , [ 'id' => $id , 'hashtag' => $tag ]);
return false ;
}
if ( in_array ( $tag , $tagList )) {
Logger :: info ( 'Subscribed hashtag found - accepted' , [ 'id' => $id , 'hashtag' => $tag ]);
return true ;
}
// We check with "strpos" for performance issues. Only when this is true, the regular expression check is used
// RegExp is taken from here: https://medium.com/@shiba1014/regex-word-boundaries-with-unicode-207794f6e7ed
if (( strpos ( $content , $tag ) !== false ) && preg_match ( '/(?<=[\s,.:;"\']|^)' . preg_quote ( $tag , '/' ) . '(?=[\s,.:;"\']|$)/' , $content )) {
Logger :: info ( 'Subscribed hashtag found in content - accepted' , [ 'id' => $id , 'hashtag' => $tag ]);
return true ;
}
2020-09-22 17:48:44 +02:00
}
}
2020-09-29 21:48:26 +02:00
if ( $scope == SR_SCOPE_ALL ) {
Logger :: info ( 'Server accept all posts - accepted' , [ 'id' => $id ]);
return true ;
}
2020-09-22 17:48:44 +02:00
Logger :: info ( 'No matching hashtags found - rejected' , [ 'id' => $id ]);
return false ;
}
2018-10-03 08:15:07 +02:00
/**
2018-10-06 06:18:40 +02:00
* perform a " follow " request
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function followUser ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 21:42:04 +02:00
$uid = User :: getIdForURL ( $activity [ 'object_id' ]);
2018-10-03 08:15:07 +02:00
if ( empty ( $uid )) {
return ;
}
$owner = User :: getOwnerDataById ( $uid );
2020-08-22 18:34:04 +02:00
if ( empty ( $owner )) {
return ;
}
2018-10-03 08:15:07 +02:00
2018-10-07 19:35:43 +02:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 08:15:07 +02:00
if ( ! empty ( $cid )) {
2018-10-09 21:58:15 +02:00
self :: switchContact ( $cid );
2019-05-05 13:02:19 +02:00
DBA :: update ( 'contact' , [ 'hub-verify' => $activity [ 'id' ], 'protocol' => Protocol :: ACTIVITYPUB ], [ 'id' => $cid ]);
2018-10-05 08:35:50 +02:00
$contact = DBA :: selectFirst ( 'contact' , [], [ 'id' => $cid , 'network' => Protocol :: NATIVE_SUPPORT ]);
2018-10-03 08:15:07 +02:00
} else {
2019-05-20 00:43:19 +02:00
$contact = [];
2018-10-03 08:15:07 +02:00
}
2018-10-07 19:35:43 +02:00
$item = [ 'author-id' => Contact :: getIdForURL ( $activity [ 'actor' ]),
'author-link' => $activity [ 'actor' ]];
2018-10-03 08:15:07 +02:00
2019-10-16 14:35:14 +02:00
$note = Strings :: escapeTags ( trim ( $activity [ 'content' ] ? ? '' ));
2019-05-05 11:17:45 +02:00
2018-10-13 20:13:01 +02:00
// Ensure that the contact has got the right network type
self :: switchContact ( $item [ 'author-id' ]);
2019-05-20 00:43:19 +02:00
$result = Contact :: addRelationship ( $owner , $contact , $item , false , $note );
2019-05-20 22:33:09 +02:00
if ( $result === true ) {
2020-07-10 07:30:12 +02:00
ActivityPub\Transmitter :: sendContactAccept ( $item [ 'author-link' ], $activity [ 'id' ], $owner [ 'uid' ]);
2019-05-20 00:46:29 +02:00
}
2018-10-07 19:35:43 +02:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 08:15:07 +02:00
if ( empty ( $cid )) {
return ;
}
2019-01-09 23:30:26 +01:00
if ( empty ( $contact )) {
2019-05-05 13:02:19 +02:00
DBA :: update ( 'contact' , [ 'hub-verify' => $activity [ 'id' ], 'protocol' => Protocol :: ACTIVITYPUB ], [ 'id' => $cid ]);
2019-01-09 23:30:26 +01:00
}
2018-10-29 22:20:46 +01:00
Logger :: log ( 'Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity [ 'id' ]);
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Update the given profile
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Exception
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function updatePerson ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 20:41:45 +02:00
if ( empty ( $activity [ 'object_id' ])) {
2018-10-03 08:15:07 +02:00
return ;
}
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Updating profile' , [ 'object' => $activity [ 'object_id' ]]);
2020-08-06 20:53:45 +02:00
Contact :: updateFromProbeByURL ( $activity [ 'object_id' ]);
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Delete the given profile
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function deletePerson ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 21:42:04 +02:00
if ( empty ( $activity [ 'object_id' ]) || empty ( $activity [ 'actor' ])) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Empty object id or actor.' );
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-07 21:42:04 +02:00
if ( $activity [ 'object_id' ] != $activity [ 'actor' ]) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Object id does not match actor.' );
2018-10-03 08:15:07 +02:00
return ;
}
2018-11-08 17:28:29 +01:00
$contacts = DBA :: select ( 'contact' , [ 'id' ], [ 'nurl' => Strings :: normaliseLink ( $activity [ 'object_id' ])]);
2018-10-03 08:15:07 +02:00
while ( $contact = DBA :: fetch ( $contacts )) {
2018-10-07 17:34:51 +02:00
Contact :: remove ( $contact [ 'id' ]);
2018-10-03 08:15:07 +02:00
}
DBA :: close ( $contacts );
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Deleted contact' , [ 'object' => $activity [ 'object_id' ]]);
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Accept a follow request
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function acceptFollowUser ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 20:41:45 +02:00
$uid = User :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 08:15:07 +02:00
if ( empty ( $uid )) {
return ;
}
2018-10-07 19:35:43 +02:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 08:15:07 +02:00
if ( empty ( $cid )) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-09 21:58:15 +02:00
self :: switchContact ( $cid );
2018-10-03 08:15:07 +02:00
$fields = [ 'pending' => false ];
$contact = DBA :: selectFirst ( 'contact' , [ 'rel' ], [ 'id' => $cid ]);
if ( $contact [ 'rel' ] == Contact :: FOLLOWER ) {
$fields [ 'rel' ] = Contact :: FRIEND ;
}
$condition = [ 'id' => $cid ];
DBA :: update ( 'contact' , $fields , $condition );
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Accept contact request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Reject a follow request
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function rejectFollowUser ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 20:41:45 +02:00
$uid = User :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 08:15:07 +02:00
if ( empty ( $uid )) {
return ;
}
2018-10-07 19:35:43 +02:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 08:15:07 +02:00
if ( empty ( $cid )) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-09 21:58:15 +02:00
self :: switchContact ( $cid );
2019-05-02 22:04:15 +02:00
if ( DBA :: exists ( 'contact' , [ 'id' => $cid , 'rel' => Contact :: SHARING ])) {
2018-10-03 08:15:07 +02:00
Contact :: remove ( $cid );
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Rejected contact request - contact removed' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 08:15:07 +02:00
} else {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Rejected contact request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 08:15:07 +02:00
}
}
/**
2018-10-06 06:18:40 +02:00
* Undo activity like " like " or " dislike "
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function undoActivity ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 20:41:45 +02:00
if ( empty ( $activity [ 'object_id' ])) {
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-07 20:41:45 +02:00
if ( empty ( $activity [ 'object_actor' ])) {
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-07 20:41:45 +02:00
$author_id = Contact :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 08:15:07 +02:00
if ( empty ( $author_id )) {
return ;
}
2020-03-03 07:47:28 +01:00
Item :: markForDeletion ([ 'uri' => $activity [ 'object_id' ], 'author-id' => $author_id , 'gravity' => GRAVITY_ACTIVITY ]);
2018-10-03 08:15:07 +02:00
}
/**
2018-10-06 06:18:40 +02:00
* Activity to remove a follower
2018-10-03 08:15:07 +02:00
*
* @ param array $activity
2019-01-06 22:06:53 +01:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 08:15:07 +02:00
*/
2018-10-03 11:15:38 +02:00
public static function undoFollowUser ( $activity )
2018-10-03 08:15:07 +02:00
{
2018-10-07 20:41:45 +02:00
$uid = User :: getIdForURL ( $activity [ 'object_object' ]);
2018-10-03 08:15:07 +02:00
if ( empty ( $uid )) {
return ;
}
$owner = User :: getOwnerDataById ( $uid );
2020-08-22 18:34:04 +02:00
if ( empty ( $owner )) {
return ;
}
2018-10-03 08:15:07 +02:00
2018-10-07 19:35:43 +02:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 08:15:07 +02:00
if ( empty ( $cid )) {
2020-06-28 19:50:11 +02:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 08:15:07 +02:00
return ;
}
2018-10-09 21:58:15 +02:00
self :: switchContact ( $cid );
2018-10-03 08:15:07 +02:00
$contact = DBA :: selectFirst ( 'contact' , [], [ 'id' => $cid ]);
if ( ! DBA :: isResult ( $contact )) {
return ;
}
Contact :: removeFollower ( $owner , $contact );
2020-06-28 19:50:11 +02:00
Logger :: info ( 'Undo following request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 08:15:07 +02:00
}
2018-10-09 21:58:15 +02:00
/**
* Switches a contact to AP if needed
*
* @ param integer $cid Contact ID
2019-01-06 22:06:53 +01:00
* @ throws \Exception
2018-10-09 21:58:15 +02:00
*/
private static function switchContact ( $cid )
{
2020-01-16 07:43:21 +01:00
$contact = DBA :: selectFirst ( 'contact' , [ 'network' , 'url' ], [ 'id' => $cid ]);
if ( ! DBA :: isResult ( $contact ) || in_array ( $contact [ 'network' ], [ Protocol :: ACTIVITYPUB , Protocol :: DFRN ]) || Contact :: isLocal ( $contact [ 'url' ])) {
2018-10-09 21:58:15 +02:00
return ;
}
2020-01-16 07:43:21 +01:00
Logger :: info ( 'Change existing contact' , [ 'cid' => $cid , 'previous' => $contact [ 'network' ]]);
Contact :: updateFromProbe ( $cid );
2018-10-09 21:58:15 +02:00
}
2019-02-09 04:57:35 +01:00
/**
* Collects implicit mentions like :
* - the author of the parent item
* - all the mentioned conversants in the parent item
*
* @ param array $parent Item array with at least [ 'id' , 'author-link' , 'alias' ]
* @ return array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function getImplicitMentionList ( array $parent )
{
2020-05-01 08:01:22 +02:00
$parent_terms = Tag :: getByURIId ( $parent [ 'uri-id' ], [ Tag :: MENTION , Tag :: IMPLICIT_MENTION , Tag :: EXCLUSIVE_MENTION ]);
2019-02-23 05:42:04 +01:00
2020-07-15 19:06:48 +02:00
$parent_author = Contact :: getByURL ( $parent [ 'author-link' ], false , [ 'url' , 'nurl' , 'alias' ]);
2019-02-09 04:57:35 +01:00
2019-02-24 21:48:56 +01:00
$implicit_mentions = [];
2020-06-16 22:41:34 +02:00
if ( empty ( $parent_author [ 'url' ])) {
2019-02-24 21:48:56 +01:00
Logger :: notice ( 'Author public contact unknown.' , [ 'author-link' => $parent [ 'author-link' ], 'item-id' => $parent [ 'id' ]]);
2019-02-25 09:16:18 +01:00
} else {
2019-02-24 21:48:56 +01:00
$implicit_mentions [] = $parent_author [ 'url' ];
$implicit_mentions [] = $parent_author [ 'nurl' ];
$implicit_mentions [] = $parent_author [ 'alias' ];
}
2019-02-09 04:57:35 +01:00
2019-02-24 21:48:56 +01:00
if ( ! empty ( $parent [ 'alias' ])) {
2019-02-09 04:57:35 +01:00
$implicit_mentions [] = $parent [ 'alias' ];
}
foreach ( $parent_terms as $term ) {
2020-07-15 19:06:48 +02:00
$contact = Contact :: getByURL ( $term [ 'url' ], false , [ 'url' , 'nurl' , 'alias' ]);
2020-06-16 22:41:34 +02:00
if ( ! empty ( $contact [ 'url' ])) {
2019-02-10 19:42:51 +01:00
$implicit_mentions [] = $contact [ 'url' ];
$implicit_mentions [] = $contact [ 'nurl' ];
$implicit_mentions [] = $contact [ 'alias' ];
}
2019-02-09 04:57:35 +01:00
}
return $implicit_mentions ;
}
/**
* Strips from the body prepended implicit mentions
*
* @ param string $body
2020-05-09 10:55:10 +02:00
* @ param array $parent
2019-02-09 04:57:35 +01:00
* @ return string
*/
2020-05-09 10:55:10 +02:00
private static function removeImplicitMentionsFromBody ( string $body , array $parent )
2019-02-09 04:57:35 +01:00
{
2020-01-19 21:21:13 +01:00
if ( DI :: config () -> get ( 'system' , 'disable_implicit_mentions' )) {
2019-02-13 18:26:54 +01:00
return $body ;
2019-02-13 18:23:23 +01:00
}
2020-05-09 10:55:10 +02:00
$potential_mentions = self :: getImplicitMentionList ( $parent );
2019-02-09 04:57:35 +01:00
$kept_mentions = [];
// Extract one prepended mention at a time from the body
2019-03-09 15:04:11 +01:00
while ( preg_match ( '#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is' , $body , $matches )) {
2019-10-16 14:58:09 +02:00
if ( ! in_array ( $matches [ 2 ], $potential_mentions )) {
2019-02-09 04:57:35 +01:00
$kept_mentions [] = $matches [ 1 ];
}
$body = $matches [ 3 ];
}
// Re-appending the kept mentions to the body after extraction
$kept_mentions [] = $body ;
return implode ( '' , $kept_mentions );
}
2018-10-03 08:15:07 +02:00
}