X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FTransmitter.php;h=e3df1a7a32fc0af404803e0ec40245faf99acce4;hb=4729fca5d944dee1ce6bbd7a0f00e9d45973838c;hp=6a1dacf0a36c9cacb2145010cdc8fcc3c16ea01a;hpb=0e6fe8d8ad821d0eea3595a4bbfe094edc8e5d31;p=friendica.git diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 6a1dacf0a3..e3df1a7a32 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -146,16 +146,16 @@ class Transmitter /** * Collects a list of contacts of the given owner * - * @param array $owner Owner array - * @param int|array $rel The relevant value(s) contact.rel should match - * @param string $module The name of the relevant AP endpoint module (followers|following) - * @param integer $page Page number - * @param string $requester URL of the requester + * @param array $owner Owner array + * @param array $rel The relevant value(s) contact.rel should match + * @param string $module The name of the relevant AP endpoint module (followers|following) + * @param integer $page Page number + * @param string $requester URL of the requester * * @return array of owners * @throws \Exception */ - public static function getContacts($owner, $rel, $module, $page = null, string $requester = null) + public static function getContacts(array $owner, array $rel, string $module, int $page = null, string $requester = null) { $parameters = [ 'rel' => $rel, @@ -179,12 +179,16 @@ class Transmitter $data['type'] = 'OrderedCollection'; $data['totalItems'] = $total; + if (!empty($page)) { + $data['id'] .= '?' . http_build_query(['page' => $page]); + } + // When we hide our friends we will only show the pure number but don't allow more. $show_contacts = empty($owner['hide-friends']); // Allow fetching the contact list when the requester is part of the list. if (($owner['page-flags'] == User::PAGE_FLAGS_PRVGROUP) && !empty($requester)) { - $show_contacts = DBA::exists('contact', ['nurl' => Strings::normaliseLink($requester), 'rel' => $rel]); + $show_contacts = DBA::exists('contact', ['nurl' => Strings::normaliseLink($requester), 'uid' => $owner['uid'], 'blocked' => false]); } if (!$show_contacts) { @@ -203,7 +207,7 @@ class Transmitter } DBA::close($contacts); - if (!empty($list)) { + if (count($list) == 100) { $data['next'] = DI::baseUrl() . $modulePath . $owner['nickname'] . '?page=' . ($page + 1); } @@ -226,7 +230,7 @@ class Transmitter * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function getOutbox($owner, $page = null, $requester = '') + public static function getOutbox(array $owner, int $page = null, string $requester = '') { $condition = ['private' => [Item::PUBLIC, Item::UNLISTED]]; @@ -258,6 +262,10 @@ class Transmitter $data['type'] = 'OrderedCollection'; $data['totalItems'] = $count; + if (!empty($page)) { + $data['id'] .= '?' . http_build_query(['page' => $page]); + } + if (empty($page)) { $data['first'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=1'; } else { @@ -276,7 +284,7 @@ class Transmitter } DBA::close($items); - if (!empty($list)) { + if (count($list) == 20) { $data['next'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1); } @@ -288,6 +296,73 @@ class Transmitter return $data; } + /** + * Public posts for the given owner + * + * @param array $owner Owner array + * @param integer $page Page number + * + * @return array of posts + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function getFeatured(array $owner, int $page = null) + { + $condition = ["`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)", + Contact::getIdForURL($owner['url'], 0, false), Post\Collection::FEATURED]; + + $condition = DBA::mergeConditions($condition, + ['uid' => $owner['uid'], + 'author-id' => Contact::getIdForURL($owner['url'], 0, false), + 'private' => [Item::PUBLIC, Item::UNLISTED], + 'gravity' => [GRAVITY_PARENT, GRAVITY_COMMENT], + 'network' => Protocol::FEDERATED, + 'parent-network' => Protocol::FEDERATED, + 'origin' => true, + 'deleted' => false, + 'visible' => true]); + + $count = Post::count($condition); + + $data = ['@context' => ActivityPub::CONTEXT]; + $data['id'] = DI::baseUrl() . '/featured/' . $owner['nickname']; + $data['type'] = 'OrderedCollection'; + $data['totalItems'] = $count; + + if (!empty($page)) { + $data['id'] .= '?' . http_build_query(['page' => $page]); + } + + if (empty($page)) { + $data['first'] = DI::baseUrl() . '/featured/' . $owner['nickname'] . '?page=1'; + } else { + $data['type'] = 'OrderedCollectionPage'; + $list = []; + + $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]); + while ($item = Post::fetch($items)) { + $activity = self::createActivityFromItem($item['id'], true); + $activity['type'] = $activity['type'] == 'Update' ? 'Create' : $activity['type']; + + // Only list "Create" activity objects here, no reshares + if (!empty($activity['object']) && ($activity['type'] == 'Create')) { + $list[] = $activity['object']; + } + } + DBA::close($items); + + if (count($list) == 20) { + $data['next'] = DI::baseUrl() . '/featured/' . $owner['nickname'] . '?page=' . ($page + 1); + } + + $data['partOf'] = DI::baseUrl() . '/featured/' . $owner['nickname']; + + $data['orderedItems'] = $list; + } + + return $data; + } + /** * Return the service array containing information the used software and it's url * @@ -328,8 +403,9 @@ class Transmitter if ($uid != 0) { $data['following'] = DI::baseUrl() . '/following/' . $owner['nick']; $data['followers'] = DI::baseUrl() . '/followers/' . $owner['nick']; - $data['inbox'] = DI::baseUrl() . '/inbox/' . $owner['nick']; - $data['outbox'] = DI::baseUrl() . '/outbox/' . $owner['nick']; + $data['inbox'] = DI::baseUrl() . '/inbox/' . $owner['nick']; + $data['outbox'] = DI::baseUrl() . '/outbox/' . $owner['nick']; + $data['featured'] = DI::baseUrl() . '/featured/' . $owner['nick']; } else { $data['inbox'] = DI::baseUrl() . '/friendica/inbox'; } @@ -424,42 +500,34 @@ class Transmitter } /** - * Returns an array with permissions of a given item array + * Returns an array with permissions of the thread parent of the given item array * * @param array $item + * @param bool $is_forum_thread * * @return array with permissions * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function fetchPermissionBlockFromConversation($item) + private static function fetchPermissionBlockFromThreadParent(array $item, bool $is_forum_thread) { - if (empty($item['thr-parent'])) { + if (empty($item['thr-parent-id'])) { return []; } - $condition = ['item-uri' => $item['thr-parent'], 'protocol' => Conversation::PARCEL_ACTIVITYPUB]; - $conversation = DBA::selectFirst('conversation', ['source'], $condition); - if (!DBA::isResult($conversation)) { + $parent = Post::selectFirstPost(['author-link'], ['uri-id' => $item['thr-parent-id']]); + if (empty($parent)) { return []; } $permissions = [ - 'to' => [], + 'to' => [$parent['author-link']], 'cc' => [], 'bto' => [], 'bcc' => [], ]; - $activity = json_decode($conversation['source'], true); - - $actor = JsonLD::fetchElement($activity, 'actor', 'id'); - if (!empty($actor)) { - $permissions['to'][] = $actor; - $profile = APContact::getByURL($actor); - } else { - $profile = []; - } + $parent_profile = APContact::getByURL($parent['author-link']); $item_profile = APContact::getByURL($item['author-link']); $exclude[] = $item['author-link']; @@ -468,26 +536,17 @@ class Transmitter $exclude[] = $item['owner-link']; } - foreach (['to', 'cc', 'bto', 'bcc'] as $element) { - if (empty($activity[$element])) { - continue; - } - if (is_string($activity[$element])) { - $activity[$element] = [$activity[$element]]; - } - - foreach ($activity[$element] as $receiver) { - if (empty($receiver)) { - continue; - } - - if (!empty($profile['followers']) && $receiver == $profile['followers'] && !empty($item_profile['followers'])) { - $permissions[$element][] = $item_profile['followers']; - } elseif (!in_array($receiver, $exclude)) { - $permissions[$element][] = $receiver; + $type = [Tag::TO => 'to', Tag::CC => 'cc', Tag::BTO => 'bto', Tag::BCC => 'bcc']; + foreach (Tag::getByURIId($item['thr-parent-id'], [Tag::TO, Tag::CC, Tag::BTO, Tag::BCC]) as $receiver) { + if (!empty($parent_profile['followers']) && $receiver['url'] == $parent_profile['followers'] && !empty($item_profile['followers'])) { + if (!$is_forum_thread) { + $permissions[$type[$receiver['type']]][] = $item_profile['followers']; } + } elseif (!in_array($receiver['url'], $exclude)) { + $permissions[$type[$receiver['type']]][] = $receiver['url']; } } + return $permissions; } @@ -509,28 +568,33 @@ class Transmitter /** * Creates an array of permissions from an item thread * - * @param array $item Item array - * @param boolean $blindcopy addressing via "bcc" or "cc"? - * @param integer $last_id Last item id for adding receivers - * @param boolean $forum_mode "true" means that we are sending content to a forum + * @param array $item Item array + * @param boolean $blindcopy addressing via "bcc" or "cc"? + * @param integer $last_id Last item id for adding receivers * * @return array with permission data * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0, $forum_mode = false) + private static function createPermissionBlockForItem($item, $blindcopy, $last_id = 0) { if ($last_id == 0) { $last_id = $item['id']; } $always_bcc = false; + $is_forum = false; + $follower = ''; // Check if we should always deliver our stuff via BCC if (!empty($item['uid'])) { - $profile = User::getOwnerDataById($item['uid']); - if (!empty($profile)) { - $always_bcc = $profile['hide-friends']; + $owner = User::getOwnerDataById($item['uid']); + if (!empty($owner)) { + $always_bcc = $owner['hide-friends']; + $is_forum = ($owner['account-type'] == User::ACCOUNT_TYPE_COMMUNITY) && $owner['manually-approve']; + + $profile = APContact::getByURL($owner['url'], false); + $follower = $profile['followers'] ?? ''; } } @@ -538,6 +602,14 @@ class Transmitter $always_bcc = true; } + $parent = Post::selectFirst(['causer-link', 'post-reason'], ['id' => $item['parent']]); + if (($parent['post-reason'] == Item::PR_ANNOUNCEMENT) && !empty($parent['causer-link'])) { + $profile = APContact::getByURL($parent['causer-link'], false); + $is_forum_thread = isset($profile['type']) && $profile['type'] == 'Group'; + } else { + $is_forum_thread = false; + } + if (self::isAnnounce($item) || DI::config()->get('debug', 'total_ap_delivery') || self::isAPPost($last_id)) { // Will be activated in a later step $networks = Protocol::FEDERATED; @@ -568,7 +640,7 @@ class Transmitter $data['cc'][] = $announce['actor']['url']; } - $data = array_merge($data, self::fetchPermissionBlockFromConversation($item)); + $data = array_merge($data, self::fetchPermissionBlockFromThreadParent($item, $is_forum_thread)); // Check if the item is completely public or unlisted if ($item['private'] == Item::PUBLIC) { @@ -600,30 +672,41 @@ class Transmitter continue; } - if (!empty($profile = APContact::getByURL($contact['url'], false))) { + $profile = APContact::getByURL($term['url'], false); + if (!empty($profile)) { + if ($term['type'] == Tag::EXCLUSIVE_MENTION) { + $exclusive = true; + if (!empty($profile['followers']) && ($profile['type'] == 'Group')) { + $data['cc'][] = $profile['followers']; + } + } $data['to'][] = $profile['url']; } } } - foreach ($receiver_list as $receiver) { - $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]); - if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) { - continue; - } + if ($is_forum && !$exclusive && !empty($follower)) { + $data['cc'][] = $follower; + } elseif (!$exclusive) { + foreach ($receiver_list as $receiver) { + $contact = DBA::selectFirst('contact', ['url', 'hidden', 'network', 'protocol', 'gsid'], ['id' => $receiver, 'network' => Protocol::FEDERATED]); + if (!DBA::isResult($contact) || !self::isAPContact($contact, $networks)) { + continue; + } - if (!empty($profile = APContact::getByURL($contact['url'], false))) { - if ($contact['hidden'] || $always_bcc) { - $data['bcc'][] = $profile['url']; - } else { - $data['cc'][] = $profile['url']; + if (!empty($profile = APContact::getByURL($contact['url'], false))) { + if ($contact['hidden'] || $always_bcc) { + $data['bcc'][] = $profile['url']; + } else { + $data['cc'][] = $profile['url']; + } } } } } if (!empty($item['parent'])) { - $parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']]); + $parents = Post::select(['id', 'author-link', 'owner-link', 'gravity', 'uri'], ['parent' => $item['parent']], ['order' => ['id']]); while ($parent = Post::fetch($parents)) { if ($parent['gravity'] == GRAVITY_PARENT) { $profile = APContact::getByURL($parent['owner-link'], false); @@ -637,15 +720,13 @@ class Transmitter $data['to'][] = $profile['url']; } else { $data['cc'][] = $profile['url']; - if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])) { + if (($item['private'] != Item::PRIVATE) && !empty($actor_profile['followers'])&& !$is_forum_thread) { $data['cc'][] = $actor_profile['followers']; } } - } elseif (!$exclusive) { + } elseif (!$exclusive && !$is_forum_thread) { // Public thread parent post always are directed to the followers. - // This mustn't be done by posts that are directed to forum servers via the exclusive mention. - // But possibly in that case we could add the "followers" collection of the forum to the message. - if (($item['private'] != Item::PRIVATE) && !$forum_mode) { + if ($item['private'] != Item::PRIVATE) { $data['cc'][] = $actor_profile['followers']; } } @@ -707,6 +788,19 @@ class Transmitter unset($receivers['bcc']); } + foreach (['to' => Tag::TO, 'cc' => Tag::CC, 'bcc' => Tag::BCC] as $element => $type) { + if (!empty($receivers[$element])) { + foreach ($receivers[$element] as $receiver) { + if ($receiver == ActivityPub::PUBLIC_COLLECTION) { + $name = Receiver::PUBLIC_COLLECTION; + } else { + $name = trim(parse_url($receiver, PHP_URL_PATH), '/'); + } + Tag::store($item['uri-id'], $type, $name, $receiver); + } + } + } + return $receivers; } @@ -811,18 +905,17 @@ class Transmitter /** * Fetches an array of inboxes for the given item and user * - * @param array $item Item array - * @param integer $uid User ID - * @param boolean $personal fetch personal inboxes - * @param integer $last_id Last item id for adding receivers - * @param boolean $forum_mode "true" means that we are sending content to a forum + * @param array $item Item array + * @param integer $uid User ID + * @param boolean $personal fetch personal inboxes + * @param integer $last_id Last item id for adding receivers * @return array with inboxes * @throws \Friendica\Network\HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0, $forum_mode = false) + public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0) { - $permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_mode); + $permissions = self::createPermissionBlockForItem($item, true, $last_id); if (empty($permissions)) { return []; } @@ -905,6 +998,7 @@ class Transmitter $mail['title'] = ''; } + $mail['content-warning'] = ''; $mail['author-link'] = $mail['owner-link'] = $mail['from-url']; $mail['owner-id'] = $mail['author-id']; $mail['allow_cid'] = '<'.$mail['contact-id'].'>'; @@ -1079,20 +1173,6 @@ class Transmitter return false; } - // In case of a forum post ensure to return the original post if author and forum are on the same machine - if (($item['gravity'] == GRAVITY_PARENT) && !empty($item['forum_mode'])) { - $author = Contact::getById($item['author-id'], ['nurl']); - if (!empty($author['nurl'])) { - $self = Contact::selectFirst(['uid'], ['nurl' => $author['nurl'], 'self' => true]); - if (!empty($self['uid'])) { - $forum_item = Post::selectFirst(Item::DELIVER_FIELDLIST, ['uri-id' => $item['uri-id'], 'uid' => $self['uid']]); - if (DBA::isResult($forum_item)) { - $item = $forum_item; - } - } - } - } - if (empty($item['uri-id'])) { Logger::warning('Item without uri-id', ['item' => $item]); return false; @@ -1161,6 +1241,7 @@ class Transmitter if (in_array($data['type'], ['Create', 'Update', 'Delete'])) { $data['object'] = $object ?? self::createNote($item); + $data['published'] = DateTimeFormat::utcNow(DateTimeFormat::ATOM); } elseif ($data['type'] == 'Add') { $data = self::createAddTag($item, $data); } elseif ($data['type'] == 'Announce') { @@ -1415,7 +1496,7 @@ class Transmitter */ private static function isSensitive($uri_id) { - return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw']); + return DBA::exists('tag-view', ['uri-id' => $uri_id, 'name' => 'nsfw', 'type' => Tag::HASHTAG]); } /** @@ -1463,10 +1544,28 @@ class Transmitter return []; } + // We are treating posts differently when they are directed to a community. + // This is done to better support Lemmy. Most of the changes should work with other systems as well. + // But to not risk compatibility issues we currently perform the changes only for communities. + if ($item['gravity'] == GRAVITY_PARENT) { + $isCommunityPost = !empty(Tag::getByURIId($item['uri-id'], [Tag::EXCLUSIVE_MENTION])); + $links = Post\Media::getByURIId($item['uri-id'], [Post\Media::HTML]); + if ($isCommunityPost && (count($links) == 1)) { + $link = $links[0]['url']; + } + } else { + $isCommunityPost = false; + } + if ($item['event-type'] == 'event') { $type = 'Event'; } elseif (!empty($item['title'])) { - $type = 'Article'; + if (!$isCommunityPost || empty($link)) { + $type = 'Article'; + } else { + // "Page" is used by Lemmy for posts that contain an external link + $type = 'Page'; + } } else { $type = 'Note'; } @@ -1498,7 +1597,7 @@ class Transmitter $data['updated'] = DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM); } - $data['url'] = $item['plink']; + $data['url'] = $link ?? $item['plink']; $data['attributedTo'] = $item['author-link']; $data['sensitive'] = self::isSensitive($item['uri-id']); $data['context'] = self::fetchContextURLForItem($item); @@ -1535,6 +1634,19 @@ class Transmitter if ($type == 'Event') { $data = array_merge($data, self::createEvent($item)); } else { + if ($isCommunityPost) { + // For community posts we remove the visible "!user@domain.tld". + // This improves the look at systems like Lemmy. + // Also in the future we should control the community delivery via other methods. + $body = preg_replace("/!\[url\=[^\[\]]*\][^\[\]]*\[\/url\]/ism", '', $body); + } + + if ($type == 'Page') { + // When we transmit "Page" posts we have to remove the attachment. + // The attachment contains the link that we already transmit in the "url" field. + $body = preg_replace("/\s*\[attachment .*?\].*?\[\/attachment\]\s*/ism", '', $body); + } + $body = BBCode::setMentionsToNicknames($body); $data['content'] = BBCode::convertForUriId($item['uri-id'], $body, BBCode::ACTIVITYPUB);