X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FProtocol%2FActivityPub%2FTransmitter.php;h=023bd5c83cce179c8c6cd16d2f9408b099f41f72;hb=74b102b94806bbf5b04b7f2a87acbfeeb68d7525;hp=7be55898c8488ff6de86bd122831cc889cb69674;hpb=d404f15312320dbe1f516db7bb91944182ad5d26;p=friendica.git diff --git a/src/Protocol/ActivityPub/Transmitter.php b/src/Protocol/ActivityPub/Transmitter.php index 7be55898c8..023bd5c83c 100644 --- a/src/Protocol/ActivityPub/Transmitter.php +++ b/src/Protocol/ActivityPub/Transmitter.php @@ -44,7 +44,6 @@ use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Relay; use Friendica\Util\DateTimeFormat; use Friendica\Util\HTTPSignature; -use Friendica\Util\JsonLD; use Friendica\Util\LDSignature; use Friendica\Util\Map; use Friendica\Util\Network; @@ -59,6 +58,10 @@ use Friendica\Util\XML; */ class Transmitter { + const CACHEKEY_FEATURED = 'transmitter:getFeatured:'; + const CACHEKEY_CONTACTS = 'transmitter:getContacts:'; + const CACHEKEY_OUTBOX = 'transmitter:getOutbox:'; + /** * Add relay servers to the list of inboxes * @@ -146,17 +149,26 @@ 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 + * @param boolean $nocache Wether to bypass caching * * @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, $nocache = false) { + if (empty($page)) { + $cachekey = self::CACHEKEY_CONTACTS . $module . ':'. $owner['uid']; + $result = DI::cache()->get($cachekey); + if (!$nocache && !is_null($result)) { + return $result; + } + } + $parameters = [ 'rel' => $rel, 'uid' => $owner['uid'], @@ -179,6 +191,10 @@ 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']); @@ -188,6 +204,10 @@ class Transmitter } if (!$show_contacts) { + if (!empty($cachekey)) { + DI::cache()->set($cachekey, $data, Duration::DAY); + } + return $data; } @@ -203,7 +223,7 @@ class Transmitter } DBA::close($contacts); - if (!empty($list)) { + if (count($list) == 100) { $data['next'] = DI::baseUrl() . $modulePath . $owner['nickname'] . '?page=' . ($page + 1); } @@ -212,6 +232,10 @@ class Transmitter $data['orderedItems'] = $list; } + if (!empty($cachekey)) { + DI::cache()->set($cachekey, $data, Duration::DAY); + } + return $data; } @@ -221,13 +245,22 @@ class Transmitter * @param array $owner Owner array * @param integer $page Page number * @param string $requester URL of requesting account + * @param boolean $nocache Wether to bypass caching * * @return array of posts * @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 = '', $nocache = false) { + if (empty($page)) { + $cachekey = self::CACHEKEY_OUTBOX . $owner['uid']; + $result = DI::cache()->get($cachekey); + if (!$nocache && !is_null($result)) { + return $result; + } + } + $condition = ['private' => [Item::PUBLIC, Item::UNLISTED]]; if (!empty($requester)) { @@ -258,6 +291,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 +313,7 @@ class Transmitter } DBA::close($items); - if (!empty($list)) { + if (count($list) == 20) { $data['next'] = DI::baseUrl() . '/outbox/' . $owner['nickname'] . '?page=' . ($page + 1); } @@ -285,6 +322,94 @@ class Transmitter $data['orderedItems'] = $list; } + if (!empty($cachekey)) { + DI::cache()->set($cachekey, $data, Duration::DAY); + } + + return $data; + } + + /** + * Public posts for the given owner + * + * @param array $owner Owner array + * @param integer $page Page number + * @param boolean $nocache Wether to bypass caching + * + * @return array of posts + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function getFeatured(array $owner, int $page = null, $nocache = false) + { + if (empty($page)) { + $cachekey = self::CACHEKEY_FEATURED . $owner['uid']; + $result = DI::cache()->get($cachekey); + if (!$nocache && !is_null($result)) { + return $result; + } + } + + $owner_cid = Contact::getIdForURL($owner['url'], 0, false); + + $condition = ["`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)", + $owner_cid, Post\Collection::FEATURED]; + + $condition = DBA::mergeConditions($condition, + ['uid' => $owner['uid'], + 'author-id' => $owner_cid, + '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)) { + $items = Post::select(['id'], $condition, ['limit' => 20, 'order' => ['created' => true]]); + } else { + $data['type'] = 'OrderedCollectionPage'; + $items = Post::select(['id'], $condition, ['limit' => [($page - 1) * 20, 20], 'order' => ['created' => true]]); + } + $list = []; + + 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); + } + + if (!empty($page)) { + $data['partOf'] = DI::baseUrl() . '/featured/' . $owner['nickname']; + } + + $data['orderedItems'] = $list; + + if (!empty($cachekey)) { + DI::cache()->set($cachekey, $data, Duration::DAY); + } + return $data; } @@ -328,8 +453,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 +550,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 +586,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 +618,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_post "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_post = 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 +652,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 +690,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) { @@ -613,7 +735,9 @@ class Transmitter } } - if (!$exclusive) { + 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)) { @@ -632,7 +756,7 @@ class Transmitter } 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); @@ -646,15 +770,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_post) { + if ($item['private'] != Item::PRIVATE) { $data['cc'][] = $actor_profile['followers']; } } @@ -716,6 +838,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; } @@ -820,18 +955,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_post "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_post = false) + public static function fetchTargetInboxes($item, $uid, $personal = false, $last_id = 0) { - $permissions = self::createPermissionBlockForItem($item, true, $last_id, $forum_post); + $permissions = self::createPermissionBlockForItem($item, true, $last_id); if (empty($permissions)) { return []; } @@ -914,6 +1048,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'].'>'; @@ -1156,6 +1291,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') { @@ -1410,7 +1546,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]); } /** @@ -1458,10 +1594,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'; } @@ -1493,7 +1647,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); @@ -1530,6 +1684,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);