From c3830ef3e969cf18253072e81bfefbc3b6c5615b Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 7 Sep 2023 12:34:46 +0000 Subject: [PATCH] Fixes the score calculation concerning the relation-cid / cid interaction --- src/Module/Conversation/Channel.php | 319 +++++++++++++++++++++++++++- src/Worker/Cron.php | 2 +- 2 files changed, 312 insertions(+), 9 deletions(-) diff --git a/src/Module/Conversation/Channel.php b/src/Module/Conversation/Channel.php index 0baa40f7f1..27f5f9cad4 100644 --- a/src/Module/Conversation/Channel.php +++ b/src/Module/Conversation/Channel.php @@ -48,13 +48,39 @@ use Friendica\Network\HTTPException; use Friendica\Database\Database; use Friendica\Module\Response; use Friendica\Navigation\SystemMessages; +use Friendica\Util\DateTimeFormat; use Friendica\Util\Profiler; use Psr\Log\LoggerInterface; class Channel extends Timeline { - /** @var TimelineFactory */ - protected $timeline; + const WHATSHOT = 'whatshot'; + const FORYOU = 'foryou'; + const FOLLOWERS = 'followers'; + const SHARERSOFSHARERS = 'sharersofsharers'; + const IMAGE = 'image'; + const VIDEO = 'video'; + const AUDIO = 'audio'; + const LANGUAGE = 'language'; + + protected static $content; + protected static $accountTypeString; + protected static $accountType; + protected static $itemsPerPage; + protected static $min_id; + protected static $max_id; + protected static $item_id; + + /** @var UserSession */ + protected $session; + /** @var ICanCache */ + protected $cache; + /** @var IManageConfigValues The config */ + protected $config; + /** @var SystemMessages */ + protected $systemMessages; + /** @var App\Page */ + protected $page; /** @var Conversation */ protected $conversation; /** @var App\Page */ @@ -103,10 +129,83 @@ class Channel extends Timeline $o .= Renderer::replaceMacros($tpl, ['$reload_uri' => $this->args->getQueryString()]); } - if (!$this->raw) { - $tabs = $this->getTabArray($this->channel->getTimelines($this->session->getLocalUserId()), 'channel'); - $tabs = array_merge($tabs, $this->getTabArray($this->channelRepository->selectByUid($this->session->getLocalUserId()), 'channel')); - $tabs = array_merge($tabs, $this->getTabArray($this->community->getTimelines(true), 'channel')); + if (empty($request['mode']) || ($request['mode'] != 'raw')) { + $tabs = []; + + $tabs[] = [ + 'label' => $this->l10n->t('For you'), + 'url' => 'channel/' . self::FORYOU, + 'sel' => self::$content == self::FORYOU ? 'active' : '', + 'title' => $this->l10n->t('Posts from contacts you interact with and who interact with you'), + 'id' => 'channel-foryou-tab', + 'accesskey' => 'y' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('What\'s Hot'), + 'url' => 'channel/' . self::WHATSHOT, + 'sel' => self::$content == self::WHATSHOT ? 'active' : '', + 'title' => $this->l10n->t('Posts with a lot of interactions'), + 'id' => 'channel-whatshot-tab', + 'accesskey' => 'h' + ]; + + $language = User::getLanguageCode($this->session->getLocalUserId()); + $languages = $this->l10n->getAvailableLanguages(true); + + $tabs[] = [ + 'label' => $languages[$language], + 'url' => 'channel/' . self::LANGUAGE, + 'sel' => self::$content == self::LANGUAGE ? 'active' : '', + 'title' => $this->l10n->t('Posts in %s', $languages[$language]), + 'id' => 'channel-language-tab', + 'accesskey' => 'g' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('Followers'), + 'url' => 'channel/' . self::FOLLOWERS, + 'sel' => self::$content == self::FOLLOWERS ? 'active' : '', + 'title' => $this->l10n->t('Posts from your followers that you don\'t follow'), + 'id' => 'channel-followers-tab', + 'accesskey' => 'f' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('Sharers of sharers'), + 'url' => 'channel/' . self::SHARERSOFSHARERS, + 'sel' => self::$content == self::SHARERSOFSHARERS ? 'active' : '', + 'title' => $this->l10n->t('Posts from accounts that are followed by accounts that you follow'), + 'id' => 'channel-' . self::SHARERSOFSHARERS . '-tab', + 'accesskey' => 'f' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('Images'), + 'url' => 'channel/' . self::IMAGE, + 'sel' => self::$content == self::IMAGE ? 'active' : '', + 'title' => $this->l10n->t('Posts with images'), + 'id' => 'channel-image-tab', + 'accesskey' => 'i' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('Audio'), + 'url' => 'channel/' . self::AUDIO, + 'sel' => self::$content == self::AUDIO ? 'active' : '', + 'title' => $this->l10n->t('Posts with audio'), + 'id' => 'channel-audio-tab', + 'accesskey' => 'd' + ]; + + $tabs[] = [ + 'label' => $this->l10n->t('Videos'), + 'url' => 'channel/' . self::VIDEO, + 'sel' => self::$content == self::VIDEO ? 'active' : '', + 'title' => $this->l10n->t('Posts with videos'), + 'id' => 'channel-video-tab', + 'accesskey' => 'v' + ]; $tab_tpl = Renderer::getMarkupTemplate('common_tabs.tpl'); $o .= Renderer::replaceMacros($tab_tpl, ['$tabs' => $tabs]); @@ -173,11 +272,215 @@ class Channel extends Timeline $this->selectedTab = ChannelEntity::FORYOU; } +<<<<<<< HEAD if (!$this->channel->isTimeline($this->selectedTab) && !$this->userDefinedChannel->isTimeline($this->selectedTab, $this->session->getLocalUserId()) && !$this->community->isTimeline($this->selectedTab)) { +======= + if (!in_array(self::$content, [self::WHATSHOT, self::FORYOU, self::FOLLOWERS, self::SHARERSOFSHARERS, self::IMAGE, self::VIDEO, self::AUDIO, self::LANGUAGE])) { +>>>>>>> 0818b4d1b3 (Fixes the score calculation concerning the relation-cid / cid interaction) throw new HTTPException\BadRequestException($this->l10n->t('Channel not available.')); } - $this->maxId = $request['last_created'] ?? $this->maxId; - $this->minId = $request['first_created'] ?? $this->minId; + if ($this->mode->isMobile()) { + self::$itemsPerPage = $this->pConfig->get( + $this->session->getLocalUserId(), + 'system', + 'itemspage_mobile_network', + $this->config->get('system', 'itemspage_network_mobile') + ); + } else { + self::$itemsPerPage = $this->pConfig->get( + $this->session->getLocalUserId(), + 'system', + 'itemspage_network', + $this->config->get('system', 'itemspage_network') + ); + } + + if (!empty($request['item'])) { + $item = Post::selectFirst(['parent-uri-id'], ['id' => $request['item']]); + self::$item_id = $item['parent-uri-id'] ?? 0; + } else { + self::$item_id = 0; + } + + self::$min_id = $request['min_id'] ?? null; + self::$max_id = $request['last_created'] ?? $request['max_id'] ?? null; + } + + /** + * Computes the displayed items. + * + * Community pages have a restriction on how many successive posts by the same author can show on any given page, + * so we may have to retrieve more content beyond the first query + * + * @return array + * @throws \Exception + */ + protected function getItems(array $request) + { + if (self::$content == ChannelEntity::WHATSHOT) { + if (!is_null(self::$accountType)) { + $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` = ?", $this->getMedianComments(4), $this->getMedianActivities(4), self::$accountType]; + } else { + $condition = ["(`comments` >= ? OR `activities` >= ?) AND `contact-type` != ?", $this->getMedianComments(4), $this->getMedianActivities(4), Contact::TYPE_COMMUNITY]; + } + } elseif (self::$content == ChannelEntity::FORYOU) { + $cid = Contact::getPublicIdByUserId($this->session->getLocalUserId()); + + $condition = [ + "(`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `relation-thread-score` > ?) OR + ((`comments` >= ? OR `activities` >= ?) AND `owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?)) OR + (`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` IN (?, ?) AND `notify_new_posts`)))", + $cid, $this->getMedianRelationThreadScore($cid, 4), $this->getMedianComments(4), $this->getMedianActivities(4), $cid, + $this->session->getLocalUserId(), Contact::FRIEND, Contact::SHARING + ]; + } elseif (self::$content == self::FOLLOWERS) { + $condition = ["`owner-id` IN (SELECT `pid` FROM `account-user-view` WHERE `uid` = ? AND `rel` = ?)", $this->session->getLocalUserId(), Contact::FOLLOWER]; + } elseif (self::$content == self::SHARERSOFSHARERS) { + $cid = Contact::getPublicIdByUserId($this->session->getLocalUserId()); + + $condition = [ + "`owner-id` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `last-interaction` > ? + AND `relation-cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ? AND `relation-thread-score` >= ?) + AND NOT `cid` IN (SELECT `cid` FROM `contact-relation` WHERE `follows` AND `relation-cid` = ?))", + DateTimeFormat::utc('now - 90 day'), $cid, $this->getMedianRelationThreadScore($cid, 4), $cid + ]; + } elseif (self::$content == self::IMAGE) { + $condition = ["`media-type` & ?", 1]; + } elseif (self::$content == ChannelEntity::VIDEO) { + $condition = ["`media-type` & ?", 2]; + } elseif (self::$content == ChannelEntity::AUDIO) { + $condition = ["`media-type` & ?", 4]; + } elseif (self::$content == ChannelEntity::LANGUAGE) { + $condition = ["JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?", $this->l10n->convertCodeForLanguageDetection(User::getLanguageCode($this->session->getLocalUserId()))]; + } + + if (self::$content != ChannelEntity::LANGUAGE) { + $condition = $this->addLanguageCondition($condition); + } + + $condition[0] .= " AND NOT EXISTS(SELECT `cid` FROM `user-contact` WHERE `uid` = ? AND `cid` = `post-engagement`.`owner-id` AND (`ignored` OR `blocked` OR `collapsed`))"; + $condition[] = $this->session->getLocalUserId(); + + if ((self::$content != ChannelEntity::WHATSHOT) && !is_null(self::$accountType)) { + $condition[0] .= " AND `contact-type` = ?"; + $condition[] = self::$accountType; + } + + $params = ['order' => ['created' => true], 'limit' => self::$itemsPerPage]; + + if (!empty(self::$item_id)) { + $condition[0] .= " AND `uri-id` = ?"; + $condition[] = self::$item_id; + } else { + if (!empty($request['no_sharer'])) { + $condition[0] .= " AND NOT `uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `post-user`.`uid` = ? AND `post-user`.`uri-id` = `post-engagement`.`uri-id`)"; + $condition[] = $this->session->getLocalUserId(); + } + + if (isset(self::$max_id)) { + $condition[0] .= " AND `created` < ?"; + $condition[] = self::$max_id; + } + + if (isset(self::$min_id)) { + $condition[0] .= " AND `created` > ?"; + $condition[] = self::$min_id; + + // Previous page case: we want the items closest to min_id but for that we need to reverse the query order + if (!isset(self::$max_id)) { + $params['order']['created'] = false; + } + } + } + + $items = $this->database->selectToArray('post-engagement', ['uri-id', 'created'], $condition, $params); + if (empty($items)) { + return []; + } + + // Previous page case: once we get the relevant items closest to min_id, we need to restore the expected display order + if (empty(self::$item_id) && isset(self::$min_id) && !isset(self::$max_id)) { + $items = array_reverse($items); + } + + Item::update(['unseen' => false], ['unseen' => true, 'uid' => $this->session->getLocalUserId(), 'uri-id' => array_column($items, 'uri-id')]); + + return $items; + } + + private function addLanguageCondition(array $condition): array + { + $conditions = []; + $languages = $this->pConfig->get($this->session->getLocalUserId(), 'channel', 'languages', [User::getLanguageCode($this->session->getLocalUserId())]); + $languages = $this->l10n->convertForLanguageDetection($languages); + foreach ($languages as $language) { + $conditions[] = "JSON_EXTRACT(JSON_KEYS(language), '$[0]') = ?"; + $condition[] = $language; + } + if (!empty($conditions)) { + $condition[0] .= " AND (`language` IS NULL OR " . implode(' OR ', $conditions) . ")"; + } + return $condition; + } + + private function getMedianComments(int $divider): int + { + $cache_key = 'Channel:getMedianComments:' . $divider; + $comments = $this->cache->get($cache_key); + if (!empty($comments)) { + return $comments; + } + + $limit = $this->database->count('post-engagement', ["`contact-type` != ? AND `comments` > ?", Contact::TYPE_COMMUNITY, 0]) / $divider; + $post = $this->database->selectToArray('post-engagement', ['comments'], ["`contact-type` != ?", Contact::TYPE_COMMUNITY], ['order' => ['comments' => true], 'limit' => [$limit, 1]]); + $comments = $post[0]['comments'] ?? 0; + if (empty($comments)) { + return 0; + } + + $this->cache->set($cache_key, $comments, Duration::HALF_HOUR); + $this->logger->debug('Calculated median comments', ['divider' => $divider, 'median' => $comments]); + return $comments; + } + + private function getMedianActivities(int $divider): int + { + $cache_key = 'Channel:getMedianActivities:' . $divider; + $activities = $this->cache->get($cache_key); + if (!empty($activities)) { + return $activities; + } + + $limit = $this->database->count('post-engagement', ["`contact-type` != ? AND `activities` > ?", Contact::TYPE_COMMUNITY, 0]) / $divider; + $post = $this->database->selectToArray('post-engagement', ['activities'], ["`contact-type` != ?", Contact::TYPE_COMMUNITY], ['order' => ['activities' => true], 'limit' => [$limit, 1]]); + $activities = $post[0]['activities'] ?? 0; + if (empty($activities)) { + return 0; + } + + $this->cache->set($cache_key, $activities, Duration::HALF_HOUR); + $this->logger->debug('Calculated median activities', ['divider' => $divider, 'median' => $activities]); + return $activities; + } + + private function getMedianRelationThreadScore(int $cid, int $divider): int + { + $cache_key = 'Channel:getThreadScore:' . $cid . ':' . $divider; + $score = $this->cache->get($cache_key); + if (!empty($score)) { + return $score; + } + + $limit = $this->database->count('contact-relation', ["`relation-cid` = ? AND `relation-thread-score` > ?", $cid, 0]) / $divider; + $relation = $this->database->selectToArray('contact-relation', ['relation-thread-score'], ['relation-cid' => $cid], ['order' => ['relation-thread-score' => true], 'limit' => [$limit, 1]]); + $score = $relation[0]['relation-thread-score'] ?? 0; + if (empty($score)) { + return 0; + } + + $this->cache->set($cache_key, $score, Duration::HALF_HOUR); + $this->logger->debug('Calculated median score', ['cid' => $cid, 'divider' => $divider, 'median' => $score]); + return $score; } } diff --git a/src/Worker/Cron.php b/src/Worker/Cron.php index 06e59ec97c..40cc5e7a44 100644 --- a/src/Worker/Cron.php +++ b/src/Worker/Cron.php @@ -147,7 +147,7 @@ class Cron DBA::close($users); // Update contact relations for our users - $users = DBA::select('user', ['uid'], ["`verified` AND NOT `blocked` AND NOT `account_removed` AND NOT `account_expired` AND `uid` > ?", 0]); + $users = DBA::select('user', ['uid'], ["NOT `account_expired` AND NOT `account_removed` AND `uid` > ?", 0]); while ($user = DBA::fetch($users)) { Worker::add(Worker::PRIORITY_LOW, 'ContactDiscoveryForUser', $user['uid']); } -- 2.39.5