X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FReceiver.php;h=4504cee4c49367b7e587d0b2d767ba21a0e55aff;hb=3d8e82d95d9cc76b45a8db301b22c4111f335e1c;hp=a8c997c60c81ce74227b8062dfb94d5bc7212351;hpb=688e056ad116ff9fcd6c3df815e6e7856c943f70;p=friendica.git diff --git a/src/Protocol/ActivityPub/Receiver.php b/src/Protocol/ActivityPub/Receiver.php index a8c997c60c..4504cee4c4 100644 --- a/src/Protocol/ActivityPub/Receiver.php +++ b/src/Protocol/ActivityPub/Receiver.php @@ -1,6 +1,6 @@ $http_signer]); } @@ -125,28 +117,28 @@ class Receiver if (LDSignature::isSigned($activity)) { $ld_signer = LDSignature::getSigner($activity); if (empty($ld_signer)) { - Logger::log('Invalid JSON-LD signature from ' . $actor, Logger::DEBUG); + Logger::info('Invalid JSON-LD signature from ' . $actor); } elseif ($ld_signer != $http_signer) { $signer[] = $ld_signer; } if (!empty($ld_signer && ($actor == $http_signer))) { - Logger::log('The HTTP and the JSON-LD signature belong to ' . $ld_signer, Logger::DEBUG); + Logger::info('The HTTP and the JSON-LD signature belong to ' . $ld_signer); $trust_source = true; } elseif (!empty($ld_signer)) { - Logger::log('JSON-LD signature is signed by ' . $ld_signer, Logger::DEBUG); + Logger::info('JSON-LD signature is signed by ' . $ld_signer); $trust_source = true; } elseif ($actor == $http_signer) { - Logger::log('Bad JSON-LD signature, but HTTP signer fits the actor.', Logger::DEBUG); + Logger::info('Bad JSON-LD signature, but HTTP signer fits the actor.'); $trust_source = true; } else { - Logger::log('Invalid JSON-LD signature and the HTTP signer is different.', Logger::DEBUG); + Logger::info('Invalid JSON-LD signature and the HTTP signer is different.'); $trust_source = false; } } elseif ($actor == $http_signer) { - Logger::log('Trusting post without JSON-LD signature, The actor fits the HTTP signer.', Logger::DEBUG); + Logger::info('Trusting post without JSON-LD signature, The actor fits the HTTP signer.'); $trust_source = true; } else { - Logger::log('No JSON-LD signature, different actor.', Logger::DEBUG); + Logger::info('No JSON-LD signature, different actor.'); $trust_source = false; } @@ -318,7 +310,7 @@ class Receiver $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); if (empty($object_id)) { - Logger::log('No object found', Logger::DEBUG); + Logger::info('No object found'); return []; } @@ -329,12 +321,36 @@ class Receiver $object_type = self::fetchObjectType($activity, $object_id, $uid); + // Fetch the activity on Lemmy "Announce" messages (announces of activities) + if (($type == 'as:Announce') && in_array($object_type, array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) { + $data = ActivityPub::fetchContent($object_id, $uid); + if (!empty($data)) { + $type = $object_type; + $activity = JsonLD::compact($data); + + // Some variables need to be refetched since the activity changed + $actor = JsonLD::fetchElement($activity, 'as:actor', '@id'); + $object_id = JsonLD::fetchElement($activity, 'as:object', '@id'); + $object_type = self::fetchObjectType($activity, $object_id, $uid); + } + } + + // Any activities on account types must not be altered + if (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; + } elseif (in_array($type, ['as:Create', 'as:Update', 'as:Announce']) || strpos($type, '#emojiReaction')) { // Fetch the content only on activities where this matters - if (in_array($type, ['as:Create', 'as:Update', 'as:Announce'])) { + // 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'), $uid); if (empty($object_data)) { - Logger::log("Object data couldn't be processed", Logger::DEBUG); + Logger::info("Object data couldn't be processed"); return []; } @@ -354,7 +370,7 @@ class Receiver } } 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. + // This way we later don't have to check for the existence of each individual array element. $object_data = self::processObject($activity); $object_data['name'] = $type; $object_data['author'] = JsonLD::fetchElement($activity, 'as:actor', '@id'); @@ -408,7 +424,7 @@ class Receiver } } - Logger::log('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id'], Logger::DEBUG); + Logger::info('Processing ' . $object_data['type'] . ' ' . $object_data['object_type'] . ' ' . $object_data['id']); return $object_data; } @@ -480,6 +496,12 @@ class Receiver return; } + // Lemmy is announcing activities. + // We are changing the announces into regular activities. + if (($type == 'as:Announce') && in_array($object_data['type'] ?? '', array_merge(self::ACTIVITY_TYPES, ['as:Delete', 'as:Undo', 'as:Update']))) { + $type = $object_data['type']; + } + if (!$trust_source) { Logger::info('Activity trust could not be achieved.', ['id' => $object_data['object_id'], 'type' => $type, 'signer' => $signer, 'actor' => $actor, 'attributedTo' => $attributed_to]); return; @@ -498,7 +520,7 @@ class Receiver if (!empty($activity['from-relay'])) { $object_data['from-relay'] = $activity['from-relay']; } - + switch ($type) { case 'as:Create': if (in_array($object_data['object_type'], self::CONTENT_TYPES)) { @@ -613,7 +635,7 @@ class Receiver break; default: - Logger::log('Unknown activity: ' . $type . ' ' . $object_data['object_type'], Logger::DEBUG); + Logger::info('Unknown activity: ' . $type . ' ' . $object_data['object_type']); break; } } @@ -624,7 +646,7 @@ class Receiver * @param array $activity * @param string $actor * @param array $tags - * @param boolean $fetch_unlisted + * @param boolean $fetch_unlisted * * @return array with receivers (user id) * @throws \Exception @@ -660,13 +682,14 @@ class Receiver } if (!empty($actor)) { - $profile = APContact::getByURL($actor); + $profile = APContact::getByURL($actor); $followers = $profile['followers'] ?? ''; - - Logger::log('Actor: ' . $actor . ' - Followers: ' . $followers, Logger::DEBUG); + $is_forum = ($actor['type'] ?? '') == 'Group'; + Logger::info('Got actor and followers', ['actor' => $actor, 'followers' => $followers]); } else { Logger::info('Empty actor', ['activity' => $activity]); $followers = ''; + $is_forum = false; } // We have to prevent false follower assumptions upon thread completions @@ -683,13 +706,13 @@ class Receiver $receivers[0] = ['uid' => 0, 'type' => self::TARGET_GLOBAL]; } - // Add receiver "-1" for unlisted posts + // Add receiver "-1" for unlisted posts if ($fetch_unlisted && ($receiver == self::PUBLIC_COLLECTION) && ($element == 'as:cc')) { $receivers[-1] = ['uid' => -1, 'type' => self::TARGET_GLOBAL]; } // Fetch the receivers for the public and the followers collection - if (in_array($receiver, [$followers, self::PUBLIC_COLLECTION]) && !empty($actor)) { + if ((($receiver == $followers) || (($receiver == self::PUBLIC_COLLECTION) && !$is_forum)) && !empty($actor)) { $receivers = self::getReceiverForActor($actor, $tags, $receivers, $follower_target); continue; } @@ -927,16 +950,16 @@ class Receiver $data = ActivityPub::fetchContent($object_id, $uid); if (!empty($data)) { $object = JsonLD::compact($data); - Logger::log('Fetched content for ' . $object_id, Logger::DEBUG); + Logger::info('Fetched content for ' . $object_id); } else { - Logger::log('Empty content for ' . $object_id . ', check if content is available locally.', Logger::DEBUG); + Logger::info('Empty content for ' . $object_id . ', check if content is available locally.'); $item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri' => $object_id]); if (!DBA::isResult($item)) { - Logger::log('Object with url ' . $object_id . ' was not found locally.', Logger::DEBUG); + Logger::info('Object with url ' . $object_id . ' was not found locally.'); return false; } - Logger::log('Using already stored item for url ' . $object_id, Logger::DEBUG); + Logger::info('Using already stored item for url ' . $object_id); $data = ActivityPub\Transmitter::createNote($item); $object = JsonLD::compact($data); } @@ -946,13 +969,13 @@ class Receiver Logger::info('Empty id'); return false; } - + if ($id != $object_id) { Logger::info('Fetched id differs from provided id', ['provided' => $object_id, 'fetched' => $id]); return false; } } else { - Logger::log('Using original object for url ' . $object_id, Logger::DEBUG); + Logger::info('Using original object for url ' . $object_id); } $type = JsonLD::fetchElement($object, '@type'); @@ -961,6 +984,18 @@ class Receiver return false; } + // Lemmy is resharing "create" activities instead of content + // We fetch the content from the activity. + if (in_array($type, ['as:Create'])) { + $object = $object['as:object']; + $type = JsonLD::fetchElement($object, '@type'); + if (empty($type)) { + Logger::info('Empty type'); + return false; + } + $object_data = self::processObject($object); + } + // We currently don't handle 'pt:CacheFile', but with this step we avoid logging if (in_array($type, self::CONTENT_TYPES) || ($type == 'pt:CacheFile')) { $object_data = self::processObject($object); @@ -979,7 +1014,7 @@ class Receiver return self::fetchObject($object_id, [], false, $uid); } - Logger::log('Unhandled object type: ' . $type, Logger::DEBUG); + Logger::info('Unhandled object type: ' . $type); return false; } @@ -1108,14 +1143,6 @@ class Receiver 'image' => $pageImage, ]; break; - case 'as:Link': - $attachlist[] = [ - 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), - 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'), - 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), - 'url' => JsonLD::fetchElement($attachment, 'as:href', '@id') - ]; - break; case 'as:Image': $mediaType = JsonLD::fetchElement($attachment, 'as:mediaType', '@value'); $imageFullUrl = JsonLD::fetchElement($attachment, 'as:url', '@id'); @@ -1177,7 +1204,10 @@ class Receiver 'type' => str_replace('as:', '', JsonLD::fetchElement($attachment, '@type')), 'mediaType' => JsonLD::fetchElement($attachment, 'as:mediaType', '@value'), 'name' => JsonLD::fetchElement($attachment, 'as:name', '@value'), - 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id') + 'url' => JsonLD::fetchElement($attachment, 'as:url', '@id'), + 'height' => JsonLD::fetchElement($attachment, 'as:height', '@value'), + 'width' => JsonLD::fetchElement($attachment, 'as:width', '@value'), + 'image' => JsonLD::fetchElement($attachment, 'as:image', '@id') ]; } } @@ -1219,30 +1249,60 @@ class Receiver return $object_data; } + /** + * Extracts a potential alternate URL from a list of additional URL elements + * + * @param array $urls + * @return string + */ + private static function extractAlternateUrl(array $urls): string + { + $alternateUrl = ''; + foreach ($urls as $key => $url) { + // Not a list but a single URL element + if (!is_numeric($key)) { + continue; + } + + if (empty($url['@type']) || ($url['@type'] != 'as:Link')) { + continue; + } + + $href = JsonLD::fetchElement($url, 'as:href', '@id'); + if (empty($href)) { + continue; + } + + $mediatype = JsonLD::fetchElement($url, 'as:mediaType'); + if (empty($mediatype)) { + continue; + } + + if ($mediatype == 'text/html') { + $alternateUrl = $href; + } + } + + return $alternateUrl; + } + /** * Check if the "as:url" element is an array with multiple links * This is the case with audio and video posts. * Then the links are added as attachments * - * @param array $object The raw object - * @param array $object_data The parsed object data for later processing - * @return array the object data + * @param array $urls The object URL list + * @return array an array of attachments */ - private static function processAttachmentUrls(array $object, array $object_data) { - // Check if this is some url with multiple links - if (empty($object['as:url'])) { - return $object_data; - } - - $urls = $object['as:url']; - $keys = array_keys($urls); - if (!is_numeric(array_pop($keys))) { - return $object_data; - } - + private static function processAttachmentUrls(array $urls): array + { $attachments = []; + foreach ($urls as $key => $url) { + // Not a list but a single URL element + if (!is_numeric($key)) { + continue; + } - foreach ($urls as $url) { if (empty($url['@type']) || ($url['@type'] != 'as:Link')) { continue; } @@ -1257,26 +1317,19 @@ class Receiver continue; } - if ($mediatype == 'text/html') { - $object_data['alternate-url'] = $href; - } - $filetype = strtolower(substr($mediatype, 0, strpos($mediatype, '/'))); if ($filetype == 'audio') { - $attachments[$filetype] = ['type' => $mediatype, 'url' => $href, 'height' => null, 'size' => null]; + $attachments[] = ['type' => $filetype, 'mediaType' => $mediatype, 'url' => $href, 'height' => null, 'size' => null, 'name' => '']; } elseif ($filetype == 'video') { $height = (int)JsonLD::fetchElement($url, 'as:height', '@value'); - $size = (int)JsonLD::fetchElement($url, 'pt:size', '@value'); - - // We save bandwidth by using a moderate height (alt least 480 pixel height) - // Peertube normally uses these heights: 240, 360, 480, 720, 1080 - if (!empty($attachments[$filetype]['height']) && - ($height > $attachments[$filetype]['height']) && ($attachments[$filetype]['height'] >= 480)) { + // PeerTube audio-only track + if ($height === 0) { continue; } - $attachments[$filetype] = ['type' => $mediatype, 'url' => $href, 'height' => $height, 'size' => $size]; + $size = (int)JsonLD::fetchElement($url, 'pt:size', '@value'); + $attachments[] = ['type' => $filetype, 'mediaType' => $mediatype, 'url' => $href, 'height' => $height, 'size' => $size, 'name' => '']; } elseif (in_array($mediatype, ['application/x-bittorrent', 'application/x-bittorrent;x-scheme-handler/magnet'])) { $height = (int)JsonLD::fetchElement($url, 'as:height', '@value'); @@ -1285,19 +1338,14 @@ class Receiver continue; } - $attachments[$mediatype] = ['type' => $mediatype, 'url' => $href, 'height' => $height, 'size' => null]; + $attachments[$mediatype] = ['type' => $mediatype, 'mediaType' => $mediatype, 'url' => $href, 'height' => $height, 'size' => null, 'name' => '']; + } elseif ($mediatype == 'application/x-mpegURL') { + // PeerTube exception, actual video link is in the tags of this URL element + $attachments = array_merge($attachments, self::processAttachmentUrls($url['as:tag'])); } } - foreach ($attachments as $type => $attachment) { - $object_data['attachments'][] = ['type' => $type, - 'mediaType' => $attachment['type'], - 'height' => $attachment['height'], - 'size' => $attachment['size'], - 'name' => '', - 'url' => $attachment['url']]; - } - return $object_data; + return array_values($attachments); } /** @@ -1322,6 +1370,14 @@ 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']; + + // On activities the "reply to" is the id of the object it refers to + if (in_array($object_data['object_type'], self::ACTIVITY_TYPES)) { + $object_id = JsonLD::fetchElement($object, 'as:object', '@id'); + if (!empty($object_id)) { + $object_data['reply-to-id'] = $object_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']); @@ -1353,8 +1409,7 @@ class Receiver // Some AP software allow formatted text in post location, so we run all the text converters we have to boil // down to HTML and then finally format to plaintext. $location = Markdown::convert($location); - $location = BBCode::convert($location); - $location = HTML::toPlaintext($location); + $location = BBCode::toPlaintext($location); } $object_data['sc:identifier'] = JsonLD::fetchElement($object, 'sc:identifier', '@value'); @@ -1395,7 +1450,8 @@ class Receiver } if (in_array($object_data['object_type'], ['as:Audio', 'as:Video'])) { - $object_data = self::processAttachmentUrls($object, $object_data); + $object_data['alternate-url'] = self::extractAlternateUrl($object['as:url'] ?? []) ?: $object_data['alternate-url']; + $object_data['attachments'] = array_merge($object_data['attachments'], self::processAttachmentUrls($object['as:url'] ?? [])); } $receiverdata = self::getReceivers($object, $object_data['actor'], $object_data['tags'], true);