From 24a82110fd45a8422efee0677b7acc214aa66e9a Mon Sep 17 00:00:00 2001 From: Hypolite Petovan Date: Wed, 29 Jul 2020 11:30:54 -0400 Subject: [PATCH] Add common relationship methods to Model\ContactRelation - Introduce DBA::mergeConditions method - Replace GContact relationship method contents with Model\ContactRelation method calls --- src/BaseRepository.php | 10 +- src/Database/DBA.php | 30 +- src/Model/ContactRelation.php | 335 +++++++++++++++++++++ src/Model/GContact.php | 157 ++++------ src/Module/Api/Twitter/ContactEndpoint.php | 10 +- 5 files changed, 425 insertions(+), 117 deletions(-) diff --git a/src/BaseRepository.php b/src/BaseRepository.php index 64a0d1c510..abec4c119b 100644 --- a/src/BaseRepository.php +++ b/src/BaseRepository.php @@ -109,26 +109,22 @@ abstract class BaseRepository extends BaseFactory */ public function selectByBoundaries(array $condition = [], array $params = [], int $max_id = null, int $since_id = null, int $limit = self::LIMIT) { - $condition = DBA::collapseCondition($condition); + $totalCount = DBA::count(static::$table_name, $condition); $boundCondition = $condition; if (isset($max_id)) { - $boundCondition[0] .= " AND `id` < ?"; - $boundCondition[] = $max_id; + $boundCondition = DBA::mergeConditions($boundCondition, ['`id` < ?', $max_id]); } if (isset($since_id)) { - $boundCondition[0] .= " AND `id` > ?"; - $boundCondition[] = $since_id; + $boundCondition = DBA::mergeConditions($boundCondition, ['`id` > ?', $since_id]); } $params['limit'] = $limit; $models = $this->selectModels($boundCondition, $params); - $totalCount = DBA::count(static::$table_name, $condition); - return new static::$collection_class($models, $totalCount); } diff --git a/src/Database/DBA.php b/src/Database/DBA.php index f3edf52be5..46bd871b4a 100644 --- a/src/Database/DBA.php +++ b/src/Database/DBA.php @@ -539,7 +539,7 @@ class DBA * Returns the SQL condition string built from the provided condition array * * This function operates with two modes. - * - Supplied with a filed/value associative array, it builds simple strict + * - Supplied with a field/value associative array, it builds simple strict * equality conditions linked by AND. * - Supplied with a flat list, the first element is the condition string and * the following arguments are the values to be interpolated @@ -645,6 +645,34 @@ class DBA return $condition; } + /** + * Merges the provided conditions into a single collapsed one + * + * @param array ...$conditions One or more condition arrays + * @return array A collapsed condition + * @see DBA::collapseCondition() for the condition array formats + */ + public static function mergeConditions(array ...$conditions) + { + $conditionStrings = []; + $result = []; + + foreach ($conditions as $key => $condition) { + $condition = self::collapseCondition($condition); + + $conditionStrings[] = array_shift($condition); + // The result array holds the eventual parameter values + $result = array_merge($result, $condition); + } + + if (count($conditionStrings)) { + // We prepend the condition string at the end to form a collapsed condition array again + array_unshift($result, implode(' AND ', $conditionStrings)); + } + + return $result; + } + /** * Returns the SQL parameter string built from the provided parameter array * diff --git a/src/Model/ContactRelation.php b/src/Model/ContactRelation.php index e8b0f81f03..85dbc93b9b 100644 --- a/src/Model/ContactRelation.php +++ b/src/Model/ContactRelation.php @@ -21,6 +21,7 @@ namespace Friendica\Model; +use Exception; use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Database\DBA; @@ -29,6 +30,11 @@ use Friendica\Protocol\ActivityPub; use Friendica\Util\DateTimeFormat; use Friendica\Util\Strings; +/** + * This class provides relationship information based on the `contact-relation` table. + * This table is directional (cid = source, relation-cid = target), references public contacts (with uid=0) and records both + * follows and the last interaction (likes/comments) on public posts. + */ class ContactRelation { /** @@ -200,4 +206,333 @@ class ContactRelation return true; } + + /** + * Counts all the known follows of the provided public contact + * + * @param int $cid Public contact id + * @param array $condition Additional condition on the contact table + * @return int + * @throws Exception + */ + public static function countFollows(int $cid, array $condition = []) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +)', $cid] + ); + + return DI::dba()->count('contact', $condition); + } + + /** + * Returns a paginated list of contacts that are followed the provided public contact. + * + * @param int $cid Public contact id + * @param array $condition Additional condition on the contact table + * @param int $count + * @param int $offset + * @param bool $shuffle + * @return array + * @throws Exception + */ + public static function listFollows(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +)', $cid] + ); + + $follows = DI::dba()->selectToArray( + 'contact', + $condition, + [ + 'limit' => [$offset, $count], + 'order' => [$shuffle ? 'RAND()' : 'name'] + ] + ); + + return $follows; + } + + /** + * Counts all the known followers of the provided public contact + * + * @param int $cid Public contact id + * @param array $condition Additional condition on the contact table + * @return int + * @throws Exception + */ + public static function countFollowers(int $cid, array $condition = []) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +)', $cid] + ); + + return DI::dba()->count('contact', $condition); + } + + /** + * Returns a paginated list of contacts that follow the provided public contact. + * + * @param int $cid Public contact id + * @param array $condition Additional condition on the contact table + * @param int $count + * @param int $offset + * @param bool $shuffle + * @return array + * @throws Exception + */ + public static function listFollowers(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +)', $cid] + ); + + $followers = DI::dba()->selectToArray( + 'contact', + $condition, + [ + 'limit' => [$offset, $count], + 'order' => [$shuffle ? 'RAND()' : 'name'] + ] + ); + + return $followers; + } + + /** + * Counts the number of contacts that both provided public contacts have interacted with at least once. + * Interactions include follows and likes and comments on public posts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition array on the contact table + * @return int + * @throws Exception + */ + public static function countCommon(int $sourceId, int $targetId, array $condition = []) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? +) + AND `id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? +)', $sourceId, $targetId] + ); + + $total = DI::dba()->count('contact', $condition); + + return $total; + } + + /** + * Returns a paginated list of contacts that both provided public contacts have interacted with at least once. + * Interactions include follows and likes and comments on public posts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition on the contact table + * @param int $count + * @param int $offset + * @param bool $shuffle + * @return array + * @throws Exception + */ + public static function listCommon(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false) + { + $condition = DBA::mergeConditions($condition, + ["`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +) + AND `id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +)", $sourceId, $targetId] + ); + + $contacts = DI::dba()->selectToArray( + 'contact', + $condition, + [ + 'limit' => [$offset, $count], + 'order' => [$shuffle ? 'name' : 'RAND()'], + ] + ); + + return $contacts; + } + + + /** + * Counts the number of contacts that are followed by both provided public contacts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition array on the contact table + * @return int + * @throws Exception + */ + public static function countCommonFollows(int $sourceId, int $targetId, array $condition = []) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +) + AND `id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +)', $sourceId, $targetId] + ); + + $total = DI::dba()->count('contact', $condition); + + return $total; + } + + /** + * Returns a paginated list of contacts that are followed by both provided public contacts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition array on the contact table + * @param int $count + * @param int $offset + * @param bool $shuffle + * @return array + * @throws Exception + */ + public static function listCommonFollows(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false) + { + $condition = DBA::mergeConditions($condition, + ["`id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +) + AND `id` IN ( + SELECT `relation-cid` + FROM `contact-relation` + WHERE `cid` = ? + AND `follows` +)", $sourceId, $targetId] + ); + + $contacts = DI::dba()->selectToArray( + 'contact', + $condition, + [ + 'limit' => [$offset, $count], + 'order' => [$shuffle ? 'name' : 'RAND()'], + ] + ); + + return $contacts; + } + + /** + * Counts the number of contacts that follow both provided public contacts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition on the contact table + * @return int + * @throws Exception + */ + public static function countCommonFollowers(int $sourceId, int $targetId, array $condition = []) + { + $condition = DBA::mergeConditions($condition, + ['`id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +) + AND `id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +)', $sourceId, $targetId] + ); + + $total = DI::dba()->count('contact', $condition); + + return $total; + } + + /** + * Returns a paginated list of contacts that follow both provided public contacts. + * + * @param int $sourceId Public contact id + * @param int $targetId Public contact id + * @param array $condition Additional condition on the contact table + * @param int $count + * @param int $offset + * @param bool $shuffle + * @return array + * @throws Exception + */ + public static function listCommonFollowers(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false) + { + $condition = DBA::mergeConditions($condition, + ["`id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +) + AND `id` IN ( + SELECT `cid` + FROM `contact-relation` + WHERE `relation-cid` = ? + AND `follows` +)", $sourceId, $targetId] + ); + + $contacts = DI::dba()->selectToArray( + 'contact', + $condition, + [ + 'limit' => [$offset, $count], + 'order' => [$shuffle ? 'name' : 'RAND()'], + ] + ); + + return $contacts; + } } diff --git a/src/Model/GContact.php b/src/Model/GContact.php index ce224f44c0..55749d3b19 100644 --- a/src/Model/GContact.php +++ b/src/Model/GContact.php @@ -39,22 +39,16 @@ class GContact */ public static function countCommonFriends($uid, $cid) { - $r = q( - "SELECT count(*) as `total` - FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id` - WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND - NOT `gcontact`.`failed` - AND `gcontact`.`nurl` IN (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0 and id != %d) ", - intval($cid), - intval($uid), - intval($uid), - intval($cid) - ); + $sourceId = Contact::getPublicIdByUserId($uid); - if (DBA::isResult($r)) { - return $r[0]['total']; - } - return 0; + $targetIds = Contact::getPublicAndUserContacID($cid, $uid); + + $condition = [ + 'NOT `self` AND NOT `blocked` AND NOT `hidden` AND `id` != ?', + $sourceId, + ]; + + return ContactRelation::countCommonFollows($sourceId, $targetIds['public'] ?? 0, $condition); } /** @@ -65,92 +59,74 @@ class GContact */ public static function countCommonFriendsZcid($uid, $zcid) { - $r = q( - "SELECT count(*) as `total` - FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id` - where `glink`.`zcid` = %d - and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0) ", - intval($zcid), - intval($uid) + $sourceId = Contact::getPublicIdByUserId($uid); + + $targetPublicContact = DI::dba()->fetchFirst(" +SELECT `id` +FROM `contact` c +JOIN `gcontact` z ON z.`nurl` = c.`nurl` +AND z.`id` = ? +AND c.`uid` = 0 +LIMIT 1", + $zcid ); - if (DBA::isResult($r)) { - return $r[0]['total']; - } - - return 0; + return ContactRelation::countCommonFollowers($sourceId, $targetPublicContact['id'] ?? 0); } /** - * @param integer $uid user - * @param integer $cid cid + * Returns the cross-section between the local user contacts and one of their contact's own relationships + * as known by the local node. + * + * @param integer $uid local user id + * @param integer $cid user contact id to compare friends with * @param integer $start optional, default 0 * @param integer $limit optional, default 9999 * @param boolean $shuffle optional, default false - * @return object + * @return array * @throws Exception */ public static function commonFriends($uid, $cid, $start = 0, $limit = 9999, $shuffle = false) { - if ($shuffle) { - $sql_extra = " order by rand() "; - } else { - $sql_extra = " order by `gcontact`.`name` asc "; - } + $sourceId = Contact::getPublicIdByUserId($uid); - $r = q( - "SELECT `gcontact`.*, `contact`.`id` AS `cid` - FROM `glink` - INNER JOIN `gcontact` ON `glink`.`gcid` = `gcontact`.`id` - INNER JOIN `contact` ON `gcontact`.`nurl` = `contact`.`nurl` - WHERE `glink`.`cid` = %d and `glink`.`uid` = %d - AND `contact`.`uid` = %d AND `contact`.`self` = 0 AND `contact`.`blocked` = 0 - AND `contact`.`hidden` = 0 AND `contact`.`id` != %d - AND NOT `gcontact`.`failed` - $sql_extra LIMIT %d, %d", - intval($cid), - intval($uid), - intval($uid), - intval($cid), - intval($start), - intval($limit) - ); + $targetIds = Contact::getPublicAndUserContacID($cid, $uid); - /// @TODO Check all calling-findings of this function if they properly use DBA::isResult() - return $r; + $condition = [ + 'NOT `self` AND NOT `blocked` AND NOT `hidden` AND `id` != ?', + $sourceId, + ]; + + return ContactRelation::listCommonFollows($sourceId, $targetIds['public'] ?? 0, $condition, $limit, $start, $shuffle); } /** - * @param integer $uid user - * @param integer $zcid zcid + * Returns the cross-section between a local user and a remote visitor contact's own relationships + * as known by the local node. + * + * @param integer $uid local user id + * @param integer $zcid remote visitor contact zcid * @param integer $start optional, default 0 * @param integer $limit optional, default 9999 * @param boolean $shuffle optional, default false - * @return object + * @return array * @throws Exception */ public static function commonFriendsZcid($uid, $zcid, $start = 0, $limit = 9999, $shuffle = false) { - if ($shuffle) { - $sql_extra = " order by rand() "; - } else { - $sql_extra = " order by `gcontact`.`name` asc "; - } + $sourceId = Contact::getPublicIdByUserId($uid); - $r = q( - "SELECT `gcontact`.* - FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id` - where `glink`.`zcid` = %d - and `gcontact`.`nurl` in (select nurl from contact where uid = %d and self = 0 and blocked = 0 and hidden = 0) - $sql_extra limit %d, %d", - intval($zcid), - intval($uid), - intval($start), - intval($limit) + $targetPublicContact = DI::dba()->fetchFirst(" +SELECT c.`id` +FROM `contact` c +JOIN `gcontact` z ON z.`nurl` = c.`nurl` +AND z.`id` = ? +AND c.`uid` = 0 +LIMIT 1", + $zcid ); - /// @TODO Check all calling-findings of this function if they properly use DBA::isResult() - return $r; + return ContactRelation::listCommonFollows($sourceId, $targetPublicContact['id'] ?? 0, [], $limit, $start, $shuffle); } /** @@ -161,20 +137,9 @@ class GContact */ public static function countAllFriends($uid, $cid) { - $r = q( - "SELECT count(*) as `total` - FROM `glink` INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id` - where `glink`.`cid` = %d and `glink`.`uid` = %d AND - NOT `gcontact`.`failed`", - intval($cid), - intval($uid) - ); + $cids = Contact::getPublicAndUserContacID($cid, $uid); - if (DBA::isResult($r)) { - return $r[0]['total']; - } - - return 0; + return ContactRelation::countFollows($cids['public'] ?? 0); } /** @@ -187,22 +152,8 @@ class GContact */ public static function allFriends($uid, $cid, $start = 0, $limit = 80) { - $r = q( - "SELECT `gcontact`.*, `contact`.`id` AS `cid` - FROM `glink` - INNER JOIN `gcontact` on `glink`.`gcid` = `gcontact`.`id` - LEFT JOIN `contact` ON `contact`.`nurl` = `gcontact`.`nurl` AND `contact`.`uid` = %d - WHERE `glink`.`cid` = %d AND `glink`.`uid` = %d AND - NOT `gcontact`.`failed` - ORDER BY `gcontact`.`name` ASC LIMIT %d, %d ", - intval($uid), - intval($cid), - intval($uid), - intval($start), - intval($limit) - ); + $cids = Contact::getPublicAndUserContacID($cid, $uid); - /// @TODO Check all calling-findings of this function if they properly use DBA::isResult() - return $r; + return ContactRelation::listFollows($cids['public'] ?? 0, [], $limit, $start); } } diff --git a/src/Module/Api/Twitter/ContactEndpoint.php b/src/Module/Api/Twitter/ContactEndpoint.php index 116f8eea2d..f341019716 100644 --- a/src/Module/Api/Twitter/ContactEndpoint.php +++ b/src/Module/Api/Twitter/ContactEndpoint.php @@ -143,7 +143,7 @@ abstract class ContactEndpoint extends BaseApi $previous_cursor = 0; $total_count = 0; if (!$hide_friends) { - $condition = DBA::collapseCondition([ + $condition = [ 'rel' => $rel, 'uid' => $uid, 'self' => false, @@ -151,17 +151,15 @@ abstract class ContactEndpoint extends BaseApi 'hidden' => false, 'archive' => false, 'pending' => false - ]); + ]; $total_count = DBA::count('contact', $condition); if ($cursor !== -1) { if ($cursor > 0) { - $condition[0] .= " AND `id` > ?"; - $condition[] = $cursor; + $condition = DBA::mergeConditions($condition, ['`id` > ?', $cursor]); } else { - $condition[0] .= " AND `id` < ?"; - $condition[] = -$cursor; + $condition = DBA::mergeConditions($condition, ['`id` < ?', -$cursor]); } }