X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FProcessor.php;h=aba285c1800e941029d0deaaa2449e610e7477d7;hb=a3c323b366e58c154e5a43f56d85412762e0ac83;hp=95eecabca215af7c42e7158df1815e4e0f33750a;hpb=ea698b670267b230ecd958424720fe29d4b836bc;p=friendica.git diff --git a/src/Protocol/ActivityPub/Processor.php b/src/Protocol/ActivityPub/Processor.php index 95eecabca2..aba285c180 100644 --- a/src/Protocol/ActivityPub/Processor.php +++ b/src/Protocol/ActivityPub/Processor.php @@ -1,6 +1,6 @@ $uriid]; + $data['type'] = Post\Media::UNKNOWN; + $data['url'] = $attachment['url']; + $data['mimetype'] = $attachment['mediaType']; + $data['height'] = $attachment['height'] ?? null; + $data['size'] = $attachment['size'] ?? null; + $data['preview'] = $attachment['image'] ?? null; + $data['description'] = $attachment['name'] ?? null; + + Post\Media::insert($data); + } + + /** + * Stire attachment data * * @param array $activity * @param array $item - * - * @return array array */ - private static function constructAttachList($activity, $item) + private static function storeAttachments($activity, $item) { if (empty($activity['attachments'])) { - return $item; + return; } foreach ($activity['attachments'] as $attach) { - switch ($attach['type']) { - case 'link': - $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'], '/'))); - if ($filetype == 'image') { - if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) { - 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'] .= '[img]' . $attach['url'] . '[/img]'; - } else { - $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'])) { - continue 2; - } - - $item['body'] .= "\n[audio]" . $attach['url'] . '[/audio]'; - } elseif ($filetype == 'video') { - if (!empty($activity['source']) && strpos($activity['source'], $attach['url'])) { - continue 2; - } - - $item['body'] .= "\n[video]" . $attach['url'] . '[/video]'; - } else { - if (!empty($item["attach"])) { - $item["attach"] .= ','; - } else { - $item["attach"] = ''; - } - - $item["attach"] .= '[attach]href="' . $attach['url'] . '" length="' . ($attach['length'] ?? '0') . '" type="' . $attach['mediaType'] . '" title="' . ($attach['name'] ?? '') . '"[/attach]'; - } - } + self::storeAttachmentAsMedia($item['uri-id'], $attach); } - - return $item; } /** @@ -169,7 +158,7 @@ class Processor */ public static function updateItem($activity) { - $item = Item::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity'], ['uri' => $activity['id']]); + $item = Post::selectFirst(['uri', 'uri-id', 'thr-parent', 'gravity', 'post-type'], ['uri' => $activity['id']]); if (!DBA::isResult($item)) { Logger::warning('No existing item, item will be created', ['uri' => $activity['id']]); $item = self::createItem($activity); @@ -181,6 +170,9 @@ class Processor $item['edited'] = DateTimeFormat::utc($activity['updated']); $item = self::processContent($activity, $item); + + self::storeAttachments($activity, $item); + if (empty($item)) { return; } @@ -210,7 +202,7 @@ class Processor $item['object-type'] = Activity\ObjectType::COMMENT; } - if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Item::exists(['uri' => $activity['reply-to-id']])) { + if (empty($activity['directmessage']) && ($activity['id'] != $activity['reply-to-id']) && !Post::exists(['uri' => $activity['reply-to-id']])) { Logger::notice('Parent not found. Try to refetch it.', ['parent' => $activity['reply-to-id']]); self::fetchMissingActivity($activity['reply-to-id'], $activity); } @@ -218,7 +210,7 @@ class Processor $item['diaspora_signed_text'] = $activity['diaspora:comment'] ?? ''; /// @todo What to do with $activity['context']? - if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Item::exists(['uri' => $item['thr-parent']])) { + if (empty($activity['directmessage']) && ($item['gravity'] != GRAVITY_PARENT) && !Post::exists(['uri' => $item['thr-parent']])) { Logger::info('Parent not found, message will be discarded.', ['thr-parent' => $item['thr-parent']]); return []; } @@ -248,14 +240,43 @@ class Processor } } + if (!empty($activity['from-relay'])) { + $item['direction'] = Conversation::RELAY; + } + + 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; + } elseif ($activity['object_type'] == 'as:Video') { + $item['post-type'] = Item::PT_VIDEO; + } else { + $item['post-type'] = Item::PT_NOTE; + } + $item['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']; + 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']]); + } - 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 { @@ -271,6 +292,10 @@ class Processor $item['guid'] = $activity['diaspora:guid'] ?: $guid; $item['uri-id'] = ItemURI::insert(['uri' => $item['uri'], 'guid' => $item['guid']]); + if (empty($item['uri-id'])) { + Logger::warning('Unable to get a uri-id for an item uri', ['uri' => $item['uri'], 'guid' => $item['guid']]); + return []; + } $item = self::processContent($activity, $item); if (empty($item)) { @@ -280,7 +305,20 @@ class Processor $item['plink'] = $activity['alternate-url'] ?? $item['uri']; - $item = self::constructAttachList($activity, $item); + self::storeAttachments($activity, $item); + + // 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); + } + } return $item; } @@ -314,7 +352,7 @@ class Processor } foreach ($activity['receiver'] as $receiver) { - $item = Item::selectFirst(['id', 'uri-id', 'tag', 'origin', 'author-link'], ['uri' => $activity['target_id'], 'uid' => $receiver]); + $item = Post::selectFirst(['id', 'uri-id', 'origin', 'author-link'], ['uri' => $activity['target_id'], 'uid' => $receiver]); if (!DBA::isResult($item)) { // We don't fetch missing content for this purpose continue; @@ -344,6 +382,7 @@ class Processor $item['verb'] = $verb; $item['thr-parent'] = $activity['object_id']; $item['gravity'] = GRAVITY_ACTIVITY; + unset($item['post-type']); $item['object-type'] = Activity\ObjectType::NOTE; $item['diaspora_signed_text'] = $activity['diaspora:like'] ?? ''; @@ -360,20 +399,24 @@ class Processor */ public static function createEvent($activity, $item) { - $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['adjust'] = true; - $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['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['adjust'] = $activity['adjust'] ?? true; + $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']; $condition = ['uri' => $item['uri'], 'uid' => $item['uid']]; $ev = DBA::selectFirst('event', ['id'], $condition); @@ -395,22 +438,36 @@ class Processor */ private static function processContent($activity, $item) { - $item['title'] = HTML::toBBCode($activity['name']); - - if (!empty($activity['source'])) { - $item['body'] = $activity['source']; + 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']); + } - if (!empty($activity['emojis'])) { - $content = self::replaceEmojis($content, $activity['emojis']); - } + if (!empty($activity['languages'])) { + $item['language'] = self::processLanguages($activity['languages']); + } - $content = self::convertMentions($content); + if (!empty($activity['emojis'])) { + $content = self::replaceEmojis($content, $activity['emojis']); + } + + $content = self::convertMentions($content); + if (!empty($activity['source'])) { + $item['body'] = $activity['source']; + $item['raw-body'] = $content; + $item['body'] = Item::improveSharedDataInBody($item); + } else { if (empty($activity['directmessage']) && ($item['thr-parent'] != $item['uri']) && ($item['gravity'] == GRAVITY_COMMENT)) { $item_private = !in_array(0, $activity['item_receiver']); - $parent = Item::selectFirst(['id', 'uri-id', 'private', 'author-link', 'alias'], ['uri' => $item['thr-parent']]); + $parent = Post::selectFirst(['id', 'uri-id', 'private', 'author-link', 'alias'], ['uri' => $item['thr-parent']]); if (!DBA::isResult($parent)) { Logger::warning('Unknown parent item.', ['uri' => $item['thr-parent']]); return false; @@ -423,7 +480,7 @@ class Processor $content = self::removeImplicitMentionsFromBody($content, $parent); } $item['content-warning'] = HTML::toBBCode($activity['summary']); - $item['body'] = $content; + $item['raw-body'] = $item['body'] = $content; } self::storeFromBody($item); @@ -488,6 +545,7 @@ class Processor } $stored = false; + ksort($activity['receiver']); foreach ($activity['receiver'] as $receiver) { if ($receiver == -1) { @@ -499,40 +557,34 @@ class Processor $type = $activity['reception_type'][$receiver] ?? Receiver::TARGET_UNKNOWN; switch($type) { case Receiver::TARGET_TO: - $item['post-type'] = Item::PT_TO; + $item['post-reason'] = Item::PR_TO; break; case Receiver::TARGET_CC: - $item['post-type'] = Item::PT_CC; + $item['post-reason'] = Item::PR_CC; break; case Receiver::TARGET_BTO: - $item['post-type'] = Item::PT_BTO; + $item['post-reason'] = Item::PR_BTO; break; case Receiver::TARGET_BCC: - $item['post-type'] = Item::PT_BCC; + $item['post-reason'] = Item::PR_BCC; break; case Receiver::TARGET_FOLLOWER: - $item['post-type'] = Item::PT_FOLLOWER; + $item['post-reason'] = Item::PR_FOLLOWER; break; case Receiver::TARGET_ANSWER: - $item['post-type'] = Item::PT_COMMENT; + $item['post-reason'] = Item::PR_COMMENT; break; case Receiver::TARGET_GLOBAL: - $item['post-type'] = Item::PT_GLOBAL; + $item['post-reason'] = Item::PR_GLOBAL; break; default: - $item['post-type'] = Item::PT_ARTICLE; - } - - if (in_array($item['post-type'], [Item::PT_COMMENT, Item::PT_GLOBAL, 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; - } + $item['post-reason'] = Item::PR_NONE; } if (!empty($activity['from-relay'])) { - $item['causer-id'] = $activity['from-relay']; + $item['post-reason'] = Item::PR_RELAY; + } elseif (!empty($activity['thread-completion'])) { + $item['post-reason'] = Item::PR_FETCHED; } if ($item['isForum'] ?? false) { @@ -550,6 +602,12 @@ class Processor continue; } + if (!$item['isForum'] && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT) && + ($item['post-reason'] == Item::PR_BCC) && !Contact::isSharingByURL($activity['author'], $receiver)) { + Logger::info('Top level post via BCC from a non follower, ignoring', ['uid' => $receiver, 'contact' => $item['contact-id']]); + continue; + } + if (DI::pConfig()->get($receiver, 'system', 'accept_only_sharer', false) && ($receiver != 0) && ($item['gravity'] == GRAVITY_PARENT)) { $skip = !Contact::isSharingByURL($activity['author'], $receiver); @@ -718,12 +776,12 @@ class Processor $object = ActivityPub::fetchContent($url, $uid); if (empty($object)) { - Logger::log('Activity ' . $url . ' was not fetchable, aborting.'); + Logger::notice('Activity was not fetchable, aborting.', ['url' => $url]); return ''; } if (empty($object['id'])) { - Logger::log('Activity ' . $url . ' has got not id, aborting. ' . json_encode($object)); + Logger::notice('Activity has got not id, aborting. ', ['url' => $url, 'object' => $object]); return ''; } @@ -731,6 +789,10 @@ class Processor $object_actor = $object['actor']; } elseif (!empty($object['attributedTo'])) { $object_actor = $object['attributedTo']; + if (is_array($object_actor)) { + $compacted = JsonLD::compact($object); + $object_actor = JsonLD::fetchElement($compacted, 'as:attributedTo', '@id'); + } } else { // Shouldn't happen $object_actor = ''; @@ -754,7 +816,7 @@ class Processor } $activity = []; - $activity['@context'] = $object['@context']; + $activity['@context'] = $object['@context'] ?? ActivityPub::CONTEXT; unset($object['@context']); $activity['id'] = $object['id']; $activity['to'] = $object['to'] ?? []; @@ -766,8 +828,13 @@ class Processor $ldactivity = JsonLD::compact($activity); - $ldactivity['thread-completion'] = true; - $ldactivity['from-relay'] = Contact::getIdForURL($relay_actor); + 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); + } if (!empty($relay_actor) && !self::acceptIncomingMessage($ldactivity, $object['id'])) { return ''; @@ -794,30 +861,17 @@ class Processor 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]); + $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]); return true; } - if ($scope == SR_SCOPE_NONE) { - Logger::info('Server does not accept relay posts - rejected', ['id' => $id]); - return false; - } + $attributed_to = JsonLD::fetchElement($activity['as:object'], 'as:attributedTo', '@id'); + $authorid = Contact::getIdForURL($attributed_to); + + $body = HTML::toBBCode(JsonLD::fetchElement($activity['as:object'], 'as:content', '@value')); $messageTags = []; $tags = Receiver::processTags(JsonLD::fetchElementArray($activity['as:object'], 'as:tag') ?? []); @@ -830,40 +884,7 @@ class Processor } } - $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; + return Relay::isSolicitedPost($messageTags, $body, $authorid, $id, Protocol::ACTIVITYPUB); } /** @@ -889,20 +910,15 @@ class Processor if (!empty($cid)) { self::switchContact($cid); DBA::update('contact', ['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]); - $contact = DBA::selectFirst('contact', [], ['id' => $cid, 'network' => Protocol::NATIVE_SUPPORT]); - } else { - $contact = []; } $item = ['author-id' => Contact::getIdForURL($activity['actor']), 'author-link' => $activity['actor']]; - $note = Strings::escapeTags(trim($activity['content'] ?? '')); - // Ensure that the contact has got the right network type self::switchContact($item['author-id']); - $result = Contact::addRelationship($owner, $contact, $item, false, $note); + $result = Contact::addRelationship($owner, [], $item, false, $activity['content'] ?? ''); if ($result === true) { ActivityPub\Transmitter::sendContactAccept($item['author-link'], $activity['id'], $owner['uid']); } @@ -916,7 +932,7 @@ class Processor DBA::update('contact', ['hub-verify' => $activity['id'], 'protocol' => Protocol::ACTIVITYPUB], ['id' => $cid]); } - Logger::log('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); + Logger::notice('Follow user ' . $uid . ' from contact ' . $cid . ' with id ' . $activity['id']); } /** @@ -1121,7 +1137,7 @@ class Processor $implicit_mentions = []; if (empty($parent_author['url'])) { - Logger::notice('Author public contact unknown.', ['author-link' => $parent['author-link'], 'item-id' => $parent['id']]); + Logger::notice('Author public contact unknown.', ['author-link' => $parent['author-link'], 'parent-id' => $parent['id']]); } else { $implicit_mentions[] = $parent_author['url']; $implicit_mentions[] = $parent_author['nurl'];