diff --git a/mod/suggest.php b/mod/suggest.php index d592537f48..f3421de47e 100644 --- a/mod/suggest.php +++ b/mod/suggest.php @@ -26,7 +26,6 @@ use Friendica\Core\Renderer; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; -use Friendica\Model\GContact; function suggest_init(App $a) { @@ -60,9 +59,8 @@ function suggest_content(App $a) DI::page()['aside'] .= Widget::follow(); - $r = GContact::suggestionQuery(local_user()); - - if (! DBA::isResult($r)) { + $contacts = Contact::getSuggestions(local_user()); + if (!DBA::isResult($contacts)) { $o .= DI::l10n()->t('No suggestions available. If this is a new site, please try again in 24 hours.'); return $o; } @@ -94,35 +92,20 @@ function suggest_content(App $a) $id = 0; $entries = []; - foreach ($r as $rr) { - $connlnk = DI::baseUrl() . '/follow/?url=' . (($rr['connect']) ? $rr['connect'] : $rr['url']); - $ignlnk = DI::baseUrl() . '/suggest?ignore=' . $rr['id']; - $photo_menu = [ - 'profile' => [DI::l10n()->t("View Profile"), Contact::magicLink($rr["url"])], - 'follow' => [DI::l10n()->t("Connect/Follow"), $connlnk], - 'hide' => [DI::l10n()->t('Ignore/Hide'), $ignlnk] - ]; - - $contact_details = Contact::getByURLForUser($rr["url"], local_user()) ?: $rr; - + foreach ($contacts as $contact) { $entry = [ - 'url' => Contact::magicLink($rr['url']), - 'itemurl' => (($contact_details['addr'] != "") ? $contact_details['addr'] : $rr['url']), - 'img_hover' => $rr['url'], - 'name' => $contact_details['name'], - 'thumb' => Contact::getThumb($contact_details), - 'details' => $contact_details['location'], - 'tags' => $contact_details['keywords'], - 'about' => $contact_details['about'], - 'account_type' => Contact::getAccountType($contact_details), - 'ignlnk' => $ignlnk, - 'ignid' => $rr['id'], - 'conntxt' => DI::l10n()->t('Connect'), - 'connlnk' => $connlnk, - 'photo_menu' => $photo_menu, - 'ignore' => DI::l10n()->t('Ignore/Hide'), - 'network' => ContactSelector::networkToName($rr['network'], $rr['url']), - 'id' => ++$id, + 'url' => Contact::magicLink($contact['url']), + 'itemurl' => $contact['addr'] ?: $contact['url'], + 'name' => $contact['name'], + 'thumb' => Contact::getThumb($contact), + 'img_hover' => $contact['url'], + 'details' => $contact['location'], + 'tags' => $contact['keywords'], + 'about' => $contact['about'], + 'account_type' => Contact::getAccountType($contact), + 'network' => ContactSelector::networkToName($contact['network'], $contact['url']), + 'photo_menu' => Contact::photoMenu($contact), + 'id' => ++$id, ]; $entries[] = $entry; } diff --git a/src/Core/Search.php b/src/Core/Search.php index 577b11266e..7f9ffeec5d 100644 --- a/src/Core/Search.php +++ b/src/Core/Search.php @@ -24,7 +24,6 @@ namespace Friendica\Core; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; -use Friendica\Model\GContact; use Friendica\Network\HTTPException; use Friendica\Object\Search\ContactResult; use Friendica\Object\Search\ResultList; @@ -170,6 +169,8 @@ class Search */ public static function getContactsFromLocalDirectory($search, $type = self::TYPE_ALL, $start = 0, $itemPage = 80) { + Logger::info('Searching', ['search' => $search, 'type' => $type, 'start' => $start, 'itempage' => $itemPage]); + $config = DI::config(); $diaspora = $config->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::DFRN; @@ -177,18 +178,20 @@ class Search $wildcard = Strings::escapeHtml('%' . $search . '%'); - $count = DBA::count('gcontact', [ - 'NOT `hide` + $condition = [ + 'NOT `unsearchable` AND `network` IN (?, ?, ?, ?) - AND NOT `failed` + AND NOT `failed` AND `uid` = ? AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) - AND `community` = ?', - Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, + AND `forum` = ?', + Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, 0, $wildcard, $wildcard, $wildcard, $wildcard, $wildcard, $wildcard, ($type === self::TYPE_FORUM), - ]); + ]; + + $count = DBA::count('contact', $condition); $resultList = new ResultList($start, $itemPage, $count); @@ -196,18 +199,7 @@ class Search return $resultList; } - $data = DBA::select('gcontact', ['nurl', 'name', 'addr', 'url', 'photo', 'network', 'keywords'], [ - 'NOT `hide` - AND `network` IN (?, ?, ?, ?) - AND NOT `failed` - AND (`url` LIKE ? OR `name` LIKE ? OR `location` LIKE ? - OR `addr` LIKE ? OR `about` LIKE ? OR `keywords` LIKE ?) - AND `community` = ?', - Protocol::ACTIVITYPUB, Protocol::DFRN, $ostatus, $diaspora, - $wildcard, $wildcard, $wildcard, - $wildcard, $wildcard, $wildcard, - ($type === self::TYPE_FORUM), - ], [ + $data = DBA::select('contact', [], $condition, [ 'group_by' => ['nurl', 'updated'], 'limit' => [$start, $itemPage], 'order' => ['updated' => 'DESC'] @@ -217,21 +209,7 @@ class Search return $resultList; } - while ($row = DBA::fetch($data)) { - $urlParts = parse_url($row["nurl"]); - - // Ignore results that look strange. - // For historic reasons the gcontact table does contain some garbage. - if (!empty($urlParts['query']) || !empty($urlParts['fragment'])) { - continue; - } - - $contact = Contact::getByURLForUser($row["nurl"], local_user()) ?: $row; - - if ($contact["name"] == "") { - $contact["name"] = end(explode("/", $urlParts["path"])); - } - + while ($contact = DBA::fetch($data)) { $result = new ContactResult( $contact["name"], $contact["addr"], @@ -256,7 +234,7 @@ class Search } /** - * Searching for global contacts for autocompletion + * Searching for contacts for autocompletion * * @param string $search Name or part of a name or nick * @param string $mode Search mode (e.g. "community") @@ -264,8 +242,10 @@ class Search * @return array with the search results * @throws HTTPException\InternalServerErrorException */ - public static function searchGlobalContact($search, $mode, int $page = 1) + public static function searchContact($search, $mode, int $page = 1) { + Logger::info('Searching', ['search' => $search, 'mode' => $mode, 'page' => $page]); + if (DI::config()->get('system', 'block_public') && !Session::isAuthenticated()) { return []; } @@ -281,7 +261,7 @@ class Search // check if searching in the local global contact table is enabled if (DI::config()->get('system', 'poco_local_search')) { - $return = GContact::searchByName($search, $mode); + $return = Contact::searchByName($search, $mode); } else { $p = $page > 1 ? 'p=' . $page : ''; $curlResult = DI::httpRequest()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), false, ['accept_content' => 'application/json']); diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 855f90431c..61b4c9d05a 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -2915,4 +2915,157 @@ class Contact return in_array($protocol, [Protocol::DFRN, Protocol::DIASPORA, Protocol::ACTIVITYPUB]) && !$self; } + + /** + * Search contact table by nick or name + * + * @param string $search Name or nick + * @param string $mode Search mode (e.g. "community") + * + * @return array with search results + * @throws \Friendica\Network\HTTPException\InternalServerErrorException + */ + public static function searchByName($search, $mode = '') + { + if (empty($search)) { + return []; + } + + // check supported networks + if (DI::config()->get('system', 'diaspora_enabled')) { + $diaspora = Protocol::DIASPORA; + } else { + $diaspora = Protocol::DFRN; + } + + if (!DI::config()->get('system', 'ostatus_disabled')) { + $ostatus = Protocol::OSTATUS; + } else { + $ostatus = Protocol::DFRN; + } + + // check if we search only communities or every contact + if ($mode === 'community') { + $extra_sql = sprintf(' AND `contact-type` = %d', Contact::TYPE_COMMUNITY); + } else { + $extra_sql = ''; + } + + $search .= '%'; + + $results = DBA::p("SELECT * FROM `contact` + WHERE NOT `unsearchable` AND `network` IN (?, ?, ?, ?) AND + NOT `failed` AND `uid` = ? AND + (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql + ORDER BY `nurl` DESC LIMIT 1000", + Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, 0, $search, $search, $search + ); + + $contacts = DBA::toArray($results); + return $contacts; + } + + /** + * @param int $uid user + * @param int $start optional, default 0 + * @param int $limit optional, default 80 + * @return array + */ + static public function getSuggestions(int $uid, int $start = 0, int $limit = 80) + { + $cid = self::getPublicIdByUserId($uid); + $totallimit = $start + $limit; + $contacts = []; + + Logger::info('Collecting suggestions', ['uid' => $uid, 'cid' => $cid, 'start' => $start, 'limit' => $limit]); + + $diaspora = DI::config()->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::ACTIVITYPUB; + $ostatus = !DI::config()->get('system', 'ostatus_disabled') ? Protocol::OSTATUS : Protocol::ACTIVITYPUB; + + // The query returns contacts where contacts interacted with whom the given user follows. + // Contacts who already are in the user's contact table are ignored. + $results = DBA::select('contact', [], + ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` IN + (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ?) + AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN + (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?)))) + AND NOT `hidden` AND `network` IN (?, ?, ?, ?)", + $cid, 0, $uid, Contact::FRIEND, Contact::SHARING, + Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus], + ['order' => ['last-item' => true], 'limit' => $totallimit] + ); + + while ($contact = DBA::fetch($results)) { + $contacts[$contact['id']] = $contact; + } + DBA::close($results); + + Logger::info('Contacts of contacts who are followed by the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]); + + if (count($contacts) >= $totallimit) { + return array_slice($contacts, $start, $limit); + } + + // The query returns contacts where contacts interacted with whom also interacted with the given user. + // Contacts who already are in the user's contact table are ignored. + $results = DBA::select('contact', [], + ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` IN + (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?) + AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN + (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?)))) + AND NOT `hidden` AND `network` IN (?, ?, ?, ?)", + $cid, 0, $uid, Contact::FRIEND, Contact::SHARING, + Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus], + ['order' => ['last-item' => true], 'limit' => $totallimit] + ); + + while ($contact = DBA::fetch($results)) { + $contacts[$contact['id']] = $contact; + } + DBA::close($results); + + Logger::info('Contacts of contacts who are following the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]); + + if (count($contacts) >= $totallimit) { + return array_slice($contacts, $start, $limit); + } + + // The query returns contacts that follow the given user but aren't followed by that user. + $results = DBA::select('contact', [], + ["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` = ?) + AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)", + $uid, Contact::FOLLOWER, 0, + Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus], + ['order' => ['last-item' => true], 'limit' => $totallimit] + ); + + while ($contact = DBA::fetch($results)) { + $contacts[$contact['id']] = $contact; + } + DBA::close($results); + + Logger::info('Followers that are not followed by the given user', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]); + + if (count($contacts) >= $totallimit) { + return array_slice($contacts, $start, $limit); + } + + // The query returns any contact that isn't followed by that user. + $results = DBA::select('contact', [], + ["NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?)) + AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)", + $uid, Contact::FRIEND, Contact::SHARING, 0, + Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus], + ['order' => ['last-item' => true], 'limit' => $totallimit] + ); + + while ($contact = DBA::fetch($results)) { + $contacts[$contact['id']] = $contact; + } + DBA::close($results); + + Logger::info('Any contact', ['uid' => $uid, 'cid' => $cid, 'count' => count($contacts)]); + + return array_slice($contacts, $start, $limit); + } } diff --git a/src/Model/GContact.php b/src/Model/GContact.php index ec53133c94..fdf2d14077 100644 --- a/src/Model/GContact.php +++ b/src/Model/GContact.php @@ -28,14 +28,12 @@ use Friendica\Core\Logger; use Friendica\Core\Protocol; use Friendica\Core\Search; use Friendica\Core\System; -use Friendica\Core\Worker; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\Probe; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\PortableContact; use Friendica\Util\DateTimeFormat; -use Friendica\Util\Network; use Friendica\Util\Strings; /** @@ -43,67 +41,6 @@ use Friendica\Util\Strings; */ class GContact { - /** - * Search global contact table by nick or name - * - * @param string $search Name or nick - * @param string $mode Search mode (e.g. "community") - * - * @return array with search results - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function searchByName($search, $mode = '') - { - if (empty($search)) { - return []; - } - - // check supported networks - if (DI::config()->get('system', 'diaspora_enabled')) { - $diaspora = Protocol::DIASPORA; - } else { - $diaspora = Protocol::DFRN; - } - - if (!DI::config()->get('system', 'ostatus_disabled')) { - $ostatus = Protocol::OSTATUS; - } else { - $ostatus = Protocol::DFRN; - } - - // check if we search only communities or every contact - if ($mode === 'community') { - $extra_sql = ' AND `community`'; - } else { - $extra_sql = ''; - } - - $search .= '%'; - - $results = DBA::p("SELECT `nurl` FROM `gcontact` - WHERE NOT `hide` AND `network` IN (?, ?, ?, ?) AND - NOT `failed` AND - (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?) $extra_sql - GROUP BY `nurl` ORDER BY `nurl` DESC LIMIT 1000", - Protocol::DFRN, Protocol::ACTIVITYPUB, $ostatus, $diaspora, $search, $search, $search - ); - - $gcontacts = []; - while ($result = DBA::fetch($results)) { - $urlparts = parse_url($result['nurl']); - - // Ignore results that look strange. - // For historic reasons the gcontact table does contain some garbage. - if (empty($result['nurl']) || !empty($urlparts['query']) || !empty($urlparts['fragment'])) { - continue; - } - - $gcontacts[] = Contact::getByURLForUser($result['nurl'], local_user()); - } - DBA::close($results); - return $gcontacts; - } - /** * Link the gcontact entry with user, contact and global contact * @@ -424,92 +361,6 @@ class GContact return $r; } - /** - * @param int $uid user - * @param integer $start optional, default 0 - * @param integer $limit optional, default 80 - * @return array - * @throws \Friendica\Network\HTTPException\InternalServerErrorException - */ - public static function suggestionQuery($uid, $start = 0, $limit = 80) - { - if (!$uid) { - return []; - } - - $network = [Protocol::DFRN, Protocol::ACTIVITYPUB]; - - if (DI::config()->get('system', 'diaspora_enabled')) { - $network[] = Protocol::DIASPORA; - } - - if (!DI::config()->get('system', 'ostatus_disabled')) { - $network[] = Protocol::OSTATUS; - } - - $sql_network = "'" . implode("', '", $network) . "'"; - - /// @todo This query is really slow - // By now we cache the data for five minutes - $r = q( - "SELECT count(glink.gcid) as `total`, gcontact.* from gcontact - INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id` - where uid = %d and not gcontact.nurl in ( select nurl from contact where uid = %d ) - AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d) - AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d) - AND `gcontact`.`updated` >= '%s' AND NOT `gcontact`.`hide` - AND NOT `gcontact`.`failed` - AND `gcontact`.`network` IN (%s) - GROUP BY `glink`.`gcid` ORDER BY `gcontact`.`updated` DESC,`total` DESC LIMIT %d, %d", - intval($uid), - intval($uid), - intval($uid), - intval($uid), - DBA::NULL_DATETIME, - $sql_network, - intval($start), - intval($limit) - ); - - if (DBA::isResult($r) && count($r) >= ($limit -1)) { - return $r; - } - - $r2 = q( - "SELECT gcontact.* FROM gcontact - INNER JOIN `glink` ON `glink`.`gcid` = `gcontact`.`id` - WHERE `glink`.`uid` = 0 AND `glink`.`cid` = 0 AND `glink`.`zcid` = 0 AND NOT `gcontact`.`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = %d) - AND NOT `gcontact`.`name` IN (SELECT `name` FROM `contact` WHERE `uid` = %d) - AND NOT `gcontact`.`id` IN (SELECT `gcid` FROM `gcign` WHERE `uid` = %d) - AND `gcontact`.`updated` >= '%s' - AND NOT `gcontact`.`failed` - AND `gcontact`.`network` IN (%s) - ORDER BY rand() LIMIT %d, %d", - intval($uid), - intval($uid), - intval($uid), - DBA::NULL_DATETIME, - $sql_network, - intval($start), - intval($limit) - ); - - $list = []; - foreach ($r2 as $suggestion) { - $list[$suggestion['nurl']] = $suggestion; - } - - foreach ($r as $suggestion) { - $list[$suggestion['nurl']] = $suggestion; - } - - while (sizeof($list) > ($limit)) { - array_pop($list); - } - - return $list; - } - /** * @return void * @throws \Friendica\Network\HTTPException\InternalServerErrorException diff --git a/src/Module/Search/Acl.php b/src/Module/Search/Acl.php index 1e08adbe66..7e7a0d27f5 100644 --- a/src/Module/Search/Acl.php +++ b/src/Module/Search/Acl.php @@ -74,22 +74,17 @@ class Acl extends BaseModule $mode = $_REQUEST['smode']; $page = $_REQUEST['page'] ?? 1; - $r = Search::searchGlobalContact($search, $mode, $page); + $result = Search::searchContact($search, $mode, $page); $contacts = []; - foreach ($r as $g) { - if (empty($g['name'])) { - DI::logger()->warning('Wrong result item from Search::searchGlobalContact', ['$g' => $g, '$search' => $search, '$mode' => $mode, '$page' => $page]); - continue; - } - $contact = Contact::getByURL($g['url']); + foreach ($result as $contact) { $contacts[] = [ - 'photo' => Contact::getMicro($contact, $g['photo']), - 'name' => htmlspecialchars($contact['name'] ?? $g['name']), - 'nick' => $contact['nick'] ?? ($g['addr'] ?: $g['url']), - 'network' => $contact['network'] ?? $g['network'], - 'link' => $g['url'], - 'forum' => !empty($g['community']), + 'photo' => Contact::getMicro($contact), + 'name' => htmlspecialchars($contact['name']), + 'nick' => $contact['nick'], + 'network' => $contact['network'], + 'link' => $contact['url'], + 'forum' => $contact['contact-type'] == Contact::TYPE_COMMUNITY, ]; } diff --git a/static/dbstructure.config.php b/static/dbstructure.config.php index 56412b20bd..e9f70bec20 100755 --- a/static/dbstructure.config.php +++ b/static/dbstructure.config.php @@ -202,6 +202,7 @@ return [ "dfrn-id" => ["dfrn-id(64)"], "issued-id" => ["issued-id(64)"], "network_uid_lastupdate" => ["network", "uid", "last-update"], + "uid_lastitem" => ["uid", "last-item"], "gsid" => ["gsid"] ] ], diff --git a/view/theme/vier/theme.php b/view/theme/vier/theme.php index 3f7a1a66a9..f4452e3c76 100644 --- a/view/theme/vier/theme.php +++ b/view/theme/vier/theme.php @@ -118,20 +118,19 @@ function vier_community_info() // comunity_profiles if ($show_profiles) { - $r = GContact::suggestionQuery(local_user(), 0, 9); + $contacts = Contact::getSuggestions(local_user(), 0, 9); $tpl = Renderer::getMarkupTemplate('ch_directory_item.tpl'); - if (DBA::isResult($r)) { + if (DBA::isResult($contacts)) { $aside['$comunity_profiles_title'] = DI::l10n()->t('Community Profiles'); $aside['$comunity_profiles_items'] = []; - foreach ($r as $rr) { - $contact = Contact::getByURL($rr['url']); + foreach ($contacts as $contact) { $entry = Renderer::replaceMacros($tpl, [ - '$id' => $rr['id'], - '$profile_link' => 'follow/?url='.urlencode($rr['url']), - '$photo' => Contact::getMicro($contact, $rr['photo']), - '$alt_text' => $contact['name'] ?? $rr['name'], + '$id' => $contact['id'], + '$profile_link' => 'follow/?url='.urlencode($contact['url']), + '$photo' => Contact::getMicro($contact), + '$alt_text' => $contact['name'], ]); $aside['$comunity_profiles_items'][] = $entry; }