X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FReceiver.php;h=42fcb50865e142740cfbcd9058a7a0d097a620cb;hb=7b871f3ecd8cbb1e10fbe2f13e6380c1e29d2994;hp=cb76ca7e182d477e47ca16d35dbdd4f3e9a54aee;hpb=520cb824ec7be1ddb64f8cd01db7b732c640f4bf;p=friendica.git diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index cb76ca7e18..42fcb50865 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -1,6 +1,6 @@ $data) { $receivers[$key] = $data['uid']; @@ -406,23 +409,27 @@ class Receiver } // Any activities on account types must not be altered - if (in_array($object_type, self::ACCOUNT_TYPES)) { + if (in_array($type, ['as:Flag'])) { + $object_data = []; + $object_data['id'] = JsonLD::fetchElement($activity, '@id'); + $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_data['object_ids'] = JsonLD::fetchElementArray($activity, 'as:object', '@id'); + $object_data['content'] = JsonLD::fetchElement($activity, 'as:content', '@type'); + } elseif (in_array($object_type, self::ACCOUNT_TYPES)) { $object_data = []; $object_data['id'] = JsonLD::fetchElement($activity, '@id'); $object_data['object_id'] = JsonLD::fetchElement($activity, 'as:object', '@id'); $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object'); $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); - $object_data['push'] = $push; if (!$trust_source && ($type == 'as:Delete')) { $apcontact = APContact::getByURL($object_data['object_id'], true); $trust_source = empty($apcontact) || ($apcontact['type'] == 'Tombstone') || $apcontact['suspended']; } - } elseif (in_array($type, ['as:Create', 'as:Update', 'as:Announce', 'as:Invite']) || strpos($type, '#emojiReaction')) { + } elseif (in_array($type, ['as:Create', 'as:Update', 'as:Invite']) || strpos($type, '#emojiReaction')) { // Fetch the content only on activities where this matters // We can receive "#emojiReaction" when fetching content from Hubzilla systems - // Always fetch on "Announce" - $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source && ($type != 'as:Announce'), $fetch_uid); + $object_data = self::fetchObject($object_id, $activity['as:object'], $trust_source, $fetch_uid); if (empty($object_data)) { Logger::info("Object data couldn't be processed"); return []; @@ -430,19 +437,13 @@ class Receiver $object_data['object_id'] = $object_id; - 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'); } - } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Follow', 'litepub:EmojiReact', 'as:View'])) && in_array($object_type, self::CONTENT_TYPES)) { + } elseif (in_array($type, array_merge(self::ACTIVITY_TYPES, ['as:Announce', '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 each individual array element. $object_data = self::processObject($activity); @@ -450,7 +451,6 @@ 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 - $object_data['push'] = $push; } elseif (in_array($type, ['as:Add', 'as:Remove', 'as:Move'])) { $object_data = []; $object_data['id'] = JsonLD::fetchElement($activity, '@id'); @@ -458,7 +458,6 @@ class Receiver $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'); - $object_data['push'] = $push; } else { $object_data = []; $object_data['id'] = JsonLD::fetchElement($activity, '@id'); @@ -466,7 +465,6 @@ class Receiver $object_data['object_actor'] = JsonLD::fetchElement($activity['as:object'], 'as:actor', '@id'); $object_data['object_object'] = JsonLD::fetchElement($activity['as:object'], 'as:object'); $object_data['object_type'] = JsonLD::fetchElement($activity['as:object'], '@type'); - $object_data['push'] = $push; // An Undo is done on the object of an object, so we need that type as well if (($type == 'as:Undo') && !empty($object_data['object_object'])) { @@ -481,6 +479,8 @@ class Receiver } } + $object_data['push'] = $push; + $object_data = self::addActivityFields($object_data, $activity); if (empty($object_data['object_type'])) { @@ -643,7 +643,9 @@ class Receiver } } - if (DI::config()->get('system', 'decoupled_receiver') && ($trust_source || DI::config()->get('debug', 'ap_inbox_store_untrusted'))) { + $decouple = DI::config()->get('system', 'decoupled_receiver') && !in_array($completion, [self::COMPLETION_MANUAL, self::COMPLETION_ANNOUCE]); + + if ($decouple && ($trust_source || DI::config()->get('debug', 'ap_inbox_store_untrusted'))) { $object_data = Queue::add($object_data, $type, $uid, $http_signer, $push, $trust_source); } @@ -652,7 +654,7 @@ class Receiver return true; } - if (!empty($object_data['entry-id']) && DI::config()->get('system', 'decoupled_receiver') && ($push || ($completion == self::COMPLETION_RELAY))) { + if (!empty($object_data['entry-id']) && $decouple && ($push || ($completion == self::COMPLETION_RELAY))) { if (Queue::isProcessable($object_data['entry-id'])) { // We delay by 5 seconds to allow to accumulate all receivers $delayed = date(DateTimeFormat::MYSQL, time() + 5); @@ -669,7 +671,7 @@ class Receiver $object_data['recursion-depth'] = $activity['recursion-depth']; } - if (!self::routeActivities($object_data, $type, $push)) { + if (!self::routeActivities($object_data, $type, $push, true, $uid)) { self::storeUnhandledActivity(true, $type, $object_data, $activity, $body, $uid, $trust_source, $push, $signer); Queue::remove($object_data); } @@ -683,13 +685,12 @@ class Receiver * @param string $type * @param bool $push * @param bool $fetch_parents + * @param int $uid * * @return boolean Could the activity be routed? */ - public static function routeActivities(array $object_data, string $type, bool $push, bool $fetch_parents = true): bool + public static function routeActivities(array $object_data, string $type, bool $push, bool $fetch_parents = true, int $uid = 0): bool { - $activity = $object_data['object_activity'] ?? []; - switch ($type) { case 'as:Create': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { @@ -719,8 +720,8 @@ class Receiver ActivityPub\Processor::addTag($object_data); } elseif (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::addToFeaturedCollection($object_data); - } elseif ($object_data['object_type'] == '') { - // The object type couldn't be determined. We don't have it and we can't fetch it. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -729,43 +730,24 @@ class Receiver case 'as:Announce': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { - $actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); - $object_data['thread-completion'] = Contact::getIdForURL($actor); - $object_data['completion-mode'] = self::COMPLETION_ANNOUCE; - - if (!Post::exists(['uri' => $object_data['id'], 'uid' => 0])) { - $item = ActivityPub\Processor::createItem($object_data, $fetch_parents); - if (empty($item)) { - Logger::debug('announced id was not created', ['id' => $object_data['id']]); + if (!Item::searchByLink($object_data['object_id'], $uid)) { + if (ActivityPub\Processor::fetchMissingActivity($object_data['object_id'], [], $object_data['actor'], self::COMPLETION_ANNOUCE, $uid)) { + Logger::debug('Created announced id', ['uid' => $uid, 'id' => $object_data['object_id']]); + Queue::remove($object_data); + } else { + Logger::debug('Announced id was not created', ['uid' => $uid, 'id' => $object_data['object_id']]); Queue::remove($object_data); return true; } - - $item['post-reason'] = Item::PR_ANNOUNCEMENT; - ActivityPub\Processor::postItem($object_data, $item); - Logger::debug('Created announced id', ['id' => $object_data['id']]); } else { - Logger::info('Announced id already exists', ['id' => $object_data['id']]); + Logger::info('Announced id already exists', ['uid' => $uid, 'id' => $object_data['object_id']]); Queue::remove($object_data); } - if (!empty($activity)) { - $announce_object_data = self::processObject($activity); - $announce_object_data['name'] = $type; - $announce_object_data['author'] = $actor; - $announce_object_data['object_id'] = $object_data['id']; - $announce_object_data['object_type'] = $object_data['object_type']; - $announce_object_data['push'] = $push; - Logger::debug('Create announce activity', ['id' => $announce_object_data['id'], 'object_data' => $announce_object_data]); - - if (!empty($object_data['raw'])) { - $announce_object_data['raw'] = $object_data['raw']; - } - if (!empty($object_data['raw-object'])) { - $announce_object_data['raw-object'] = $object_data['raw-object']; - } - ActivityPub\Processor::createActivity($announce_object_data, Activity::ANNOUNCE); - } + ActivityPub\Processor::createActivity($object_data, Activity::ANNOUNCE); + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. + Queue::remove($object_data); } else { return false; } @@ -774,8 +756,8 @@ class Receiver case 'as:Like': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::createActivity($object_data, Activity::LIKE); - } elseif (in_array($object_data['object_type'], ['', 'as:Tombstone'])) { - // The object type couldn't be determined. We don't have it and we can't fetch it. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -785,8 +767,8 @@ class Receiver case 'as:Dislike': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::createActivity($object_data, Activity::DISLIKE); - } elseif (in_array($object_data['object_type'], ['', 'as:Tombstone'])) { - // The object type couldn't be determined. We don't have it and we can't fetch it. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -834,7 +816,7 @@ class Receiver return false; } break; - + case 'as:Block': if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) { ActivityPub\Processor::blockAccount($object_data); @@ -843,11 +825,19 @@ class Receiver } break; + case 'as:Flag': + if (in_array($object_data['object_type'], self::ACCOUNT_TYPES)) { + ActivityPub\Processor::ReportAccount($object_data); + } else { + return false; + } + break; + case 'as:Remove': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::removeFromFeaturedCollection($object_data); - } elseif ($object_data['object_type'] == '') { - // The object type couldn't be determined. We don't have it and we can't fetch it. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -919,6 +909,9 @@ class Receiver } elseif (in_array($object_data['object_type'], ['as:Delete'])) { // We cannot undo deletions, so we just ignore this Queue::remove($object_data); + } elseif (in_array($object_data['object_object_type'], ['as:Tombstone'])) { + // The object is a tombstone, we ignore any actions on it. + Queue::remove($object_data); } else { return false; } @@ -927,8 +920,18 @@ class Receiver case 'as:View': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::createActivity($object_data, Activity::VIEW); - } elseif ($object_data['object_type'] == '') { - // The object type couldn't be determined. Most likely we don't have it here. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. + Queue::remove($object_data); + } else { + return false; + } + break; + case 'as:Read': + if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { + ActivityPub\Processor::createActivity($object_data, Activity::READ); + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -938,8 +941,8 @@ class Receiver case 'litepub:EmojiReact': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { ActivityPub\Processor::createActivity($object_data, Activity::EMOJIREACT); - } elseif ($object_data['object_type'] == '') { - // The object type couldn't be determined. We don't have it and we can't fetch it. We ignore this activity. + } elseif (in_array($object_data['object_type'], ['as:Tombstone', ''])) { + // We don't have the object here or it is deleted. We ignore this activity. Queue::remove($object_data); } else { return false; @@ -1001,7 +1004,7 @@ class Receiver $uid = 0; $actor = JsonLD::fetchElement($activity, 'as:actor', '@id') ?? ''; - $receivers = self::getReceivers($activity, $actor); + $receivers = self::getReceivers($activity, $actor, [], false, false); foreach ($receivers as $receiver) { if ($receiver['type'] == self::TARGET_GLOBAL) { return 0; @@ -1034,6 +1037,10 @@ class Receiver } foreach ($receiver_list as $receiver) { + if ($receiver == 'Public') { + Logger::warning('Not compacted public collection found', ['activity' => $activity, 'callstack' => System::callstack(20)]); + $receiver = ActivityPub::PUBLIC_COLLECTION; + } if ($receiver == self::PUBLIC_COLLECTION) { $receiver = ActivityPub::PUBLIC_COLLECTION; } @@ -1048,14 +1055,15 @@ class Receiver * Fetch the receiver list from an activity array * * @param array $activity - * @param string $actor - * @param array $tags - * @param boolean $fetch_unlisted + * @param string $actor + * @param array $tags + * @param bool $fetch_unlisted + * @param bool $push * * @return array with receivers (user id) * @throws \Exception */ - private static function getReceivers(array $activity, string $actor, array $tags = [], bool $fetch_unlisted = false): array + private static function getReceivers(array $activity, string $actor, array $tags, bool $fetch_unlisted, bool $push): array { $reply = $receivers = $profile = []; @@ -1089,6 +1097,9 @@ class Receiver $profile = APContact::getByURL($actor); $followers = $profile['followers'] ?? ''; $is_forum = ($actor['type'] ?? '') == 'Group'; + if ($push) { + Contact::updateByUrlIfNeeded($actor); + } Logger::info('Got actor and followers', ['actor' => $actor, 'followers' => $followers]); } else { Logger::info('Empty actor', ['activity' => $activity]); @@ -1435,14 +1446,6 @@ class Receiver return $object_data; } - if ($type == 'as:Announce') { - $object_id = JsonLD::fetchElement($object, 'object', '@id'); - if (empty($object_id) || !is_string($object_id)) { - return false; - } - return self::fetchObject($object_id, [], false, $uid); - } - Logger::info('Unhandled object type: ' . $type); return false; } @@ -1850,6 +1853,52 @@ class Receiver return false; } + $object_data = self::getObjectDataFromActivity($object); + + $receiverdata = self::getReceivers($object, $object_data['actor'] ?? '', $object_data['tags'], true, false); + $receivers = $reception_types = []; + foreach ($receiverdata as $key => $data) { + $receivers[$key] = $data['uid']; + $reception_types[$data['uid']] = $data['type'] ?? 0; + } + + $object_data['receiver_urls'] = self::getReceiverURL($object); + $object_data['receiver'] = $receivers; + $object_data['reception_type'] = $reception_types; + + $object_data['unlisted'] = in_array(-1, $object_data['receiver']); + unset($object_data['receiver'][-1]); + unset($object_data['reception_type'][-1]); + + return $object_data; + } + + /** + * Fetches data from the object part of an client to server activity + * + * @param array $object + * + * @return array Object data + */ + private static function processC2SObject(array $object): array + { + $object_data = self::getObjectDataFromActivity($object); + + $object_data['target'] = self::getTargets($object, $object_data['actor'] ?? ''); + $object_data['receiver'] = []; + + return $object_data; + } + + /** + * Create an object data array from a given activity + * + * @param array $object + * + * @return array Object data + */ + private static function getObjectDataFromActivity(array $object): array + { $object_data = []; $object_data['object_type'] = JsonLD::fetchElement($object, '@type'); $object_data['id'] = JsonLD::fetchElement($object, '@id'); @@ -1860,7 +1909,7 @@ class Receiver $object_data['reply-to-id'] = $object_data['id']; // On activities the "reply to" is the id of the object it refers to - if (in_array($object_data['object_type'], self::ACTIVITY_TYPES)) { + if (in_array($object_data['object_type'], array_merge(self::ACTIVITY_TYPES, ['as:Announce']))) { $object_id = JsonLD::fetchElement($object, 'as:object', '@id'); if (!empty($object_id)) { $object_data['reply-to-id'] = $object_id; @@ -1978,21 +2027,6 @@ class Receiver $object_data['question'] = self::processQuestion($object); } - $receiverdata = self::getReceivers($object, $object_data['actor'] ?? '', $object_data['tags'], true); - $receivers = $reception_types = []; - foreach ($receiverdata as $key => $data) { - $receivers[$key] = $data['uid']; - $reception_types[$data['uid']] = $data['type'] ?? 0; - } - - $object_data['receiver_urls'] = self::getReceiverURL($object); - $object_data['receiver'] = $receivers; - $object_data['reception_type'] = $reception_types; - - $object_data['unlisted'] = in_array(-1, $object_data['receiver']); - unset($object_data['receiver'][-1]); - unset($object_data['reception_type'][-1]); - return $object_data; } @@ -2020,4 +2054,190 @@ class Receiver { return DBA::exists('arrived-activity', ['object-id' => $id]); } + + /** + * Process client to server activities + * + * @param array $activity + * @param integer $uid + * @param array $application + * @return array + */ + public static function processC2SActivity(array $activity, int $uid, array $application): array + { + $ldactivity = JsonLD::compact($activity); + if (empty($ldactivity)) { + Logger::notice('Invalid activity', ['activity' => $activity, 'uid' => $uid]); + return []; + } + + $type = JsonLD::fetchElement($ldactivity, '@type'); + if (!$type) { + Logger::notice('Empty type', ['activity' => $ldactivity, 'uid' => $uid]); + return []; + } + + $object_id = JsonLD::fetchElement($ldactivity, 'as:object', '@id') ?? ''; + $object_type = self::fetchObjectType($ldactivity, $object_id, $uid); + if (!$object_type && !$object_id) { + Logger::notice('Empty object type or id', ['activity' => $ldactivity, 'uid' => $uid]); + return []; + } + + Logger::debug('Processing activity', ['type' => $type, 'object_type' => $object_type, 'object_id' => $object_id, 'activity' => $ldactivity]); + return self::routeC2SActivities($type, $object_type, $object_id, $uid, $application, $ldactivity); + } + + /** + * Accumulate the targets and visibility of this post + * + * @param array $object + * @param string $actor + * @return array + */ + private static function getTargets(array $object, string $actor): array + { + $profile = APContact::getByURL($actor); + $followers = $profile['followers']; + + $targets = []; + + foreach (['as:to', 'as:cc', 'as:bto', 'as:bcc'] as $element) { + switch ($element) { + case 'as:to': + $type = self::TARGET_TO; + break; + case 'as:cc': + $type = self::TARGET_CC; + break; + case 'as:bto': + $type = self::TARGET_BTO; + break; + case 'as:bcc': + $type = self::TARGET_BCC; + break; + } + $receiver_list = JsonLD::fetchElementArray($object, $element, '@id'); + if (empty($receiver_list)) { + continue; + } + + foreach ($receiver_list as $receiver) { + if ($receiver == self::PUBLIC_COLLECTION) { + $targets[self::TARGET_GLOBAL] = ($element == 'as:to'); + continue; + } + + if ($receiver == $followers) { + $targets[self::TARGET_FOLLOWER] = true; + continue; + } + $targets[$type][] = Contact::getIdForURL($receiver); + } + } + return $targets; + } + + /** + * Route client to server activities + * + * @param string $type + * @param string $object_type + * @param string $object_id + * @param integer $uid + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function routeC2SActivities(string $type, string $object_type, string $object_id, int $uid, array $application, array $ldactivity): array + { + switch ($type) { + case 'as:Create': + if (in_array($object_type, self::CONTENT_TYPES)) { + return self::createContent($uid, $application, $ldactivity); + } + break; + case 'as:Update': + if (in_array($object_type, self::CONTENT_TYPES) && !empty($object_id)) { + return self::updateContent($uid, $object_id, $application, $ldactivity); + } + break; + case 'as:Follow': + if (in_array($object_type, self::ACCOUNT_TYPES) && !empty($object_id)) { + return self::followAccount($uid, $object_id, $ldactivity); + } + break; + } + return []; + } + + /** + * Create a new post or comment + * + * @param integer $uid + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function createContent(int $uid, array $application, array $ldactivity): array + { + $object_data = self::processC2SObject($ldactivity['as:object']); + $item = Processor::processC2SContent($object_data, $application, $uid); + Logger::debug('Got data', ['item' => $item, 'object' => $object_data]); + + $id = Item::insert($item, true); + if (!empty($id)) { + $item = Post::selectFirst(['uri-id'], ['id' => $id]); + if (!empty($item['uri-id'])) { + return Transmitter::createActivityFromItem($id); + } + } + return []; + } + + /** + * Update an existing post or comment + * + * @param integer $uid + * @param string $object_id + * @param array $application + * @param array $ldactivity + * @return array + */ + private static function updateContent(int $uid, string $object_id, array $application, array $ldactivity):array + { + $id = Item::fetchByLink($object_id, $uid); + $original_post = Post::selectFirst(['uri-id'], ['uid' => $uid, 'origin' => true, 'id' => $id]); + if (empty($original_post)) { + Logger::debug('Item not found or does not belong to the user', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + return []; + } + + $object_data = self::processC2SObject($ldactivity['as:object']); + $item = Processor::processC2SContent($object_data, $application, $uid); + if (empty($item['title']) && empty($item['body'])) { + Logger::debug('Empty body and title', ['id' => $id, 'uid' => $uid, 'object_id' => $object_id, 'activity' => $ldactivity]); + return []; + } + $post = ['title' => $item['title'], 'body' => $item['body']]; + Logger::debug('Got data', ['id' => $id, 'uid' => $uid, 'item' => $post]); + Item::update($post, ['id' => $id]); + Item::updateDisplayCache($original_post['uri-id']); + + return Transmitter::createActivityFromItem($id); + } + + /** + * Follow a given account + * @todo Check the expected return value + * + * @param integer $uid + * @param string $object_id + * @param array $ldactivity + * @return array + */ + private static function followAccount(int $uid, string $object_id, array $ldactivity): array + { + return []; + } }