2018-10-03 02:15:07 -04:00
< ? php
/**
2022-01-02 02:27:47 -05:00
* @ copyright Copyright ( C ) 2010 - 2022 , the Friendica project
2020-02-09 10:18:46 -05: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 02:15:07 -04:00
*/
2020-02-09 10:18:46 -05:00
2018-10-03 02:15:07 -04:00
namespace Friendica\Protocol\ActivityPub ;
2019-05-16 01:44:59 -04:00
use Friendica\Content\Text\BBCode ;
2018-11-08 11:28:29 -05:00
use Friendica\Content\Text\HTML ;
2021-03-06 03:43:25 -05:00
use Friendica\Content\Text\Markdown ;
2018-10-29 17:20:46 -04:00
use Friendica\Core\Logger ;
2018-10-03 02:15:07 -04:00
use Friendica\Core\Protocol ;
2021-12-11 10:17:34 -05:00
use Friendica\Core\System ;
2019-10-24 18:10:20 -04:00
use Friendica\Database\DBA ;
2020-01-18 10:50:57 -05:00
use Friendica\DI ;
2018-10-03 02:15:07 -04:00
use Friendica\Model\APContact ;
2019-10-24 18:10:20 -04:00
use Friendica\Model\Contact ;
2020-02-28 04:46:53 -05:00
use Friendica\Model\Conversation ;
2018-10-26 00:13:26 -04:00
use Friendica\Model\Event ;
2021-03-10 17:31:33 -05:00
use Friendica\Model\GServer ;
2019-10-24 18:10:20 -04:00
use Friendica\Model\Item ;
2020-04-13 19:54:28 -04:00
use Friendica\Model\ItemURI ;
2019-10-24 18:10:20 -04:00
use Friendica\Model\Mail ;
2020-04-17 02:35:20 -04:00
use Friendica\Model\Tag ;
2018-10-03 02:15:07 -04:00
use Friendica\Model\User ;
2020-10-29 01:20:26 -04:00
use Friendica\Model\Post ;
2019-10-23 18:25:43 -04:00
use Friendica\Protocol\Activity ;
2018-10-03 02:15:07 -04:00
use Friendica\Protocol\ActivityPub ;
2020-09-30 13:37:46 -04:00
use Friendica\Protocol\Relay ;
2018-10-27 02:17:17 -04:00
use Friendica\Util\DateTimeFormat ;
2018-11-08 11:28:29 -05:00
use Friendica\Util\JsonLD ;
use Friendica\Util\Strings ;
2018-10-03 02:15:07 -04:00
/**
2018-10-24 00:51:37 -04:00
* ActivityPub Processor Protocol class
2018-10-03 02:15:07 -04:00
*/
class Processor
{
/**
2021-08-15 22:53:42 -04:00
* Extracts the tag character ( #, @, !) from mention links
2018-10-03 02:15:07 -04:00
*
* @ param string $body
2021-08-15 22:53:42 -04:00
* @ return string
2018-10-03 02:15:07 -04:00
*/
2021-08-15 22:53:42 -04:00
protected static function normalizeMentionLinks ( string $body ) : string
2018-10-03 02:15:07 -04:00
{
2021-08-15 22:53:42 -04:00
return preg_replace ( '%\[url=([^\[\]]*)]([#@!])(.*?)\[/url]%ism' , '$2[url=$1]$3[/url]' , $body );
2018-10-03 02:15:07 -04:00
}
2021-03-06 03:43:25 -05:00
/**
* Convert the language array into a language JSON
*
* @ param array $languages
* @ return string language JSON
*/
private static function processLanguages ( array $languages )
{
$codes = array_keys ( $languages );
$lang = [];
foreach ( $codes as $code ) {
$lang [ $code ] = 1 ;
}
if ( empty ( $lang )) {
return '' ;
}
return json_encode ( $lang );
}
2018-11-07 15:34:03 -05:00
/**
* Replaces emojis in the body
*
* @ param array $emojis
* @ param string $body
*
* @ return string with replaced emojis
*/
2021-10-03 05:42:14 -04:00
private static function replaceEmojis ( int $uri_id , $body , array $emojis )
2018-11-07 15:34:03 -05:00
{
2020-11-11 18:28:26 -05:00
$body = strtr ( $body ,
array_combine (
array_column ( $emojis , 'name' ),
array_map ( function ( $emoji ) {
2021-10-17 21:29:54 -04:00
return '[emoji=' . $emoji [ 'href' ] . ']' . $emoji [ 'name' ] . '[/emoji]' ;
2020-11-11 18:28:26 -05:00
}, $emojis )
)
);
2021-10-03 05:42:14 -04:00
// We store the emoji here to be able to avoid storing it in the media
foreach ( $emojis as $emoji ) {
Post\Link :: getByLink ( $uri_id , $emoji [ 'href' ]);
}
2018-11-07 15:34:03 -05:00
return $body ;
}
2020-10-29 01:20:26 -04:00
/**
* Store attached media files in the post - media table
*
* @ param int $uriid
* @ param array $attachment
* @ return void
*/
2020-10-29 05:03:06 -04:00
private static function storeAttachmentAsMedia ( int $uriid , array $attachment )
2020-10-29 01:20:26 -04:00
{
if ( empty ( $attachment [ 'url' ])) {
return ;
}
$data = [ 'uri-id' => $uriid ];
2021-04-14 15:12:01 -04:00
$data [ 'type' ] = Post\Media :: UNKNOWN ;
2020-10-29 01:20:26 -04:00
$data [ 'url' ] = $attachment [ 'url' ];
2022-01-27 12:51:23 -05:00
$data [ 'mimetype' ] = $attachment [ 'mediaType' ] ? ? null ;
2020-10-29 01:20:26 -04:00
$data [ 'height' ] = $attachment [ 'height' ] ? ? null ;
2021-07-04 02:30:54 -04:00
$data [ 'width' ] = $attachment [ 'width' ] ? ? null ;
2020-10-29 01:20:26 -04:00
$data [ 'size' ] = $attachment [ 'size' ] ? ? null ;
$data [ 'preview' ] = $attachment [ 'image' ] ? ? null ;
$data [ 'description' ] = $attachment [ 'name' ] ? ? null ;
Post\Media :: insert ( $data );
}
2018-10-03 02:15:07 -04:00
/**
2021-04-26 02:50:12 -04:00
* Stire attachment data
2018-10-03 02:15:07 -04:00
*
2019-11-28 01:34:35 -05:00
* @ param array $activity
2019-05-13 15:56:46 -04:00
* @ param array $item
2018-10-03 02:15:07 -04:00
*/
2021-04-26 02:50:12 -04:00
private static function storeAttachments ( $activity , $item )
2018-10-03 02:15:07 -04:00
{
2019-11-28 01:34:35 -05:00
if ( empty ( $activity [ 'attachments' ])) {
2021-04-26 02:50:12 -04:00
return ;
2018-10-03 02:15:07 -04:00
}
2019-11-28 01:34:35 -05:00
foreach ( $activity [ 'attachments' ] as $attach ) {
2021-04-26 02:50:12 -04:00
self :: storeAttachmentAsMedia ( $item [ 'uri-id' ], $attach );
2018-10-03 02:15:07 -04:00
}
}
2018-10-27 02:17:17 -04:00
/**
* Updates a message
*
2019-01-06 16:06:53 -05:00
* @ param array $activity Activity array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-27 02:17:17 -04:00
*/
public static function updateItem ( $activity )
{
2021-04-07 02:02:06 -04:00
$item = Post :: selectFirst ([ 'uri' , 'uri-id' , 'thr-parent' , 'gravity' , 'post-type' ], [ 'uri' => $activity [ 'id' ]]);
2019-02-08 22:57:35 -05:00
if ( ! DBA :: isResult ( $item )) {
2020-04-23 15:57:20 -04:00
Logger :: warning ( 'No existing item, item will be created' , [ 'uri' => $activity [ 'id' ]]);
2020-07-20 00:37:43 -04:00
$item = self :: createItem ( $activity );
self :: postItem ( $activity , $item );
2019-02-08 22:57:35 -05:00
return ;
}
2018-10-27 02:17:17 -04:00
$item [ 'changed' ] = DateTimeFormat :: utcNow ();
2019-06-12 21:02:37 -04:00
$item [ 'edited' ] = DateTimeFormat :: utc ( $activity [ 'updated' ]);
2019-02-08 22:57:35 -05:00
2019-03-17 09:50:14 -04:00
$item = self :: processContent ( $activity , $item );
2020-10-29 01:20:26 -04:00
2021-04-26 02:50:12 -04:00
self :: storeAttachments ( $activity , $item );
2020-10-29 01:20:26 -04:00
2019-03-17 09:50:14 -04:00
if ( empty ( $item )) {
return ;
2019-02-08 22:57:35 -05:00
}
2018-10-27 02:17:17 -04:00
Item :: update ( $item , [ 'uri' => $activity [ 'id' ]]);
}
2018-10-03 02:15:07 -04:00
/**
2018-10-07 14:41:45 -04:00
* Prepares data for a message
2018-10-03 02:15:07 -04:00
*
2019-01-06 16:06:53 -05:00
* @ param array $activity Activity array
2020-07-20 00:37:43 -04:00
* @ return array Internal item
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-11 16:08:04 -04:00
public static function createItem ( $activity )
2018-10-03 02:15:07 -04:00
{
$item = [];
2019-10-23 18:25:43 -04:00
$item [ 'verb' ] = Activity :: POST ;
2019-02-22 23:42:04 -05:00
$item [ 'thr-parent' ] = $activity [ 'reply-to-id' ];
2018-10-03 02:15:07 -04:00
if ( $activity [ 'reply-to-id' ] == $activity [ 'id' ]) {
$item [ 'gravity' ] = GRAVITY_PARENT ;
2019-10-24 18:10:20 -04:00
$item [ 'object-type' ] = Activity\ObjectType :: NOTE ;
2018-10-03 02:15:07 -04:00
} else {
$item [ 'gravity' ] = GRAVITY_COMMENT ;
2019-10-24 18:10:20 -04:00
$item [ 'object-type' ] = Activity\ObjectType :: COMMENT ;
2018-10-03 02:15:07 -04:00
}
2021-01-15 23:14:58 -05:00
if ( empty ( $activity [ 'directmessage' ]) && ( $activity [ 'id' ] != $activity [ 'reply-to-id' ]) && ! Post :: exists ([ 'uri' => $activity [ 'reply-to-id' ]])) {
2020-02-02 14:59:14 -05:00
Logger :: notice ( 'Parent not found. Try to refetch it.' , [ 'parent' => $activity [ 'reply-to-id' ]]);
2018-10-03 02:15:07 -04:00
self :: fetchMissingActivity ( $activity [ 'reply-to-id' ], $activity );
}
2019-10-16 08:35:14 -04:00
$item [ 'diaspora_signed_text' ] = $activity [ 'diaspora:comment' ] ? ? '' ;
2018-10-27 10:35:22 -04:00
2020-07-20 00:37:43 -04:00
/// @todo What to do with $activity['context']?
2021-01-15 23:14:58 -05:00
if ( empty ( $activity [ 'directmessage' ]) && ( $item [ 'gravity' ] != GRAVITY_PARENT ) && ! Post :: exists ([ 'uri' => $item [ 'thr-parent' ]])) {
2020-07-20 00:37:43 -04:00
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 09:49:59 -04:00
$item [ 'author-id' ] = Contact :: getIdForURL ( $activity [ 'author' ]);
2020-07-20 00:37:43 -04:00
$item [ 'owner-link' ] = $activity [ 'actor' ];
2020-08-07 09:49:59 -04:00
$item [ 'owner-id' ] = Contact :: getIdForURL ( $activity [ 'actor' ]);
2020-07-20 00:37:43 -04: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 ;
}
}
2021-01-09 11:56:42 -05:00
if ( ! empty ( $activity [ 'from-relay' ])) {
$item [ 'direction' ] = Conversation :: RELAY ;
}
2021-04-07 02:02:06 -04:00
if ( $activity [ 'object_type' ] == 'as:Article' ) {
$item [ 'post-type' ] = Item :: PT_ARTICLE ;
} elseif ( $activity [ 'object_type' ] == 'as:Audio' ) {
$item [ 'post-type' ] = Item :: PT_AUDIO ;
} elseif ( $activity [ 'object_type' ] == 'as:Document' ) {
$item [ 'post-type' ] = Item :: PT_DOCUMENT ;
} elseif ( $activity [ 'object_type' ] == 'as:Event' ) {
$item [ 'post-type' ] = Item :: PT_EVENT ;
} elseif ( $activity [ 'object_type' ] == 'as:Image' ) {
$item [ 'post-type' ] = Item :: PT_IMAGE ;
} elseif ( $activity [ 'object_type' ] == 'as:Page' ) {
$item [ 'post-type' ] = Item :: PT_PAGE ;
2022-01-22 23:40:45 -05:00
} elseif ( $activity [ 'object_type' ] == 'as:Question' ) {
$item [ 'post-type' ] = Item :: PT_POLL ;
2021-04-07 02:02:06 -04:00
} elseif ( $activity [ 'object_type' ] == 'as:Video' ) {
$item [ 'post-type' ] = Item :: PT_VIDEO ;
} else {
$item [ 'post-type' ] = Item :: PT_NOTE ;
}
2020-07-20 00:37:43 -04:00
$item [ 'isForum' ] = false ;
if ( ! empty ( $activity [ 'thread-completion' ])) {
2020-09-25 08:16:08 -04: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 00:37:43 -04: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' ];
2021-12-11 10:17:34 -05:00
if ( empty ( $activity [ 'published' ]) || empty ( $activity [ 'updated' ])) {
DI :: logger () -> notice ( 'published or updated keys are empty for activity' , [ 'activity' => $activity , 'callstack' => System :: callstack ( 10 )]);
}
2021-12-11 10:16:57 -05:00
$item [ 'created' ] = DateTimeFormat :: utc ( $activity [ 'published' ] ? ? 'now' );
$item [ 'edited' ] = DateTimeFormat :: utc ( $activity [ 'updated' ] ? ? 'now' );
2020-07-20 00:37:43 -04:00
$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' ]]);
2020-12-13 13:42:08 -05:00
if ( empty ( $item [ 'uri-id' ])) {
Logger :: warning ( 'Unable to get a uri-id for an item uri' , [ 'uri' => $item [ 'uri' ], 'guid' => $item [ 'guid' ]]);
return [];
}
2020-07-20 00:37:43 -04:00
$item = self :: processContent ( $activity , $item );
if ( empty ( $item )) {
2020-09-20 03:46:23 -04:00
Logger :: info ( 'Message was not processed' );
2020-07-20 00:37:43 -04:00
return [];
}
$item [ 'plink' ] = $activity [ 'alternate-url' ] ? ? $item [ 'uri' ];
2021-04-26 02:50:12 -04:00
self :: storeAttachments ( $activity , $item );
2020-07-20 00:37:43 -04:00
2021-03-10 17:31:33 -05:00
// We received the post via AP, so we set the protocol of the server to AP
$contact = Contact :: getById ( $item [ 'author-id' ], [ 'gsid' ]);
if ( ! empty ( $contact [ 'gsid' ])) {
GServer :: setProtocol ( $contact [ 'gsid' ], Post\DeliveryData :: ACTIVITYPUB );
}
if ( $item [ 'author-id' ] != $item [ 'owner-id' ]) {
$contact = Contact :: getById ( $item [ 'owner-id' ], [ 'gsid' ]);
if ( ! empty ( $contact [ 'gsid' ])) {
GServer :: setProtocol ( $contact [ 'gsid' ], Post\DeliveryData :: ACTIVITYPUB );
}
}
2020-07-20 00:37:43 -04:00
return $item ;
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Delete items
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function deleteItem ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 13:35:43 -04:00
$owner = Contact :: getIdForURL ( $activity [ 'actor' ]);
2018-10-07 14:41:45 -04:00
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Deleting item' , [ 'object' => $activity [ 'object_id' ], 'owner' => $owner ]);
2020-03-03 01:47:28 -05:00
Item :: markForDeletion ([ 'uri' => $activity [ 'object_id' ], 'owner-id' => $owner ]);
2018-10-03 02:15:07 -04:00
}
2019-05-26 07:20:03 -04: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 ) {
2021-01-19 05:30:45 -05:00
$item = Post :: selectFirst ([ 'id' , 'uri-id' , 'origin' , 'author-link' ], [ 'uri' => $activity [ 'target_id' ], 'uid' => $receiver ]);
2019-05-26 07:20:03 -04: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 09:34:29 -04:00
Tag :: store ( $item [ 'uri-id' ], Tag :: HASHTAG , $activity [ 'object_content' ], $activity [ 'object_id' ]);
2020-05-05 01:11:59 -04:00
Logger :: info ( 'Tagged item' , [ 'id' => $item [ 'id' ], 'tag' => $activity [ 'object_content' ], 'uri' => $activity [ 'target_id' ], 'actor' => $activity [ 'actor' ]]);
2019-05-26 07:20:03 -04:00
}
}
2018-10-03 02:15:07 -04:00
/**
2018-10-27 02:17:17 -04:00
* Prepare the item array for an activity
2018-10-03 02:15:07 -04:00
*
2018-10-07 14:41:45 -04:00
* @ param array $activity Activity array
2018-10-27 02:17:17 -04:00
* @ param string $verb Activity verb
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-27 02:17:17 -04:00
public static function createActivity ( $activity , $verb )
2018-10-03 02:15:07 -04:00
{
2020-07-20 00:37:43 -04:00
$item = self :: createItem ( $activity );
2018-10-27 02:17:17 -04:00
$item [ 'verb' ] = $verb ;
2019-02-22 23:42:04 -05:00
$item [ 'thr-parent' ] = $activity [ 'object_id' ];
2018-10-03 02:15:07 -04:00
$item [ 'gravity' ] = GRAVITY_ACTIVITY ;
2021-04-07 02:02:06 -04:00
unset ( $item [ 'post-type' ]);
2019-10-24 18:10:20 -04:00
$item [ 'object-type' ] = Activity\ObjectType :: NOTE ;
2018-10-03 02:15:07 -04:00
2019-10-16 08:35:14 -04:00
$item [ 'diaspora_signed_text' ] = $activity [ 'diaspora:like' ] ? ? '' ;
2018-10-27 10:35:22 -04:00
2018-10-11 16:08:04 -04:00
self :: postItem ( $activity , $item );
2018-10-03 02:15:07 -04:00
}
2018-10-26 00:13:26 -04:00
/**
* Create an event
*
2018-10-27 02:17:17 -04:00
* @ param array $activity Activity array
* @ param array $item
2021-10-03 05:42:14 -04:00
*
2021-07-18 11:05:46 -04:00
* @ return int event id
2019-01-06 16:06:53 -05:00
* @ throws \Exception
2018-10-26 00:13:26 -04:00
*/
public static function createEvent ( $activity , $item )
{
2021-01-09 07:59:30 -05:00
$event [ 'summary' ] = HTML :: toBBCode ( $activity [ 'name' ]);
$event [ 'desc' ] = HTML :: toBBCode ( $activity [ 'content' ]);
$event [ 'start' ] = $activity [ 'start-time' ];
$event [ 'finish' ] = $activity [ 'end-time' ];
$event [ 'nofinish' ] = empty ( $event [ 'finish' ]);
$event [ 'location' ] = $activity [ 'location' ];
$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' ];
$event [ 'network' ] = $item [ 'network' ];
$event [ 'protocol' ] = $item [ 'protocol' ];
$event [ 'direction' ] = $item [ 'direction' ];
$event [ 'source' ] = $item [ 'source' ];
2018-10-26 00:13:26 -04:00
2021-07-18 11:05:46 -04:00
$ev = DBA :: selectFirst ( 'event' , [ 'id' ], [ 'uri' => $item [ 'uri' ], 'uid' => $item [ 'uid' ]]);
2018-10-26 00:13:26 -04:00
if ( DBA :: isResult ( $ev )) {
$event [ 'id' ] = $ev [ 'id' ];
}
$event_id = Event :: store ( $event );
2021-07-18 11:05:46 -04:00
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Event was stored' , [ 'id' => $event_id ]);
2021-07-18 11:05:46 -04:00
return $event_id ;
2018-10-26 00:13:26 -04:00
}
2019-03-17 09:50:14 -04:00
/**
* Process the content
*
* @ param array $activity Activity array
* @ param array $item
2019-03-17 11:49:21 -04:00
* @ return array | bool Returns the item array or false if there was an unexpected occurrence
2019-03-17 09:50:14 -04:00
* @ throws \Exception
*/
private static function processContent ( $activity , $item )
{
2021-03-06 03:43:25 -05:00
if ( ! empty ( $activity [ 'mediatype' ]) && ( $activity [ 'mediatype' ] == 'text/markdown' )) {
$item [ 'title' ] = Markdown :: toBBCode ( $activity [ 'name' ]);
$content = Markdown :: toBBCode ( $activity [ 'content' ]);
} elseif ( ! empty ( $activity [ 'mediatype' ]) && ( $activity [ 'mediatype' ] == 'text/bbcode' )) {
$item [ 'title' ] = $activity [ 'name' ];
$content = $activity [ 'content' ];
} else {
// By default assume "text/html"
$item [ 'title' ] = HTML :: toBBCode ( $activity [ 'name' ]);
$content = HTML :: toBBCode ( $activity [ 'content' ]);
}
2019-03-17 09:50:14 -04:00
2021-03-06 03:43:25 -05:00
if ( ! empty ( $activity [ 'languages' ])) {
$item [ 'language' ] = self :: processLanguages ( $activity [ 'languages' ]);
}
2019-03-17 18:13:17 -04:00
2020-10-29 01:20:26 -04:00
if ( ! empty ( $activity [ 'emojis' ])) {
2021-10-03 05:42:14 -04:00
$content = self :: replaceEmojis ( $item [ 'uri-id' ], $content , $activity [ 'emojis' ]);
2020-10-29 01:20:26 -04:00
}
2019-03-17 18:13:17 -04:00
2021-08-15 22:53:42 -04:00
$content = self :: addMentionLinks ( $content , $activity [ 'tags' ]);
2019-03-17 09:50:14 -04:00
2020-10-29 01:20:26 -04:00
if ( ! empty ( $activity [ 'source' ])) {
$item [ 'body' ] = $activity [ 'source' ];
$item [ 'raw-body' ] = $content ;
2021-05-07 02:26:41 -04:00
$item [ 'body' ] = Item :: improveSharedDataInBody ( $item );
2020-10-29 01:20:26 -04:00
} else {
2019-05-16 01:44:59 -04:00
if ( empty ( $activity [ 'directmessage' ]) && ( $item [ 'thr-parent' ] != $item [ 'uri' ]) && ( $item [ 'gravity' ] == GRAVITY_COMMENT )) {
2019-03-17 09:50:14 -04:00
$item_private = ! in_array ( 0 , $activity [ 'item_receiver' ]);
2021-01-15 23:14:58 -05:00
$parent = Post :: selectFirst ([ 'id' , 'uri-id' , 'private' , 'author-link' , 'alias' ], [ 'uri' => $item [ 'thr-parent' ]]);
2019-03-17 09:50:14 -04:00
if ( ! DBA :: isResult ( $parent )) {
2019-03-17 09:56:47 -04:00
Logger :: warning ( 'Unknown parent item.' , [ 'uri' => $item [ 'thr-parent' ]]);
2019-03-17 09:50:14 -04:00
return false ;
}
2020-06-27 06:35:45 -04:00
if ( $item_private && ( $parent [ 'private' ] != Item :: PRIVATE )) {
2019-03-17 09:50:14 -04: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 04:55:10 -04:00
$content = self :: removeImplicitMentionsFromBody ( $content , $parent );
2019-03-17 09:50:14 -04:00
}
$item [ 'content-warning' ] = HTML :: toBBCode ( $activity [ 'summary' ]);
2020-10-29 01:20:26 -04:00
$item [ 'raw-body' ] = $item [ 'body' ] = $content ;
2019-03-17 09:50:14 -04:00
}
2020-04-20 05:47:26 -04:00
self :: storeFromBody ( $item );
2020-04-14 13:18:48 -04:00
self :: storeTags ( $item [ 'uri-id' ], $activity [ 'tags' ]);
2020-04-13 19:54:28 -04:00
2019-03-17 09:50:14 -04:00
$item [ 'location' ] = $activity [ 'location' ];
2020-06-28 04:46:27 -04:00
if ( ! empty ( $activity [ 'latitude' ]) && ! empty ( $activity [ 'longitude' ])) {
$item [ 'coord' ] = $activity [ 'latitude' ] . ' ' . $activity [ 'longitude' ];
2019-03-17 09:50:14 -04:00
}
$item [ 'app' ] = $activity [ 'generator' ];
return $item ;
}
2020-04-20 08:19:26 -04:00
/**
* Store hashtags and mentions
*
* @ param array $item
*/
private static function storeFromBody ( array $item )
2020-04-20 05:47:26 -04:00
{
// Make sure to delete all existing tags (can happen when called via the update functionality)
2020-04-20 08:19:26 -04:00
DBA :: delete ( 'post-tag' , [ 'uri-id' => $item [ 'uri-id' ]]);
2020-04-20 05:47:26 -04:00
Tag :: storeFromBody ( $item [ 'uri-id' ], $item [ 'body' ], '@!' );
}
2020-01-19 09:33:16 -05: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 02:15:07 -04:00
/**
2018-10-07 14:41:45 -04:00
* Creates an item post
2018-10-03 02:15:07 -04:00
*
2019-01-06 16:06:53 -05:00
* @ param array $activity Activity data
* @ param array $item item array
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2020-07-20 00:37:43 -04:00
public static function postItem ( array $activity , array $item )
2018-10-03 02:15:07 -04:00
{
2020-07-21 15:55:24 -04:00
if ( empty ( $item )) {
return ;
}
2019-02-10 13:42:51 -05:00
$stored = false ;
2020-09-25 02:47:07 -04:00
ksort ( $activity [ 'receiver' ]);
2019-02-10 13:42:51 -05:00
2018-10-03 02:15:07 -04:00
foreach ( $activity [ 'receiver' ] as $receiver ) {
2020-03-02 02:57:23 -05:00
if ( $receiver == - 1 ) {
continue ;
}
2018-10-03 02:15:07 -04:00
$item [ 'uid' ] = $receiver ;
2019-07-16 01:07:26 -04:00
2020-09-12 13:45:04 -04:00
$type = $activity [ 'reception_type' ][ $receiver ] ? ? Receiver :: TARGET_UNKNOWN ;
switch ( $type ) {
case Receiver :: TARGET_TO :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_TO ;
2020-09-12 13:45:04 -04:00
break ;
case Receiver :: TARGET_CC :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_CC ;
2020-09-12 13:45:04 -04:00
break ;
case Receiver :: TARGET_BTO :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_BTO ;
2020-09-12 13:45:04 -04:00
break ;
case Receiver :: TARGET_BCC :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_BCC ;
2020-09-12 13:45:04 -04:00
break ;
case Receiver :: TARGET_FOLLOWER :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_FOLLOWER ;
2020-09-12 13:45:04 -04:00
break ;
2020-09-13 10:15:28 -04:00
case Receiver :: TARGET_ANSWER :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_COMMENT ;
2020-09-13 10:15:28 -04:00
break ;
2020-09-14 13:48:57 -04:00
case Receiver :: TARGET_GLOBAL :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_GLOBAL ;
2020-09-14 13:48:57 -04:00
break ;
2020-09-12 13:45:04 -04:00
default :
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_NONE ;
2020-09-12 13:45:04 -04:00
}
2020-09-25 02:47:07 -04:00
if ( ! empty ( $activity [ 'from-relay' ])) {
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_RELAY ;
2020-09-25 02:47:07 -04:00
} elseif ( ! empty ( $activity [ 'thread-completion' ])) {
2021-04-07 02:02:06 -04:00
$item [ 'post-reason' ] = Item :: PR_FETCHED ;
2020-09-21 08:31:20 -04:00
}
2020-07-20 00:37:43 -04:00
if ( $item [ 'isForum' ] ? ? false ) {
2020-08-07 09:49:59 -04:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'actor' ], $receiver );
2019-07-30 09:08:14 -04:00
} else {
2020-08-07 09:49:59 -04:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'author' ], $receiver );
2019-07-16 01:07:26 -04:00
}
2018-10-03 02:15:07 -04:00
2019-08-01 20:39:42 -04:00
if (( $receiver != 0 ) && empty ( $item [ 'contact-id' ])) {
2020-08-07 09:49:59 -04:00
$item [ 'contact-id' ] = Contact :: getIdForURL ( $activity [ 'author' ]);
2018-10-03 02:15:07 -04:00
}
2019-05-16 01:44:59 -04:00
if ( ! empty ( $activity [ 'directmessage' ])) {
self :: postMail ( $activity , $item );
continue ;
}
2021-05-29 15:09:41 -04:00
if ( ! ( $item [ 'isForum' ] ? ? false ) && ( $receiver != 0 ) && ( $item [ 'gravity' ] == GRAVITY_PARENT ) &&
2021-05-26 16:52:39 -04:00
( $item [ 'post-reason' ] == Item :: PR_BCC ) && ! Contact :: isSharingByURL ( $activity [ 'author' ], $receiver )) {
2021-05-27 00:00:38 -04:00
Logger :: info ( 'Top level post via BCC from a non sharer, ignoring' , [ 'uid' => $receiver , 'contact' => $item [ 'contact-id' ]]);
2021-05-26 16:52:39 -04:00
continue ;
}
2022-02-04 01:02:21 -05:00
$is_forum = false ;
if ( $receiver != 0 ) {
$user = User :: getById ( $receiver , [ 'account-type' ]);
if ( ! empty ( $user [ 'account-type' ])) {
$is_forum = ( $user [ 'account-type' ] == User :: ACCOUNT_TYPE_COMMUNITY );
}
}
if ( ! $is_forum && DI :: pConfig () -> get ( $receiver , 'system' , 'accept_only_sharer' , false ) && ( $receiver != 0 ) && ( $item [ 'gravity' ] == GRAVITY_PARENT )) {
2019-07-17 17:37:13 -04:00
$skip = ! Contact :: isSharingByURL ( $activity [ 'author' ], $receiver );
2020-07-20 00:37:43 -04:00
if ( $skip && (( $activity [ 'type' ] == 'as:Announce' ) || ( $item [ 'isForum' ] ? ? false ))) {
2019-07-17 17:37:13 -04: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 10:02:49 -04:00
if (( $item [ 'gravity' ] != GRAVITY_ACTIVITY ) && ( $activity [ 'object_type' ] == 'as:Event' )) {
2021-07-18 11:05:46 -04:00
$event_id = self :: createEvent ( $activity , $item );
2021-08-10 06:24:14 -04:00
$item = Event :: getItemArrayForImportedId ( $event_id , $item );
2018-10-26 00:13:26 -04:00
}
2018-10-03 02:15:07 -04:00
$item_id = Item :: insert ( $item );
2019-02-22 23:00:16 -05: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 13:42:51 -05:00
2019-02-10 13:59:05 -05:00
if ( $item [ 'uid' ] == 0 ) {
$stored = $item_id ;
2019-02-10 13:42:51 -05:00
}
2018-10-03 02:15:07 -04:00
}
2019-01-30 16:33:23 -05:00
2019-02-10 13:42:51 -05:00
// Store send a follow request for every reshare - but only when the item had been stored
2020-03-02 02:57:23 -05:00
if ( $stored && ( $item [ 'private' ] != Item :: PRIVATE ) && ( $item [ 'gravity' ] == GRAVITY_PARENT ) && ( $item [ 'author-link' ] != $item [ 'owner-link' ])) {
2019-01-30 16:33:23 -05: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 13:50:11 -04:00
Logger :: info ( 'Send follow request' , [ 'uri' => $item [ 'uri' ], 'stored' => $stored , 'to' => $item [ 'author-link' ]]);
2019-01-30 16:33:23 -05:00
ActivityPub\Transmitter :: sendFollowObject ( $item [ 'uri' ], $item [ 'author-link' ]);
}
}
2018-10-03 02:15:07 -04:00
}
2020-04-15 16:59:45 -04:00
/**
* Store tags and mentions into the tag table
*
* @ param integer $uriid
* @ param array $tags
*/
2020-04-14 13:18:48 -04:00
private static function storeTags ( int $uriid , array $tags = null )
2020-04-13 19:54:28 -04:00
{
foreach ( $tags as $tag ) {
if ( empty ( $tag [ 'name' ]) || empty ( $tag [ 'type' ]) || ! in_array ( $tag [ 'type' ], [ 'Mention' , 'Hashtag' ])) {
continue ;
}
2020-04-17 02:35:20 -04:00
$hash = substr ( $tag [ 'name' ], 0 , 1 );
2020-04-13 19:54:28 -04:00
if ( $tag [ 'type' ] == 'Mention' ) {
2020-04-17 02:35:20 -04: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-13 19:54:28 -04:00
}
2020-04-20 01:43:13 -04:00
$type = Tag :: IMPLICIT_MENTION ;
2020-04-17 02:35:20 -04:00
2020-04-15 16:45:04 -04:00
if ( ! empty ( $tag [ 'href' ])) {
$apcontact = APContact :: getByURL ( $tag [ 'href' ]);
2020-04-15 16:52:30 -04:00
if ( ! empty ( $apcontact [ 'name' ]) || ! empty ( $apcontact [ 'nick' ])) {
2020-04-17 02:35:20 -04:00
$tag [ 'name' ] = $apcontact [ 'name' ] ? : $apcontact [ 'nick' ];
2020-04-15 16:45:04 -04:00
}
}
2020-04-13 19:54:28 -04:00
} elseif ( $tag [ 'type' ] == 'Hashtag' ) {
2020-04-20 01:43:13 -04:00
if ( $hash == Tag :: TAG_CHARACTER [ Tag :: HASHTAG ]) {
2020-04-17 02:35:20 -04:00
$tag [ 'name' ] = substr ( $tag [ 'name' ], 1 );
2020-04-13 19:54:28 -04:00
}
2020-04-20 01:43:13 -04:00
$type = Tag :: HASHTAG ;
2020-04-13 19:54:28 -04:00
}
2020-04-17 02:35:20 -04:00
if ( empty ( $tag [ 'name' ])) {
2020-04-13 19:54:28 -04:00
continue ;
}
2020-04-20 05:47:26 -04:00
2020-04-20 01:43:13 -04:00
Tag :: store ( $uriid , $type , $tag [ 'name' ], $tag [ 'href' ]);
2020-04-13 19:54:28 -04:00
}
}
2019-05-16 01:44:59 -04:00
/**
* Creates an mail post
*
* @ param array $activity Activity data
* @ param array $item item array
2019-06-12 21:01:44 -04:00
* @ return int | bool New mail table row id or false on error
2019-05-16 01:44:59 -04: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 ];
}
2021-07-05 03:00:35 -04:00
$title = trim ( BBCode :: toPlaintext ( $title ));
2019-05-16 01:44:59 -04:00
if ( strlen ( $title ) > 20 ) {
$title = substr ( $title , 0 , 20 ) . '...' ;
}
$msg [ 'title' ] = $title ;
}
}
$msg [ 'body' ] = $item [ 'body' ];
2019-06-12 21:01:44 -04:00
return Mail :: insert ( $msg );
2019-05-16 01:44:59 -04:00
}
2018-10-03 02:15:07 -04:00
/**
2018-10-07 14:41:45 -04:00
* Fetches missing posts
2018-10-03 02:15:07 -04:00
*
2020-09-22 01:36:01 -04: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 17:30:34 -05:00
* @ return string fetched message URL
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-03 02:15:07 -04:00
*/
2020-09-22 01:36:01 -04:00
public static function fetchMissingActivity ( string $url , array $child = [], string $relay_actor = '' )
2018-10-03 02:15:07 -04:00
{
2019-07-17 15:36:32 -04:00
if ( ! empty ( $child [ 'receiver' ])) {
$uid = ActivityPub\Receiver :: getFirstUserFromReceivers ( $child [ 'receiver' ]);
} else {
$uid = 0 ;
}
2018-11-03 17:37:08 -04:00
$object = ActivityPub :: fetchContent ( $url , $uid );
2018-10-03 02:15:07 -04:00
if ( empty ( $object )) {
2021-04-26 02:50:12 -04:00
Logger :: notice ( 'Activity was not fetchable, aborting.' , [ 'url' => $url ]);
2020-01-20 17:30:34 -05:00
return '' ;
2018-10-03 02:15:07 -04:00
}
2018-12-29 04:53:31 -05:00
if ( empty ( $object [ 'id' ])) {
2021-04-26 02:50:12 -04:00
Logger :: notice ( 'Activity has got not id, aborting. ' , [ 'url' => $url , 'object' => $object ]);
2020-01-20 17:30:34 -05:00
return '' ;
2018-12-29 04:53:31 -05:00
}
2020-09-14 16:58:41 -04:00
if ( ! empty ( $object [ 'actor' ])) {
$object_actor = $object [ 'actor' ];
2019-07-17 15:36:32 -04:00
} elseif ( ! empty ( $object [ 'attributedTo' ])) {
2020-09-14 16:58:41 -04:00
$object_actor = $object [ 'attributedTo' ];
2020-11-07 03:22:59 -05:00
if ( is_array ( $object_actor )) {
$compacted = JsonLD :: compact ( $object );
$object_actor = JsonLD :: fetchElement ( $compacted , 'as:attributedTo' , '@id' );
}
2019-07-17 15:36:32 -04:00
} else {
// Shouldn't happen
2020-09-14 16:58:41 -04:00
$object_actor = '' ;
}
$signer = [ $object_actor ];
if ( ! empty ( $child [ 'author' ])) {
$actor = $child [ 'author' ];
$signer [] = $actor ;
} else {
$actor = $object_actor ;
2019-07-17 15:36:32 -04:00
}
if ( ! empty ( $object [ 'published' ])) {
$published = $object [ 'published' ];
} elseif ( ! empty ( $child [ 'published' ])) {
$published = $child [ 'published' ];
} else {
$published = DateTimeFormat :: utcNow ();
}
2018-10-03 02:15:07 -04:00
$activity = [];
2020-12-17 13:08:07 -05:00
$activity [ '@context' ] = $object [ '@context' ] ? ? ActivityPub :: CONTEXT ;
2018-10-03 02:15:07 -04:00
unset ( $object [ '@context' ]);
$activity [ 'id' ] = $object [ 'id' ];
2019-10-16 08:35:14 -04:00
$activity [ 'to' ] = $object [ 'to' ] ? ? [];
$activity [ 'cc' ] = $object [ 'cc' ] ? ? [];
2019-07-17 15:36:32 -04:00
$activity [ 'actor' ] = $actor ;
2018-10-03 02:15:07 -04:00
$activity [ 'object' ] = $object ;
2019-07-17 15:36:32 -04:00
$activity [ 'published' ] = $published ;
2018-10-03 02:15:07 -04:00
$activity [ 'type' ] = 'Create' ;
2018-10-07 11:34:51 -04:00
$ldactivity = JsonLD :: compact ( $activity );
2018-10-09 01:04:24 -04:00
2020-09-25 08:16:08 -04: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 01:04:24 -04:00
2020-09-22 11:48:44 -04:00
if ( ! empty ( $relay_actor ) && ! self :: acceptIncomingMessage ( $ldactivity , $object [ 'id' ])) {
return '' ;
}
2020-09-14 16:58:41 -04:00
ActivityPub\Receiver :: processActivity ( $ldactivity , json_encode ( $activity ), $uid , true , false , $signer );
2020-03-03 17:43:19 -05:00
2020-01-20 17:30:34 -05:00
Logger :: notice ( 'Activity had been fetched and processed.' , [ 'url' => $url , 'object' => $activity [ 'id' ]]);
2019-07-21 03:37:50 -04:00
2020-01-20 17:30:34 -05:00
return $activity [ 'id' ];
2018-10-03 02:15:07 -04:00
}
2020-09-22 11:48:44 -04: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 ;
}
$replyto = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:inReplyTo' , '@id' );
2021-02-13 14:56:03 -05:00
$uriid = ItemURI :: getIdByURI ( $replyto );
if ( Post :: exists ([ 'uri-id' => $uriid ])) {
Logger :: info ( 'Post is a reply to an existing post - accepted' , [ 'id' => $id , 'uri-id' => $uriid , 'replyto' => $replyto ]);
2020-09-22 11:48:44 -04:00
return true ;
}
2020-10-01 23:35:22 -04:00
$attributed_to = JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:attributedTo' , '@id' );
$authorid = Contact :: getIdForURL ( $attributed_to );
2020-09-30 13:37:46 -04:00
$body = HTML :: toBBCode ( JsonLD :: fetchElement ( $activity [ 'as:object' ], 'as:content' , '@value' ));
2020-09-22 11:48:44 -04:00
$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' ]), '#' );
}
}
2020-10-01 23:35:22 -04:00
return Relay :: isSolicitedPost ( $messageTags , $body , $authorid , $id , Protocol :: ACTIVITYPUB );
2020-09-22 11:48:44 -04:00
}
2018-10-03 02:15:07 -04:00
/**
2018-10-06 00:18:40 -04:00
* perform a " follow " request
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function followUser ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 15:42:04 -04:00
$uid = User :: getIdForURL ( $activity [ 'object_id' ]);
2018-10-03 02:15:07 -04:00
if ( empty ( $uid )) {
return ;
}
$owner = User :: getOwnerDataById ( $uid );
2020-08-22 12:34:04 -04:00
if ( empty ( $owner )) {
return ;
}
2018-10-03 02:15:07 -04:00
2018-10-07 13:35:43 -04:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 02:15:07 -04:00
if ( ! empty ( $cid )) {
2018-10-09 15:58:15 -04:00
self :: switchContact ( $cid );
2021-09-10 14:21:19 -04:00
Contact :: update ([ 'hub-verify' => $activity [ 'id' ], 'protocol' => Protocol :: ACTIVITYPUB ], [ 'id' => $cid ]);
2018-10-03 02:15:07 -04:00
}
2018-10-07 13:35:43 -04:00
$item = [ 'author-id' => Contact :: getIdForURL ( $activity [ 'actor' ]),
'author-link' => $activity [ 'actor' ]];
2018-10-03 02:15:07 -04:00
2018-10-13 14:13:01 -04:00
// Ensure that the contact has got the right network type
self :: switchContact ( $item [ 'author-id' ]);
2020-12-13 12:16:04 -05:00
$result = Contact :: addRelationship ( $owner , [], $item , false , $activity [ 'content' ] ? ? '' );
2019-05-20 16:33:09 -04:00
if ( $result === true ) {
2020-07-10 01:30:12 -04:00
ActivityPub\Transmitter :: sendContactAccept ( $item [ 'author-link' ], $activity [ 'id' ], $owner [ 'uid' ]);
2019-05-19 18:46:29 -04:00
}
2018-10-07 13:35:43 -04:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 02:15:07 -04:00
if ( empty ( $cid )) {
return ;
}
2019-01-09 17:30:26 -05:00
if ( empty ( $contact )) {
2021-09-10 14:21:19 -04:00
Contact :: update ([ 'hub-verify' => $activity [ 'id' ], 'protocol' => Protocol :: ACTIVITYPUB ], [ 'id' => $cid ]);
2019-01-09 17:30:26 -05:00
}
2021-04-26 02:50:12 -04:00
Logger :: notice ( 'Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity [ 'id' ]);
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Update the given profile
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Exception
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function updatePerson ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 14:41:45 -04:00
if ( empty ( $activity [ 'object_id' ])) {
2018-10-03 02:15:07 -04:00
return ;
}
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Updating profile' , [ 'object' => $activity [ 'object_id' ]]);
2020-08-06 14:53:45 -04:00
Contact :: updateFromProbeByURL ( $activity [ 'object_id' ]);
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Delete the given profile
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function deletePerson ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 15:42:04 -04:00
if ( empty ( $activity [ 'object_id' ]) || empty ( $activity [ 'actor' ])) {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Empty object id or actor.' );
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-07 15:42:04 -04:00
if ( $activity [ 'object_id' ] != $activity [ 'actor' ]) {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Object id does not match actor.' );
2018-10-03 02:15:07 -04:00
return ;
}
2018-11-08 11:28:29 -05:00
$contacts = DBA :: select ( 'contact' , [ 'id' ], [ 'nurl' => Strings :: normaliseLink ( $activity [ 'object_id' ])]);
2018-10-03 02:15:07 -04:00
while ( $contact = DBA :: fetch ( $contacts )) {
2018-10-07 11:34:51 -04:00
Contact :: remove ( $contact [ 'id' ]);
2018-10-03 02:15:07 -04:00
}
DBA :: close ( $contacts );
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Deleted contact' , [ 'object' => $activity [ 'object_id' ]]);
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Accept a follow request
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function acceptFollowUser ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 14:41:45 -04:00
$uid = User :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 02:15:07 -04:00
if ( empty ( $uid )) {
return ;
}
2018-10-07 13:35:43 -04:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 02:15:07 -04:00
if ( empty ( $cid )) {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-09 15:58:15 -04:00
self :: switchContact ( $cid );
2018-10-03 02:15:07 -04:00
$fields = [ 'pending' => false ];
$contact = DBA :: selectFirst ( 'contact' , [ 'rel' ], [ 'id' => $cid ]);
if ( $contact [ 'rel' ] == Contact :: FOLLOWER ) {
$fields [ 'rel' ] = Contact :: FRIEND ;
}
$condition = [ 'id' => $cid ];
2021-09-10 14:21:19 -04:00
Contact :: update ( $fields , $condition );
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Accept contact request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Reject a follow request
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function rejectFollowUser ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 14:41:45 -04:00
$uid = User :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 02:15:07 -04:00
if ( empty ( $uid )) {
return ;
}
2018-10-07 13:35:43 -04:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 02:15:07 -04:00
if ( empty ( $cid )) {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-09 15:58:15 -04:00
self :: switchContact ( $cid );
2021-10-02 11:19:41 -04:00
$contact = Contact :: getById ( $cid , [ 'rel' ]);
if ( $contact [ 'rel' ] == Contact :: SHARING ) {
2018-10-03 02:15:07 -04:00
Contact :: remove ( $cid );
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Rejected contact request - contact removed' , [ 'contact' => $cid , 'user' => $uid ]);
2021-10-02 11:19:41 -04:00
} elseif ( $contact [ 'rel' ] == Contact :: FRIEND ) {
Contact :: update ([ 'rel' => Contact :: FOLLOWER ], [ 'id' => $cid ]);
2018-10-03 02:15:07 -04:00
} else {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Rejected contact request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 02:15:07 -04:00
}
}
/**
2018-10-06 00:18:40 -04:00
* Undo activity like " like " or " dislike "
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function undoActivity ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 14:41:45 -04:00
if ( empty ( $activity [ 'object_id' ])) {
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-07 14:41:45 -04:00
if ( empty ( $activity [ 'object_actor' ])) {
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-07 14:41:45 -04:00
$author_id = Contact :: getIdForURL ( $activity [ 'object_actor' ]);
2018-10-03 02:15:07 -04:00
if ( empty ( $author_id )) {
return ;
}
2020-03-03 01:47:28 -05:00
Item :: markForDeletion ([ 'uri' => $activity [ 'object_id' ], 'author-id' => $author_id , 'gravity' => GRAVITY_ACTIVITY ]);
2018-10-03 02:15:07 -04:00
}
/**
2018-10-06 00:18:40 -04:00
* Activity to remove a follower
2018-10-03 02:15:07 -04:00
*
* @ param array $activity
2019-01-06 16:06:53 -05:00
* @ throws \Friendica\Network\HTTPException\InternalServerErrorException
* @ throws \ImagickException
2018-10-03 02:15:07 -04:00
*/
2018-10-03 05:15:38 -04:00
public static function undoFollowUser ( $activity )
2018-10-03 02:15:07 -04:00
{
2018-10-07 14:41:45 -04:00
$uid = User :: getIdForURL ( $activity [ 'object_object' ]);
2018-10-03 02:15:07 -04:00
if ( empty ( $uid )) {
return ;
}
$owner = User :: getOwnerDataById ( $uid );
2020-08-22 12:34:04 -04:00
if ( empty ( $owner )) {
return ;
}
2018-10-03 02:15:07 -04:00
2018-10-07 13:35:43 -04:00
$cid = Contact :: getIdForURL ( $activity [ 'actor' ], $uid );
2018-10-03 02:15:07 -04:00
if ( empty ( $cid )) {
2020-06-28 13:50:11 -04:00
Logger :: info ( 'No contact found' , [ 'actor' => $activity [ 'actor' ]]);
2018-10-03 02:15:07 -04:00
return ;
}
2018-10-09 15:58:15 -04:00
self :: switchContact ( $cid );
2018-10-03 02:15:07 -04:00
$contact = DBA :: selectFirst ( 'contact' , [], [ 'id' => $cid ]);
if ( ! DBA :: isResult ( $contact )) {
return ;
}
2021-09-13 14:22:55 -04:00
Contact :: removeFollower ( $contact );
2020-06-28 13:50:11 -04:00
Logger :: info ( 'Undo following request' , [ 'contact' => $cid , 'user' => $uid ]);
2018-10-03 02:15:07 -04:00
}
2018-10-09 15:58:15 -04:00
/**
* Switches a contact to AP if needed
*
* @ param integer $cid Contact ID
2019-01-06 16:06:53 -05:00
* @ throws \Exception
2018-10-09 15:58:15 -04:00
*/
private static function switchContact ( $cid )
{
2020-01-16 01:43:21 -05: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 15:58:15 -04:00
return ;
}
2020-01-16 01:43:21 -05:00
Logger :: info ( 'Change existing contact' , [ 'cid' => $cid , 'previous' => $contact [ 'network' ]]);
Contact :: updateFromProbe ( $cid );
2018-10-09 15:58:15 -04:00
}
2019-02-08 22:57:35 -05: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 02:01:22 -04:00
$parent_terms = Tag :: getByURIId ( $parent [ 'uri-id' ], [ Tag :: MENTION , Tag :: IMPLICIT_MENTION , Tag :: EXCLUSIVE_MENTION ]);
2019-02-22 23:42:04 -05:00
2020-07-15 13:06:48 -04:00
$parent_author = Contact :: getByURL ( $parent [ 'author-link' ], false , [ 'url' , 'nurl' , 'alias' ]);
2019-02-08 22:57:35 -05:00
2019-02-24 15:48:56 -05:00
$implicit_mentions = [];
2020-06-16 16:41:34 -04:00
if ( empty ( $parent_author [ 'url' ])) {
2021-02-19 01:30:38 -05:00
Logger :: notice ( 'Author public contact unknown.' , [ 'author-link' => $parent [ 'author-link' ], 'parent-id' => $parent [ 'id' ]]);
2019-02-25 03:16:18 -05:00
} else {
2019-02-24 15:48:56 -05:00
$implicit_mentions [] = $parent_author [ 'url' ];
$implicit_mentions [] = $parent_author [ 'nurl' ];
$implicit_mentions [] = $parent_author [ 'alias' ];
}
2019-02-08 22:57:35 -05:00
2019-02-24 15:48:56 -05:00
if ( ! empty ( $parent [ 'alias' ])) {
2019-02-08 22:57:35 -05:00
$implicit_mentions [] = $parent [ 'alias' ];
}
foreach ( $parent_terms as $term ) {
2020-07-15 13:06:48 -04:00
$contact = Contact :: getByURL ( $term [ 'url' ], false , [ 'url' , 'nurl' , 'alias' ]);
2020-06-16 16:41:34 -04:00
if ( ! empty ( $contact [ 'url' ])) {
2019-02-10 13:42:51 -05:00
$implicit_mentions [] = $contact [ 'url' ];
$implicit_mentions [] = $contact [ 'nurl' ];
$implicit_mentions [] = $contact [ 'alias' ];
}
2019-02-08 22:57:35 -05:00
}
return $implicit_mentions ;
}
/**
* Strips from the body prepended implicit mentions
*
* @ param string $body
2020-05-09 04:55:10 -04:00
* @ param array $parent
2019-02-08 22:57:35 -05:00
* @ return string
*/
2020-05-09 04:55:10 -04:00
private static function removeImplicitMentionsFromBody ( string $body , array $parent )
2019-02-08 22:57:35 -05:00
{
2020-01-19 15:21:13 -05:00
if ( DI :: config () -> get ( 'system' , 'disable_implicit_mentions' )) {
2019-02-13 12:26:54 -05:00
return $body ;
2019-02-13 12:23:23 -05:00
}
2020-05-09 04:55:10 -04:00
$potential_mentions = self :: getImplicitMentionList ( $parent );
2019-02-08 22:57:35 -05:00
$kept_mentions = [];
// Extract one prepended mention at a time from the body
2019-03-09 09:04:11 -05:00
while ( preg_match ( '#^(@\[url=([^\]]+)].*?\[\/url]\s)(.*)#is' , $body , $matches )) {
2019-10-16 08:58:09 -04:00
if ( ! in_array ( $matches [ 2 ], $potential_mentions )) {
2019-02-08 22:57:35 -05: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 );
}
2021-08-15 22:53:42 -04:00
/**
* Adds links to string mentions
*
* @ param string $body
* @ param array $tags
* @ return string
*/
protected static function addMentionLinks ( string $body , array $tags ) : string
{
// This prevents links to be added again to Pleroma-style mention links
$body = self :: normalizeMentionLinks ( $body );
2021-08-20 03:40:23 -04:00
$body = BBCode :: performWithEscapedTags ( $body , [ 'url' ], function ( $body ) use ( $tags ) {
foreach ( $tags as $tag ) {
if ( empty ( $tag [ 'name' ]) || empty ( $tag [ 'type' ]) || empty ( $tag [ 'href' ]) || ! in_array ( $tag [ 'type' ], [ 'Mention' , 'Hashtag' ])) {
continue ;
}
2021-08-15 22:53:42 -04:00
2021-08-20 03:40:23 -04:00
$hash = substr ( $tag [ 'name' ], 0 , 1 );
$name = substr ( $tag [ 'name' ], 1 );
if ( ! in_array ( $hash , Tag :: TAG_CHARACTER )) {
$hash = '' ;
$name = $tag [ 'name' ];
}
$body = str_replace ( $tag [ 'name' ], $hash . '[url=' . $tag [ 'href' ] . ']' . $name . '[/url]' , $body );
2021-08-15 22:53:42 -04:00
}
2021-08-20 03:40:23 -04:00
return $body ;
});
2021-08-15 22:53:42 -04:00
return $body ;
}
2018-10-03 02:15:07 -04:00
}