X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FContact.php;h=9ebc12375a1aa161ed63ba283af38b5d431bd269;hb=48424334490b23b47fedbd58460f93f29a078be7;hp=646da57caa6170013b996fa62d81261643c69881;hpb=8172fbe75f0faf43a86c8650c1f0a96f39453989;p=friendica.git diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 646da57caa..9ebc12375a 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -43,6 +43,7 @@ use Friendica\Protocol\Salmon; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; use Friendica\Util\Network; +use Friendica\Util\Proxy; use Friendica\Util\Strings; /** @@ -206,7 +207,12 @@ class Contact if (empty($cid)) { return []; } - return self::getById($cid, $fields); + + $contact = self::getById($cid, $fields); + if (empty($contact)) { + return []; + } + return $contact; } // Add internal fields @@ -237,6 +243,10 @@ class Contact $contact = DBA::selectFirst('contact', $fields, $condition, $options); } + if (!DBA::isResult($contact)) { + return []; + } + // Update the contact in the background if needed if ((($contact['updated'] < DateTimeFormat::utc('now -7 days')) || empty($contact['avatar'])) && in_array($contact['network'], Protocol::FEDERATED)) { @@ -1029,7 +1039,7 @@ class Contact { // Always unarchive the relay contact entry if (!empty($contact['batch']) && !empty($contact['term-date']) && ($contact['term-date'] > DBA::NULL_DATETIME)) { - $fields = ['term-date' => DBA::NULL_DATETIME, 'archive' => false]; + $fields = ['failed' => false, 'term-date' => DBA::NULL_DATETIME, 'archive' => false]; $condition = ['uid' => 0, 'network' => Protocol::FEDERATED, 'batch' => $contact['batch'], 'contact-type' => self::TYPE_RELAY]; DBA::update('contact', $fields, $condition); } @@ -1053,7 +1063,7 @@ class Contact } // It's a miracle. Our dead contact has inexplicably come back to life. - $fields = ['term-date' => DBA::NULL_DATETIME, 'archive' => false]; + $fields = ['failed' => false, 'term-date' => DBA::NULL_DATETIME, 'archive' => false]; DBA::update('contact', $fields, ['id' => $contact['id']]); DBA::update('contact', $fields, ['nurl' => Strings::normaliseLink($contact['url']), 'self' => false]); GContact::updateFromPublicContactURL($contact['url']); @@ -1418,7 +1428,6 @@ class Contact 'poll' => $data['poll'] ?? '', 'name' => $data['name'] ?? '', 'nick' => $data['nick'] ?? '', - 'photo' => $data['photo'] ?? '', 'keywords' => $data['keywords'] ?? '', 'location' => $data['location'] ?? '', 'about' => $data['about'] ?? '', @@ -1464,7 +1473,7 @@ class Contact } if (!empty($data['photo']) && ($data['network'] != Protocol::FEED)) { - self::updateAvatar($data['photo'], $uid, $contact_id); + self::updateAvatar($contact_id, $data['photo']); } if (in_array($data["network"], array_merge(Protocol::NATIVE_SUPPORT, [Protocol::PUMPIO]))) { @@ -1474,14 +1483,6 @@ class Contact } else { // Else do a direct update self::updateFromProbe($contact_id, '', false); - - // Update the gcontact entry - if ($uid == 0) { - GContact::updateFromPublicContactID($contact_id); - if (($data['network'] == Protocol::ACTIVITYPUB) && in_array(DI::config()->get('system', 'gcontact_discovery'), [GContact::DISCOVERY_DIRECT, GContact::DISCOVERY_RECURSIVE])) { - GContact::discoverFollowers($data['url']); - } - } } } else { $fields = ['url', 'nurl', 'addr', 'alias', 'name', 'nick', 'keywords', 'location', 'about', 'avatar-date', 'baseurl', 'gsid']; @@ -1495,7 +1496,8 @@ class Contact $updated = [ 'url' => $data['url'], 'nurl' => Strings::normaliseLink($data['url']), - 'updated' => DateTimeFormat::utcNow() + 'updated' => DateTimeFormat::utcNow(), + 'failed' => false ]; $fields = ['addr', 'alias', 'name', 'nick', 'keywords', 'location', 'about', 'baseurl', 'gsid']; @@ -1774,12 +1776,146 @@ class Contact return $return; } + /** + * Ensure that cached avatar exist + * + * @param integer $cid + */ + public static function checkAvatarCache(int $cid) + { + $contact = DBA::selectFirst('contact', ['url', 'avatar', 'photo', 'thumb', 'micro'], ['id' => $cid, 'uid' => 0, 'self' => false]); + if (!DBA::isResult($contact)) { + return; + } + + if (empty($contact['avatar']) || (!empty($contact['photo']) && !empty($contact['thumb']) && !empty($contact['micro']))) { + return; + } + + Logger::info('Adding avatar cache', ['id' => $cid, 'contact' => $contact]); + + self::updateAvatar($cid, $contact['avatar'], true); + } + + /** + * Return the photo path for a given contact array in the given size + * + * @param array $contact contact array + * @param string $field Fieldname of the photo in the contact array + * @param string $default Default path when no picture had been found + * @param string $size Size of the avatar picture + * @param string $avatar Avatar path that is displayed when no photo had been found + * @return string photo path + */ + private static function getAvatarPath(array $contact, string $field, string $default, string $size, string $avatar) + { + if (!empty($contact)) { + $contact = self::checkAvatarCacheByArray($contact); + if (!empty($contact[$field])) { + $avatar = $contact[$field]; + } + } + + if (empty($avatar)) { + return $default; + } + + if (Proxy::isLocalImage($avatar)) { + return $avatar; + } else { + return Proxy::proxifyUrl($avatar, false, $size); + } + } + + /** + * Return the photo path for a given contact array + * + * @param array $contact Contact array + * @param string $avatar Avatar path that is displayed when no photo had been found + * @return string photo path + */ + public static function getPhoto(array $contact, string $avatar = '') + { + return self::getAvatarPath($contact, 'photo', DI::baseUrl() . '/images/person-300.jpg', Proxy::SIZE_SMALL, $avatar); + } + + /** + * Return the photo path (thumb size) for a given contact array + * + * @param array $contact Contact array + * @param string $avatar Avatar path that is displayed when no photo had been found + * @return string photo path + */ + public static function getThumb(array $contact, string $avatar = '') + { + return self::getAvatarPath($contact, 'thumb', DI::baseUrl() . '/images/person-80.jpg', Proxy::SIZE_THUMB, $avatar); + } + + /** + * Return the photo path (micro size) for a given contact array + * + * @param array $contact Contact array + * @param string $avatar Avatar path that is displayed when no photo had been found + * @return string photo path + */ + public static function getMicro(array $contact, string $avatar = '') + { + return self::getAvatarPath($contact, 'micro', DI::baseUrl() . '/images/person-48.jpg', Proxy::SIZE_MICRO, $avatar); + } + + /** + * Check the given contact array for avatar cache fields + * + * @param array $contact + * @return array contact array with avatar cache fields + */ + private static function checkAvatarCacheByArray(array $contact) + { + $update = false; + $contact_fields = []; + $fields = ['photo', 'thumb', 'micro']; + foreach ($fields as $field) { + if (isset($contact[$field])) { + $contact_fields[] = $field; + } + if (isset($contact[$field]) && empty($contact[$field])) { + $update = true; + } + } + + if (!$update) { + return $contact; + } + + if (!empty($contact['id']) && !empty($contact['avatar'])) { + self::updateAvatar($contact['id'], $contact['avatar'], true); + + $new_contact = self::getById($contact['id'], $contact_fields); + if (DBA::isResult($new_contact)) { + // We only update the cache fields + $contact = array_merge($contact, $new_contact); + } + } + + /// add the default avatars if the fields aren't filled + if (isset($contact['photo']) && empty($contact['photo'])) { + $contact['photo'] = DI::baseUrl() . '/images/person-300.jpg'; + } + if (isset($contact['thumb']) && empty($contact['thumb'])) { + $contact['thumb'] = DI::baseUrl() . '/images/person-80.jpg'; + } + if (isset($contact['micro']) && empty($contact['micro'])) { + $contact['micro'] = DI::baseUrl() . '/images/person-48.jpg'; + } + + return $contact; + } + /** * Updates the avatar links in a contact only if needed * - * @param string $avatar Link to avatar picture - * @param int $uid User id of contact owner * @param int $cid Contact id + * @param string $avatar Link to avatar picture * @param bool $force force picture update * * @return void @@ -1787,41 +1923,51 @@ class Contact * @throws HTTPException\NotFoundException * @throws \ImagickException */ - public static function updateAvatar($avatar, $uid, $cid, $force = false) + public static function updateAvatar(int $cid, string $avatar, bool $force = false) { - $contact = DBA::selectFirst('contact', ['avatar', 'photo', 'thumb', 'micro', 'nurl'], ['id' => $cid, 'self' => false]); + $contact = DBA::selectFirst('contact', ['uid', 'avatar', 'photo', 'thumb', 'micro', 'nurl'], ['id' => $cid, 'self' => false]); if (!DBA::isResult($contact)) { return; } + $uid = $contact['uid']; + + // Only update the cached photo links of public contacts when they already are cached + if (($uid == 0) && !$force && empty($contact['thumb']) && empty($contact['micro'])) { + if ($contact['avatar'] != $avatar) { + DBA::update('contact', ['avatar' => $avatar], ['id' => $cid]); + Logger::info('Only update the avatar', ['id' => $cid, 'avatar' => $avatar, 'contact' => $contact]); + } + return; + } + $data = [ $contact['photo'] ?? '', $contact['thumb'] ?? '', $contact['micro'] ?? '', ]; - foreach ($data as $image_uri) { - $image_rid = Photo::ridFromURI($image_uri); - if ($image_rid && !Photo::exists(['resource-id' => $image_rid, 'uid' => $uid])) { - Logger::info('Regenerating avatar', ['contact uid' => $uid, 'cid' => $cid, 'missing photo' => $image_rid, 'avatar' => $contact['avatar']]); - $force = true; + $update = ($contact['avatar'] != $avatar) || $force; + + if (!$update) { + foreach ($data as $image_uri) { + $image_rid = Photo::ridFromURI($image_uri); + if ($image_rid && !Photo::exists(['resource-id' => $image_rid, 'uid' => $uid])) { + Logger::info('Regenerating avatar', ['contact uid' => $uid, 'cid' => $cid, 'missing photo' => $image_rid, 'avatar' => $contact['avatar']]); + $update = true; + } } } - if (($contact["avatar"] != $avatar) || $force) { + if ($update) { $photos = Photo::importProfilePhoto($avatar, $uid, $cid, true); - if ($photos) { $fields = ['avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'avatar-date' => DateTimeFormat::utcNow()]; DBA::update('contact', $fields, ['id' => $cid]); - - // Update the public contact (contact id = 0) - if ($uid != 0) { - $pcontact = DBA::selectFirst('contact', ['id'], ['nurl' => $contact['nurl'], 'uid' => 0]); - if (DBA::isResult($pcontact)) { - DBA::update('contact', $fields, ['id' => $pcontact['id']]); - } - } + } elseif (empty($contact['avatar'])) { + // Ensure that the avatar field is set + DBA::update('contact', ['avatar' => $avatar], ['id' => $cid]); + Logger::info('Failed profile import', ['id' => $cid, 'force' => $force, 'avatar' => $avatar, 'contact' => $contact]); } } } @@ -1938,7 +2084,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function updateFromProbe($id, $network = '', $force = false) + public static function updateFromProbe(int $id, string $network = '', bool $force = false) { /* Warning: Never ever fetch the public key via Probe::uri and write it into the contacts. @@ -1973,7 +2119,7 @@ class Contact // We check after the probing to be able to correct falsely detected contact types. if (($contact['contact-type'] == self::TYPE_RELAY) && (!Strings::compareLink($ret['url'], $contact['url']) || in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]))) { - self::updateContact($id, $uid, $contact['url'], ['last-update' => $updated, 'success_update' => $updated]); + self::updateContact($id, $uid, $contact['url'], ['failed' => false, 'last-update' => $updated, 'success_update' => $updated]); Logger::info('Not updating relais', ['id' => $id, 'url' => $contact['url']]); return true; } @@ -1981,11 +2127,15 @@ class Contact // If Probe::uri fails the network code will be different ("feed" or "unkn") if (in_array($ret['network'], [Protocol::FEED, Protocol::PHANTOM]) && ($ret['network'] != $contact['network'])) { if ($force && ($uid == 0)) { - self::updateContact($id, $uid, $ret['url'], ['last-update' => $updated, 'failure_update' => $updated]); + self::updateContact($id, $uid, $ret['url'], ['failed' => true, 'last-update' => $updated, 'failure_update' => $updated]); } return false; } + if (ContactRelation::isDiscoverable($ret['url'])) { + Worker::add(PRIORITY_LOW, 'ContactDiscovery', $ret['url']); + } + if (isset($ret['hide']) && is_bool($ret['hide'])) { $ret['unsearchable'] = $ret['hide']; } @@ -2005,6 +2155,11 @@ class Contact $new_pubkey = $ret['pubkey']; + // Update the gcontact entry + if ($uid == 0) { + GContact::updateFromPublicContactID($id); + } + $update = false; // make sure to not overwrite existing values with blank entries except some technical fields @@ -2020,12 +2175,12 @@ class Contact } if (!empty($ret['photo']) && ($ret['network'] != Protocol::FEED)) { - self::updateAvatar($ret['photo'], $uid, $id, $update || $force); + self::updateAvatar($id, $ret['photo'], $update || $force); } if (!$update) { if ($force) { - self::updateContact($id, $uid, $ret['url'], ['last-update' => $updated, 'success_update' => $updated]); + self::updateContact($id, $uid, $ret['url'], ['failed' => false, 'last-update' => $updated, 'success_update' => $updated]); } // Update the public contact @@ -2055,6 +2210,7 @@ class Contact if ($force && ($uid == 0)) { $ret['last-update'] = $updated; $ret['success_update'] = $updated; + $ret['failed'] = false; } unset($ret['photo']); @@ -2309,7 +2465,7 @@ class Contact Group::addMember(User::getDefaultGroup($user['uid'], $contact["network"]), $contact_id); // Update the avatar - self::updateAvatar($ret['photo'], $user['uid'], $contact_id); + self::updateAvatar($contact_id, $ret['photo']); // pull feed and consume it, which should subscribe to the hub. @@ -2477,7 +2633,6 @@ class Contact 'nurl' => Strings::normaliseLink($url), 'name' => $name, 'nick' => $nick, - 'photo' => $photo, 'network' => $network, 'rel' => self::FOLLOWER, 'blocked' => 0, @@ -2491,7 +2646,7 @@ class Contact // Ensure to always have the correct network type, independent from the connection request method self::updateFromProbe($contact_id, '', true); - Contact::updateAvatar($photo, $importer["uid"], $contact_id, true); + self::updateAvatar($contact_id, $photo, true); $contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]); @@ -2760,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 who 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 who 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); + } }