X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FProcessor.php;h=26d953985a5634196a4f97a5ac057df54739f490;hb=9ea34370a60cf6eb47aadd4b8a3e64fb282b577c;hp=745a56c2a50f9c35828896eadf02dbf70ad41786;hpb=dd85f48e0de68aa71cb46ff067bd872f0f97ee1e;p=friendica.git diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 745a56c2a5..26d953985a 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -21,6 +21,7 @@ namespace Friendica\Protocol\ActivityPub; +use Friendica\Content\PageInfo; use Friendica\Content\Text\BBCode; use Friendica\Content\Text\HTML; use Friendica\Core\Logger; @@ -34,6 +35,7 @@ use Friendica\Model\Event; use Friendica\Model\Item; use Friendica\Model\ItemURI; use Friendica\Model\Mail; +use Friendica\Model\Search; use Friendica\Model\Tag; use Friendica\Model\User; use Friendica\Protocol\Activity; @@ -41,6 +43,7 @@ use Friendica\Protocol\ActivityPub; use Friendica\Util\DateTimeFormat; use Friendica\Util\JsonLD; use Friendica\Util\Strings; +use Text_LanguageDetect; /** * ActivityPub Processor Protocol class @@ -96,18 +99,16 @@ class Processor foreach ($activity['attachments'] as $attach) { switch ($attach['type']) { case 'link': - // Only one [attachment] tag is allowed - $existingAttachmentPos = strpos($item['body'], '[attachment'); - if ($existingAttachmentPos !== false) { - $linkTitle = $attach['title'] ?: $attach['url']; - // Additional link attachments are prepended before the existing [attachment] tag - $item['body'] = substr_replace($item['body'], "\n[bookmark=" . $attach['url'] . ']' . $linkTitle . "[/bookmark]\n", $existingAttachmentPos, 0); - } else { - // Strip the link preview URL from the end of the body if any - $quotedUrl = preg_quote($attach['url'], '#'); - $item['body'] = preg_replace("#\s*(?:\[bookmark={$quotedUrl}].+?\[/bookmark]|\[url={$quotedUrl}].+?\[/url]|\[url]{$quotedUrl}\[/url]|{$quotedUrl})\s*$#", '', $item['body']); - $item['body'] .= "\n[attachment type='link' url='" . $attach['url'] . "' title='" . htmlspecialchars($attach['title'] ?? '', ENT_QUOTES) . "' image='" . ($attach['image'] ?? '') . "']" . ($attach['desc'] ?? '') . '[/attachment]'; - } + $data = [ + 'url' => $attach['url'], + 'type' => $attach['type'], + 'title' => $attach['title'] ?? '', + 'text' => $attach['desc'] ?? '', + 'image' => $attach['image'] ?? '', + 'images' => [], + 'keywords' => [], + ]; + $item['body'] = PageInfo::appendDataToBody($item['body'], $data); break; default: $filetype = strtolower(substr($attach['mediaType'], 0, strpos($attach['mediaType'], '/'))); @@ -116,10 +117,22 @@ class Processor continue 2; } + $item['body'] .= "\n"; + + // image is the preview/thumbnail URL + if (!empty($attach['image'])) { + $item['body'] .= '[url=' . $attach['url'] . ']'; + $attach['url'] = $attach['image']; + } + if (empty($attach['name'])) { - $item['body'] .= "\n[img]" . $attach['url'] . '[/img]'; + $item['body'] .= '[img]' . $attach['url'] . '[/img]'; } else { - $item['body'] .= "\n[img=" . $attach['url'] . ']' . $attach['name'] . '[/img]'; + $item['body'] .= '[img=' . $attach['url'] . ']' . $attach['name'] . '[/img]'; + } + + if (!empty($attach['image'])) { + $item['body'] .= '[/url]'; } } elseif ($filetype == 'audio') { if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) { @@ -159,7 +172,8 @@ class Processor $item = Item::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity'], ['uri' => $activity['id']]); if (!DBA::isResult($item)) { Logger::warning('No existing item, item will be created', ['uri' => $activity['id']]); - self::createItem($activity); + $item = self::createItem($activity); + self::postItem($activity, $item); return; } @@ -178,6 +192,7 @@ class Processor * Prepares data for a message * * @param array $activity Activity array + * @return array Internal item * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ @@ -193,9 +208,6 @@ class Processor } else { $item['gravity'] = GRAVITY_COMMENT; $item['object-type'] = Activity\ObjectType::COMMENT; - - // Ensure that the comment reaches all receivers of the referring post - $activity['receiver'] = self::addReceivers($activity); } if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) { @@ -205,7 +217,79 @@ class Processor $item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? ''; - self::postItem($activity, $item); + /// @todo What to do with $activity['context']? + if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) { + Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]); + return []; + } + + $item['network'] = Protocol::ACTIVITYPUB; + $item['author-link'] = $activity['author']; + $item['author-id'] = Contact::getIdForURL($activity['author']); + $item['owner-link'] = $activity['actor']; + $item['owner-id'] = Contact::getIdForURL($activity['actor']); + + if (in_array(0, $activity['receiver']) && !empty($activity['unlisted'])) { + $item['private'] = Item::UNLISTED; + } elseif (in_array(0, $activity['receiver'])) { + $item['private'] = Item::PUBLIC; + } else { + $item['private'] = Item::PRIVATE; + } + + if (!empty($activity['raw'])) { + $item['source'] = $activity['raw']; + $item['protocol'] = Conversation::PARCEL_ACTIVITYPUB; + $item['conversation-href'] = $activity['context'] ?? ''; + $item['conversation-uri'] = $activity['conversation'] ?? ''; + + if (isset($activity['push'])) { + $item['direction'] = $activity['push'] ? Conversation::PUSH : Conversation::PULL; + } + } + + $item['isForum'] = false; + + if (!empty($activity['thread-completion'])) { + 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']]); + } + + $item['owner-link'] = $item['author-link']; + $item['owner-id'] = $item['author-id']; + } else { + $actor = APContact::getByURL($item['owner-link'], false); + $item['isForum'] = ($actor['type'] == 'Group'); + } + + $item['uri'] = $activity['id']; + + $item['created'] = DateTimeFormat::utc($activity['published']); + $item['edited'] = DateTimeFormat::utc($activity['updated']); + $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']); + $item['guid'] = $activity['diaspora:guid'] ?: $guid; + + $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); + + $item = self::processContent($activity, $item); + if (empty($item)) { + Logger::info('Message was not processed'); + return []; + } + + $item['plink'] = $activity['alternate-url'] ?? $item['uri']; + + $item = self::constructAttachList($activity, $item); + + return $item; } /** @@ -253,35 +337,6 @@ class Processor } } - /** - * Add users to the receiver list of the given public activity. - * This is used to ensure that the activity will be stored in every thread. - * - * @param array $activity Activity array - * @return array Modified receiver list - */ - private static function addReceivers(array $activity) - { - if (!in_array(0, $activity['receiver'])) { - // Private activities will not be modified - return $activity['receiver']; - } - - // Add all owners of the referring item to the receivers - $original = $receivers = $activity['receiver']; - $items = Item::select(['uid'], ['uri' => $activity['object_id']]); - while ($item = DBA::fetch($items)) { - $receivers['uid:' . $item['uid']] = $item['uid']; - } - DBA::close($items); - - if (count($original) != count($receivers)) { - Logger::info('Improved data', ['id' => $activity['id'], 'object' => $activity['object_id'], 'original' => $original, 'improved' => $receivers]); - } - - return $receivers; - } - /** * Prepare the item array for an activity * @@ -292,7 +347,7 @@ class Processor */ public static function createActivity($activity, $verb) { - $item = []; + $item = self::createItem($activity); $item['verb'] = $verb; $item['thr-parent'] = $activity['object_id']; $item['gravity'] = GRAVITY_ACTIVITY; @@ -300,8 +355,6 @@ class Processor $item['diaspora_signed_text'] = $activity['diaspora:like'] ?? ''; - $activity['receiver'] = self::addReceivers($activity); - self::postItem($activity, $item); } @@ -435,73 +488,14 @@ class Processor * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function postItem($activity, $item) + public static function postItem(array $activity, array $item) { - /// @todo What to do with $activity['context']? - if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) { - Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]); - return; - } - - $item['network'] = Protocol::ACTIVITYPUB; - $item['author-link'] = $activity['author']; - $item['author-id'] = Contact::getIdForURL($activity['author'], 0, false); - $item['owner-link'] = $activity['actor']; - $item['owner-id'] = Contact::getIdForURL($activity['actor'], 0, false); - - 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; - } - } - - $isForum = false; - - if (!empty($activity['thread-completion'])) { - // 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('Ignoring actor because of thread completion.', ['actor' => $item['owner-link']]); - $item['owner-link'] = $item['author-link']; - $item['owner-id'] = $item['author-id']; - } else { - $actor = APContact::getByURL($item['owner-link'], false); - $isForum = ($actor['type'] == 'Group'); - } - - $item['uri'] = $activity['id']; - - $item['created'] = DateTimeFormat::utc($activity['published']); - $item['edited'] = DateTimeFormat::utc($activity['updated']); - $guid = $activity['sc:identifier'] ?: self::getGUIDByURL($item['uri']); - $item['guid'] = $activity['diaspora:guid'] ?: $guid; - - $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); - - $item = self::processContent($activity, $item); if (empty($item)) { return; } - $item['plink'] = $activity['alternate-url'] ?? $item['uri']; - - $item = self::constructAttachList($activity, $item); - $stored = false; + ksort($activity['receiver']); foreach ($activity['receiver'] as $receiver) { if ($receiver == -1) { @@ -510,14 +504,47 @@ class Processor $item['uid'] = $receiver; - if ($isForum) { - $item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver, false); + $type = $activity['reception_type'][$receiver] ?? Receiver::TARGET_UNKNOWN; + switch($type) { + case Receiver::TARGET_TO: + $item['post-type'] = Item::PT_TO; + break; + case Receiver::TARGET_CC: + $item['post-type'] = Item::PT_CC; + break; + case Receiver::TARGET_BTO: + $item['post-type'] = Item::PT_BTO; + break; + case Receiver::TARGET_BCC: + $item['post-type'] = Item::PT_BCC; + break; + case Receiver::TARGET_FOLLOWER: + $item['post-type'] = Item::PT_FOLLOWER; + break; + case Receiver::TARGET_ANSWER: + $item['post-type'] = Item::PT_COMMENT; + break; + case Receiver::TARGET_GLOBAL: + $item['post-type'] = Item::PT_GLOBAL; + break; + default: + $item['post-type'] = Item::PT_ARTICLE; + } + + if (!empty($activity['from-relay'])) { + $item['post-type'] = Item::PT_RELAY; + } elseif (!empty($activity['thread-completion'])) { + $item['post-type'] = Item::PT_FETCHED; + } + + if ($item['isForum'] ?? false) { + $item['contact-id'] = Contact::getIdForURL($activity['actor'], $receiver); } else { - $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver, false); + $item['contact-id'] = Contact::getIdForURL($activity['author'], $receiver); } if (($receiver != 0) && empty($item['contact-id'])) { - $item['contact-id'] = Contact::getIdForURL($activity['author'], 0, false); + $item['contact-id'] = Contact::getIdForURL($activity['author']); } if (!empty($activity['directmessage'])) { @@ -528,7 +555,7 @@ class Processor if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) { $skip = !Contact::isSharingByURL($activity['author'], $receiver); - if ($skip && (($activity['type'] == 'as:Announce') || $isForum)) { + if ($skip && (($activity['type'] == 'as:Announce') || ($item['isForum'] ?? false))) { $skip = !Contact::isSharingByURL($activity['actor'], $receiver); } @@ -677,12 +704,13 @@ class Processor /** * Fetches missing posts * - * @param string $url message URL - * @param array $child activity array with the child of this message + * @param string $url message URL + * @param array $child activity array with the child of this message + * @param string $relay_actor Relay actor * @return string fetched message URL * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function fetchMissingActivity($url, $child = []) + public static function fetchMissingActivity(string $url, array $child = [], string $relay_actor = '') { if (!empty($child['receiver'])) { $uid = ActivityPub\Receiver::getFirstUserFromReceivers($child['receiver']); @@ -701,15 +729,22 @@ class Processor return ''; } - if (!empty($child['author'])) { - $actor = $child['author']; - } elseif (!empty($object['actor'])) { - $actor = $object['actor']; + if (!empty($object['actor'])) { + $object_actor = $object['actor']; } elseif (!empty($object['attributedTo'])) { - $actor = $object['attributedTo']; + $object_actor = $object['attributedTo']; } else { // Shouldn't happen - $actor = ''; + $object_actor = ''; + } + + $signer = [$object_actor]; + + if (!empty($child['author'])) { + $actor = $child['author']; + $signer[] = $actor; + } else { + $actor = $object_actor; } if (!empty($object['published'])) { @@ -733,15 +768,111 @@ class Processor $ldactivity = JsonLD::compact($activity); - $ldactivity['thread-completion'] = true; + 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); + } - ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity)); + if (!empty($relay_actor) && !self::acceptIncomingMessage($ldactivity, $object['id'])) { + return ''; + } + + ActivityPub\Receiver::processActivity($ldactivity, json_encode($activity), $uid, true, false, $signer); Logger::notice('Activity had been fetched and processed.', ['url' => $url, 'object' => $activity['id']]); return $activity['id']; } + /** + * Test if incoming relay messages should be accepted + * + * @param array $activity activity array + * @param string $id object ID + * @return boolean true if message is accepted + */ + private static function acceptIncomingMessage(array $activity, string $id) + { + if (empty($activity['as:object'])) { + Logger::info('No object field in activity - accepted', ['id' => $id]); + return true; + } + + $config = DI::config(); + + $subscribe = $config->get('system', 'relay_subscribe', false); + if ($subscribe) { + $scope = $config->get('system', 'relay_scope', SR_SCOPE_ALL); + } else { + $scope = SR_SCOPE_NONE; + } + + if ($scope == SR_SCOPE_ALL) { + Logger::info('Server accept all posts - accepted', ['id' => $id]); + return true; + } + + $replyto = JsonLD::fetchElement($activity['as:object'], 'as:inReplyTo', '@id'); + if (Item::exists(['uri' => $replyto])) { + Logger::info('Post is a reply to an existing post - accepted', ['id' => $id, 'replyto' => $replyto]); + return true; + } + + if ($scope == SR_SCOPE_NONE) { + Logger::info('Server does not accept relay posts - rejected', ['id' => $id]); + return false; + } + + $messageTags = []; + $tags = Receiver::processTags(JsonLD::fetchElementArray($activity['as:object'], 'as:tag') ?? []); + if (!empty($tags)) { + foreach ($tags as $tag) { + if ($tag['type'] != 'Hashtag') { + continue; + } + $messageTags[] = ltrim(mb_strtolower($tag['name']), '#'); + } + } + + $systemTags = []; + $userTags = []; + + if ($scope == SR_SCOPE_TAGS) { + $server_tags = $config->get('system', 'relay_server_tags', []); + $tagitems = explode(',', mb_strtolower($server_tags)); + + foreach ($tagitems AS $tag) { + $systemTags[] = trim($tag, '# '); + } + + if ($config->get('system', 'relay_user_tags')) { + $userTags = Search::getUserTags(); + } + } + + $content = mb_strtolower(BBCode::toPlaintext(HTML::toBBCode(JsonLD::fetchElement($activity['as:object'], 'as:content', '@value')), false)); + + $tagList = array_unique(array_merge($systemTags, $userTags)); + foreach ($messageTags as $tag) { + if (in_array($tag, $tagList)) { + Logger::info('Subscribed hashtag found - accepted', ['id' => $id, 'hashtag' => $tag]); + return true; + } + // We check with "strpos" for performance issues. Only when this is true, the regular expression check is used + // RegExp is taken from here: https://medium.com/@shiba1014/regex-word-boundaries-with-unicode-207794f6e7ed + if ((strpos($content, $tag) !== false) && preg_match('/(?<=[\s,.:;"\']|^)' . preg_quote($tag, '/') . '(?=[\s,.:;"\']|$)/', $content)) { + Logger::info('Subscribed hashtag found in content - accepted', ['id' => $id, 'hashtag' => $tag]); + return true; + } + } + + Logger::info('No matching hashtags found - rejected', ['id' => $id]); + return false; + } + /** * perform a "follow" request * @@ -757,6 +888,9 @@ class Processor } $owner = User::getOwnerDataById($uid); + if (empty($owner)) { + return; + } $cid = Contact::getIdForURL($activity['actor'], $uid); if (!empty($cid)) { @@ -805,7 +939,7 @@ class Processor } Logger::info('Updating profile', ['object' => $activity['object_id']]); - Contact::updateFromProbeByURL($activity['object_id'], true); + Contact::updateFromProbeByURL($activity['object_id']); } /** @@ -939,6 +1073,9 @@ class Processor } $owner = User::getOwnerDataById($uid); + if (empty($owner)) { + return; + } $cid = Contact::getIdForURL($activity['actor'], $uid); if (empty($cid)) {