X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FReceiver.php;h=21aba50944f7448bcc9b9411bf86e150526582e2;hb=7b7132971a64a92a685a5fd860fe4709dce1765a;hp=7ae68cd4d1247238fc50a15ef6fa1030ff7d4463;hpb=064c0cd5ffa472796e4a3e495dcbba1a944e78e0;p=friendica.git diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index 7ae68cd4d1..21aba50944 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -1,17 +1,36 @@ . + * */ + namespace Friendica\Protocol\ActivityPub; use Friendica\Database\DBA; +use Friendica\Content\Text\HTML; +use Friendica\Content\Text\Markdown; use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Model\Contact; use Friendica\Model\APContact; -use Friendica\Model\Conversation; use Friendica\Model\Item; use Friendica\Model\User; +use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; @@ -20,10 +39,10 @@ use Friendica\Util\LDSignature; use Friendica\Util\Strings; /** - * @brief ActivityPub Receiver Protocol class + * ActivityPub Receiver Protocol class * * To-Do: - * - Undo Announce + * @todo Undo Announce * * Check what this is meant to do: * - Add @@ -46,8 +65,8 @@ class Receiver */ public static function isRequest() { - return stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/activity+json') || - stristr(defaults($_SERVER, 'HTTP_ACCEPT', ''), 'application/ld+json'); + return stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/activity+json') || + stristr($_SERVER['HTTP_ACCEPT'] ?? '', 'application/ld+json'); } /** @@ -107,7 +126,7 @@ class Receiver $trust_source = false; } - self::processActivity($ldactivity, $body, $uid, $trust_source); + self::processActivity($ldactivity, $body, $uid, $trust_source, true); } /** @@ -155,15 +174,16 @@ class Receiver /** * Prepare the object array * - * @param array $activity - * @param integer $uid User ID - * @param $trust_source + * @param array $activity Array with activity data + * @param integer $uid User ID + * @param boolean $push Message had been pushed to our system + * @param boolean $trust_source Do we trust the source? * * @return array with object data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function prepareObjectData($activity, $uid, &$trust_source) + private static function prepareObjectData($activity, $uid, $push, &$trust_source) { $actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); if (empty($actor)) { @@ -194,6 +214,11 @@ class Receiver return []; } + if (!is_string($object_id)) { + Logger::info('Invalid object id', ['object' => $object_id]); + return []; + } + $object_type = self::fetchObjectType($activity, $object_id, $uid); // Fetch the content only on activities where this matters @@ -206,13 +231,25 @@ class Receiver Logger::log("Object data couldn't be processed", Logger::DEBUG); return []; } + $object_data['object_id'] = $object_id; - $object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage'); + + if ($type == 'as:Announce') { + $object_data['push'] = false; + } else { + $object_data['push'] = $push; + } + + // Test if it is an answer to a mail + if (DBA::exists('mail', ['uri' => $object_data['reply-to-id']])) { + $object_data['directmessage'] = true; + } else { + $object_data['directmessage'] = JsonLD::fetchElement($activity, 'litepub:directMessage'); + } // We had been able to retrieve the object data - so we can trust the source $trust_source = true; - } elseif (in_array($type, ['as:Like', 'as:Dislike']) || - (($type == 'as:Follow') && in_array($object_type, self::CONTENT_TYPES))) { + } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow'])) && in_array($object_type, self::CONTENT_TYPES)) { // Create a mostly empty array out of the activity data (instead of the object). // This way we later don't have to check for the existence of ech individual array element. $object_data = self::processObject($activity); @@ -220,6 +257,13 @@ class Receiver $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id'); $object_data['object_id'] = $object_id; $object_data['object_type'] = ''; // Since we don't fetch the object, we don't know the type + } elseif (in_array($type, ['as:Add'])) { + $object_data = []; + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['target_id'] = JsonLD::fetchElement($activity, 'as:target', '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); + $object_data['object_content'] = JsonLD::fetchElement($activity['as:object'], 'as:content', '@type'); } else { $object_data = []; $object_data['id'] = JsonLD::fetchElement($activity, '@id'); @@ -243,7 +287,7 @@ class Receiver $object_data['type'] = $type; $object_data['actor'] = $actor; $object_data['item_receiver'] = $receivers; - $object_data['receiver'] = array_merge(defaults($object_data, 'receiver', []), $receivers); + $object_data['receiver'] = array_merge($object_data['receiver'] ?? [], $receivers); Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG); @@ -266,33 +310,6 @@ class Receiver return 0; } - /** - * Store the unprocessed data into the conversation table - * This has to be done outside the regular function, - * since we store everything - not only item posts. - * - * @param array $activity Array with activity data - * @param string $body The raw message - * @throws \Exception - */ - private static function storeConversation($activity, $body) - { - if (empty($body) || empty($activity['id'])) { - return; - } - - $conversation = [ - 'protocol' => Conversation::PARCEL_ACTIVITYPUB, - 'item-uri' => $activity['id'], - 'reply-to-uri' => defaults($activity, 'reply-to-id', ''), - 'conversation-href' => defaults($activity, 'context', ''), - 'conversation-uri' => defaults($activity, 'conversation', ''), - 'source' => $body, - 'received' => DateTimeFormat::utcNow()]; - - DBA::insert('conversation', $conversation, true); - } - /** * Processes the activity object * @@ -300,9 +317,10 @@ class Receiver * @param string $body * @param integer $uid User ID * @param boolean $trust_source Do we trust the source? + * @param boolean $push Message had been pushed to our system * @throws \Exception */ - public static function processActivity($activity, $body = '', $uid = null, $trust_source = false) + public static function processActivity($activity, $body = '', $uid = null, $trust_source = false, $push = false) { $type = JsonLD::fetchElement($activity, '@type'); if (!$type) { @@ -332,7 +350,7 @@ class Receiver } // $trust_source is called by reference and is set to true if the content was retrieved successfully - $object_data = self::prepareObjectData($activity, $uid, $trust_source); + $object_data = self::prepareObjectData($activity, $uid, $push, $trust_source); if (empty($object_data)) { Logger::log('No object data found', Logger::DEBUG); return; @@ -343,9 +361,8 @@ class Receiver return; } - // Only store content related stuff - and no announces, since they possibly overwrite the original content - if (in_array($object_data['object_type'], self::CONTENT_TYPES) && ($type != 'as:Announce')) { - self::storeConversation($object_data, $body); + if (!empty($body) && empty($object_data['raw'])) { + $object_data['raw'] = $body; } // Internal flag for thread completion. See Processor.php @@ -360,44 +377,54 @@ class Receiver } break; + case 'as:Add': + if ($object_data['object_type'] == 'as:tag') { + ActivityPub\Processor::addTag($object_data); + } + break; + case 'as:Announce': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { $profile = APContact::getByURL($object_data['actor']); - if ($profile['type'] == 'Person') { - // Reshared posts from persons appear as summary at the bottom - // If this isn't set, then a single reshare appears on top. This is used for groups. - $object_data['thread-completion'] = true; - } + // Reshared posts from persons appear as summary at the bottom + // If this isn't set, then a single reshare appears on top. This is used for groups. + $object_data['thread-completion'] = ($profile['type'] != 'Group'); + ActivityPub\Processor::createItem($object_data); // Add the bottom reshare information only for persons - if ($profile['type'] == 'Person') { + if ($profile['type'] != 'Group') { $announce_object_data = self::processObject($activity); $announce_object_data['name'] = $type; $announce_object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id'); $announce_object_data['object_id'] = $object_data['object_id']; $announce_object_data['object_type'] = $object_data['object_type']; + $announce_object_data['push'] = $push; + + if (!empty($body)) { + $announce_object_data['raw'] = $body; + } - ActivityPub\Processor::createActivity($announce_object_data, ACTIVITY2_ANNOUNCE); + ActivityPub\Processor::createActivity($announce_object_data, Activity::ANNOUNCE); } } break; case 'as:Like': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - ActivityPub\Processor::createActivity($object_data, ACTIVITY_LIKE); + ActivityPub\Processor::createActivity($object_data, Activity::LIKE); } break; case 'as:Dislike': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - ActivityPub\Processor::createActivity($object_data, ACTIVITY_DISLIKE); + ActivityPub\Processor::createActivity($object_data, Activity::DISLIKE); } break; case 'as:TentativeAccept': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTENDMAYBE); + ActivityPub\Processor::createActivity($object_data, Activity::ATTENDMAYBE); } break; @@ -422,7 +449,7 @@ class Receiver ActivityPub\Processor::followUser($object_data); } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { $object_data['reply-to-id'] = $object_data['object_id']; - ActivityPub\Processor::createActivity($object_data, ACTIVITY_FOLLOW); + ActivityPub\Processor::createActivity($object_data, Activity::FOLLOW); } break; @@ -430,7 +457,7 @@ class Receiver if ($object_data['object_type'] == 'as:Follow') { ActivityPub\Processor::acceptFollowUser($object_data); } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTEND); + ActivityPub\Processor::createActivity($object_data, Activity::ATTEND); } break; @@ -438,7 +465,7 @@ class Receiver if ($object_data['object_type'] == 'as:Follow') { ActivityPub\Processor::rejectFollowUser($object_data); } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - ActivityPub\Processor::createActivity($object_data, ACTIVITY_ATTENDNO); + ActivityPub\Processor::createActivity($object_data, Activity::ATTENDNO); } break; @@ -464,20 +491,25 @@ class Receiver /** * Fetch the receiver list from an activity array * - * @param array $activity - * @param string $actor - * @param array $tags + * @param array $activity + * @param string $actor + * @param array $tags + * @param boolean $fetch_unlisted * * @return array with receivers (user id) * @throws \Exception */ - private static function getReceivers($activity, $actor, $tags = []) + private static function getReceivers($activity, $actor, $tags = [], $fetch_unlisted = false) { $receivers = []; // When it is an answer, we inherite the receivers from the parent $replyto = JsonLD::fetchElement($activity, 'as:inReplyTo', '@id'); if (!empty($replyto)) { + // Fix possibly wrong item URI (could be an answer to a plink uri) + $fixedReplyTo = Item::getURIByLink($replyto); + $replyto = $fixedReplyTo ?: $replyto; + $parents = Item::select(['uid'], ['uri' => $replyto]); while ($parent = Item::fetch($parents)) { $receivers['uid:' . $parent['uid']] = $parent['uid']; @@ -486,7 +518,7 @@ class Receiver if (!empty($actor)) { $profile = APContact::getByURL($actor); - $followers = defaults($profile, 'followers', ''); + $followers = $profile['followers'] ?? ''; Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG); } else { @@ -505,6 +537,11 @@ class Receiver $receivers['uid:0'] = 0; } + // Add receiver "-1" for unlisted posts + if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) { + $receivers['uid:-1'] = -1; + } + if (($receiver == self::PUBLIC_COLLECTION) && !empty($actor)) { // This will most likely catch all OStatus connections to Mastodon $condition = ['alias' => [$actor, Strings::normaliseLink($actor)], 'rel' => [Contact::SHARING, Contact::FRIEND] @@ -533,7 +570,7 @@ class Receiver // Check if the potential receiver is following the actor // Exception: The receiver is targetted via "to" or this is a comment if ((($element != 'as:to') && empty($replyto)) || ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) { - $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $networks = Protocol::FEDERATED; $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => $networks, 'archive' => false, 'pending' => false, 'uid' => $contact['uid']]; @@ -568,7 +605,7 @@ class Receiver public static function getReceiverForActor($actor, $tags) { $receivers = []; - $networks = [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]; + $networks = Protocol::FEDERATED; $condition = ['nurl' => Strings::normaliseLink($actor), 'rel' => [Contact::SHARING, Contact::FRIEND, Contact::FOLLOWER], 'network' => $networks, 'archive' => false, 'pending' => false]; $contacts = DBA::select('contact', ['uid', 'rel'], $condition); @@ -634,27 +671,19 @@ class Receiver */ public static function switchContact($cid, $uid, $url) { - $profile = ActivityPub::probeProfile($url); - if (empty($profile)) { + if (DBA::exists('contact', ['id' => $cid, 'network' => Protocol::ACTIVITYPUB])) { + Logger::info('Contact is already ActivityPub', ['id' => $cid, 'uid' => $uid, 'url' => $url]); return; } - Logger::log('Switch contact ' . $cid . ' (' . $profile['url'] . ') for user ' . $uid . ' to ActivityPub'); - - $photo = defaults($profile, 'photo', null); - unset($profile['photo']); - unset($profile['baseurl']); - unset($profile['guid']); - - $profile['nurl'] = Strings::normaliseLink($profile['url']); - DBA::update('contact', $profile, ['id' => $cid]); - - Contact::updateAvatar($photo, $uid, $cid); + if (Contact::updateFromProbe($cid, '', true)) { + Logger::info('Update was successful', ['id' => $cid, 'uid' => $uid, 'url' => $url]); + } // Send a new follow request to be sure that the connection still exists - if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND]])) { - ActivityPub\Transmitter::sendActivity('Follow', $profile['url'], $uid); - Logger::log('Send a new follow request to ' . $profile['url'] . ' for user ' . $uid, Logger::DEBUG); + if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB])) { + Logger::info('Contact had been switched to ActivityPub. Sending a new follow request.', ['uid' => $uid, 'url' => $url]); + ActivityPub\Transmitter::sendActivity('Follow', $url, $uid); } } @@ -706,6 +735,15 @@ class Receiver $object_data['service'] = JsonLD::fetchElement($activity, 'as:instrument', 'as:name', '@type', 'as:Service'); $object_data['service'] = JsonLD::fetchElement($object_data, 'service', '@value'); + if (!empty($object_data['object_id'])) { + // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field. + $objectId = Item::getURIByLink($object_data['object_id']); + if (!empty($objectId) && ($object_data['object_id'] != $objectId)) { + Logger::notice('Fix wrong object-id', ['received' => $object_data['object_id'], 'correct' => $objectId]); + $object_data['object_id'] = $objectId; + } + } + return $object_data; } @@ -717,11 +755,11 @@ class Receiver * @param boolean $trust_source Do we trust the provided object? * @param integer $uid User ID for the signature that we use to fetch data * - * @return array with trusted and valid object data + * @return array|false with trusted and valid object data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function fetchObject($object_id, $object = [], $trust_source = false, $uid = 0) + private static function fetchObject(string $object_id, array $object = [], bool $trust_source = false, int $uid = 0) { // By fetching the type we check if the object is complete. $type = JsonLD::fetchElement($object, '@type'); @@ -755,18 +793,24 @@ class Receiver } if (in_array($type, self::CONTENT_TYPES)) { - return self::processObject($object); + $object_data = self::processObject($object); + + if (!empty($data)) { + $object_data['raw'] = json_encode($data); + } + return $object_data; } if ($type == 'as:Announce') { $object_id = JsonLD::fetchElement($object, 'object', '@id'); - if (empty($object_id)) { + if (empty($object_id) || !is_string($object_id)) { return false; } return self::fetchObject($object_id, [], false, $uid); } Logger::log('Unhandled object type: ' . $type, Logger::DEBUG); + return false; } /** @@ -858,6 +902,40 @@ class Receiver return $attachlist; } + /** + * Fetch the original source or content with the "language" Markdown or HTML + * + * @param array $object + * @param array $object_data + * + * @return array + * @throws \Exception + */ + private static function getSource($object, $object_data) + { + $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/bbcode'); + $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value'); + if (!empty($object_data['source'])) { + return $object_data; + } + + $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/markdown'); + $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value'); + if (!empty($object_data['source'])) { + $object_data['source'] = Markdown::toBBCode($object_data['source']); + return $object_data; + } + + $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/html'); + $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value'); + if (!empty($object_data['source'])) { + $object_data['source'] = HTML::toBBCode($object_data['source']); + return $object_data; + } + + return $object_data; + } + /** * Fetches data from the object part of an activity * @@ -880,6 +958,13 @@ class Receiver // An empty "id" field is translated to "./" by the compactor, so we have to check for this content if (empty($object_data['reply-to-id']) || ($object_data['reply-to-id'] == './')) { $object_data['reply-to-id'] = $object_data['id']; + } else { + // Some systems (e.g. GNU Social) don't reply to the "id" field but the "uri" field. + $replyToId = Item::getURIByLink($object_data['reply-to-id']); + if (!empty($replyToId) && ($object_data['reply-to-id'] != $replyToId)) { + Logger::notice('Fix wrong reply-to', ['received' => $object_data['reply-to-id'], 'correct' => $replyToId]); + $object_data['reply-to-id'] = $replyToId; + } } $object_data['published'] = JsonLD::fetchElement($object, 'as:published', '@value'); @@ -908,8 +993,7 @@ class Receiver $object_data['name'] = JsonLD::fetchElement($object, 'as:name', '@value'); $object_data['summary'] = JsonLD::fetchElement($object, 'as:summary', '@value'); $object_data['content'] = JsonLD::fetchElement($object, 'as:content', '@value'); - $object_data['source'] = JsonLD::fetchElement($object, 'as:source', 'as:content', 'as:mediaType', 'text/bbcode'); - $object_data['source'] = JsonLD::fetchElement($object_data, 'source', '@value'); + $object_data = self::getSource($object, $object_data); $object_data['start-time'] = JsonLD::fetchElement($object, 'as:startTime', '@value'); $object_data['end-time'] = JsonLD::fetchElement($object, 'as:endTime', '@value'); $object_data['location'] = JsonLD::fetchElement($object, 'as:location', 'as:name', '@type', 'as:Place'); @@ -934,12 +1018,9 @@ class Receiver } } - $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags']); - - // Test if it is an answer to a mail - if (DBA::exists('mail', ['uri' => $object_data['reply-to-id']])) { - $object_data['directmessage'] = true; - } + $object_data['receiver'] = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true); + $object_data['unlisted'] = in_array(-1, $object_data['receiver']); + unset($object_data['receiver']['uid:-1']); // Common object data: