X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FContact.php;h=3ed2785cfa41972f0676360fbea35df4ca24d3a3;hb=624e4c192c7f837ac0587a50da6e1409081eb519;hp=25af61deced979cc5a82fa986043f2d0103c7aba;hpb=dd422a1e03c2faa8a480a09b740d477167e04e3d;p=friendica.git diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 25af61dece..3ed2785cfa 100644 --- a/src/Model/Contact.php +++ b/src/Model/Contact.php @@ -21,7 +21,7 @@ namespace Friendica\Model; -use Friendica\App\BaseURL; +use Friendica\Contact\Avatar; use Friendica\Contact\Introduction\Exception\IntroductionNotFoundException; use Friendica\Content\Pager; use Friendica\Content\Text\HTML; @@ -35,15 +35,12 @@ use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Network\HTTPClient\Client\HttpClientAccept; -use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Module\NoScrape; use Friendica\Network\HTTPException; use Friendica\Network\Probe; -use Friendica\Object\Image; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Util\DateTimeFormat; -use Friendica\Util\HTTPSignature; use Friendica\Util\Images; use Friendica\Util\Network; use Friendica\Util\Proxy; @@ -686,7 +683,7 @@ class Contact */ public static function updateSelfFromUserID($uid, $update_avatar = false) { - $fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve', + $fields = ['id', 'uri-id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', 'manually-approve', 'xmpp', 'matrix', 'contact-type', 'forum', 'prv', 'avatar-date', 'url', 'nurl', 'unsearchable', 'photo', 'thumb', 'micro', 'header', 'addr', 'request', 'notify', 'poll', 'confirm', 'poco', 'network']; $self = DBA::selectFirst('contact', $fields, ['uid' => $uid, 'self' => true]); @@ -718,6 +715,7 @@ class Contact // it seems as if ported accounts can have wrong values, so we make sure that now everything is fine. $fields['url'] = DI::baseUrl() . '/profile/' . $user['nickname']; $fields['nurl'] = Strings::normaliseLink($fields['url']); + $fields['uri-id'] = ItemURI::getIdByURI($fields['url']); $fields['addr'] = $user['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3); $fields['request'] = DI::baseUrl() . '/dfrn_request/' . $user['nickname']; $fields['notify'] = DI::baseUrl() . '/dfrn_notify/' . $user['nickname']; @@ -775,10 +773,10 @@ class Contact $fields['updated'] = DateTimeFormat::utcNow(); self::update($fields, ['id' => $self['id']]); - // Update the public contact as well - $fields['prvkey'] = null; - $fields['self'] = false; - self::update($fields, ['uid' => 0, 'nurl' => $self['nurl']]); + // Update the other contacts as well + unset($fields['prvkey']); + $fields['self'] = false; + self::update($fields, ['uri-id' => $self['uri-id'], 'self' => false]); // Update the profile $fields = [ @@ -801,18 +799,18 @@ class Contact public static function remove($id) { // We want just to make sure that we don't delete our "self" contact - $contact = DBA::selectFirst('contact', ['uri-id', 'photo', 'thumb', 'micro'], ['id' => $id, 'self' => false]); + $contact = DBA::selectFirst('contact', ['uri-id', 'photo', 'thumb', 'micro', 'uid'], ['id' => $id, 'self' => false]); if (!DBA::isResult($contact)) { return; } + self::clearFollowerFollowingEndpointCache($contact['uid']); + // Archive the contact self::update(['archive' => true, 'network' => Protocol::PHANTOM, 'deleted' => true], ['id' => $id]); if (!DBA::exists('contact', ['uri-id' => $contact['uri-id'], 'deleted' => false])) { - self::deleteAvatarCache($contact['photo']); - self::deleteAvatarCache($contact['thumb']); - self::deleteAvatarCache($contact['micro']); + Avatar::deleteCache($contact); } // Delete it in the background @@ -905,6 +903,16 @@ class Contact self::remove($contact['id']); } + private static function clearFollowerFollowingEndpointCache(int $uid) + { + if (empty($uid)) { + return; + } + + DI::cache()->delete(ActivityPub\Transmitter::CACHEKEY_CONTACTS . 'followers:' . $uid); + DI::cache()->delete(ActivityPub\Transmitter::CACHEKEY_CONTACTS . 'following:' . $uid); + DI::cache()->delete(NoScrape::CACHEKEY . $uid); + } /** * Marks a contact for archival after a communication issue delay @@ -1581,16 +1589,24 @@ class Contact return; } + if (Network::isLocalLink($contact['url'])) { + return; + } + if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) || DI::config()->get('system', 'cache_contact_avatar')) { if (!empty($contact['avatar']) && (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']))) { Logger::info('Adding avatar cache', ['id' => $cid, 'contact' => $contact]); self::updateAvatar($cid, $contact['avatar'], true); return; } - } elseif (!self::getAvatarFile($contact['photo']) || !self::getAvatarFile($contact['thumb']) || !self::getAvatarFile($contact['micro'])) { - Logger::info('Removing/replacing avatar cache', ['id' => $cid, 'contact' => $contact]); + } elseif (Photo::isPhotoURI($contact['photo']) || Photo::isPhotoURI($contact['thumb']) || Photo::isPhotoURI($contact['micro'])) { + Logger::info('Replacing legacy avatar cache', ['id' => $cid, 'contact' => $contact]); self::updateAvatar($cid, $contact['avatar'], true); return; + } elseif (DI::config()->get('system', 'avatar_cache') && (empty($contact['photo']) || empty($contact['thumb']) || empty($contact['micro']))) { + Logger::info('Adding avatar cache file', ['id' => $cid, 'contact' => $contact]); + self::updateAvatar($cid, $contact['avatar'], true); + return; } } @@ -1608,20 +1624,20 @@ class Contact { $contact = self::checkAvatarCacheByArray($contact, $no_update); - if (!DI::config()->get('system', 'avatar_cache')) { + if (DI::config()->get('system', 'avatar_cache')) { switch ($size) { case Proxy::SIZE_MICRO: - if (self::getAvatarFile($contact['micro'])) { + if (!empty($contact['micro']) && !Photo::isPhotoURI($contact['micro'])) { return $contact['micro']; } break; case Proxy::SIZE_THUMB: - if (self::getAvatarFile($contact['thumb'])) { + if (!empty($contact['thumb']) && !Photo::isPhotoURI($contact['thumb'])) { return $contact['thumb']; } break; case Proxy::SIZE_SMALL: - if (self::getAvatarFile($contact['photo'])) { + if (!empty($contact['photo']) && !Photo::isPhotoURI($contact['photo'])) { return $contact['photo']; } break; @@ -1692,7 +1708,9 @@ class Contact return $contact; } - if (!empty($contact['id']) && !empty($contact['avatar'])) { + $local = !empty($contact['url']) && Network::isLocalLink($contact['url']); + + if (!$local && !empty($contact['id']) && !empty($contact['avatar'])) { self::updateAvatar($contact['id'], $contact['avatar'], true); $new_contact = self::getById($contact['id'], $contact_fields); @@ -1700,6 +1718,8 @@ class Contact // We only update the cache fields $contact = array_merge($contact, $new_contact); } + } elseif ($local && !empty($contact['avatar'])) { + return $contact; } /// add the default avatars if the fields aren't filled @@ -1758,7 +1778,7 @@ class Contact break; default: /** - * Use a random picture. + * Use a random picture. * The service provides random pictures from Unsplash. * @license https://unsplash.com/license */ @@ -1797,6 +1817,114 @@ class Contact } if (!DI::config()->get('system', 'remote_avatar_lookup')) { + $platform = ''; + $type = Contact::TYPE_PERSON; + + if (!empty($contact['id'])) { + $account = DBA::selectFirst('account-user-view', ['platform', 'contact-type'], ['id' => $contact['id']]); + $platform = $account['platform'] ?? ''; + $type = $account['contact-type'] ?? Contact::TYPE_PERSON; + } + + if (empty($platform) && !empty($contact['uri-id'])) { + $account = DBA::selectFirst('account-user-view', ['platform', 'contact-type'], ['uri-id' => $contact['uri-id']]); + $platform = $account['platform'] ?? ''; + $type = $account['contact-type'] ?? Contact::TYPE_PERSON; + } + + switch ($platform) { + case 'corgidon': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/msdos621/corgidon/blob/main/public/avatars/original/missing.png + */ + $default = '/images/default/corgidon.png'; + break; + + case 'diaspora': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/diaspora/diaspora/ + */ + $default = '/images/default/diaspora.png'; + break; + + case 'gotosocial': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/superseriousbusiness/gotosocial/blob/main/web/assets/default_avatars/GoToSocial_icon1.svg + */ + $default = '/images/default/gotosocial.svg'; + break; + + case 'hometown': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/hometown-fork/hometown/blob/hometown-dev/public/avatars/original/missing.png + */ + $default = '/images/default/hometown.png'; + break; + + case 'koyuspace': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/koyuspace/mastodon/blob/main/public/avatars/original/missing.png + */ + $default = '/images/default/koyuspace.png'; + break; + + case 'ecko': + case 'qoto': + case 'mastodon': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/mastodon/mastodon/tree/main/public/avatars/original/missing.png + */ + $default = '/images/default/mastodon.png'; + break; + + case 'peertube': + if ($type == Contact::TYPE_COMMUNITY) { + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/Chocobozzz/PeerTube/blob/develop/client/src/assets/images/default-avatar-video-channel.png + */ + $default = '/images/default/peertube-channel.png'; + } else { + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/Chocobozzz/PeerTube/blob/develop/client/src/assets/images/default-avatar-account.png + */ + $default = '/images/default/peertube-account.png'; + } + break; + + case 'pleroma': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://git.pleroma.social/pleroma/pleroma/-/blob/develop/priv/static/images/avi.png + */ + $default = '/images/default/pleroma.png'; + break; + + case 'plume': + /** + * Picture credits + * @license GNU Affero General Public License v3.0 + * @link https://github.com/Plume-org/Plume/blob/main/assets/images/default-avatar.png + */ + $default = '/images/default/plume.png'; + break; + } return DI::baseUrl() . $default; } @@ -1977,9 +2105,7 @@ class Contact } if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) || $cache_avatar) { - self::deleteAvatarCache($contact['photo']); - self::deleteAvatarCache($contact['thumb']); - self::deleteAvatarCache($contact['micro']); + Avatar::deleteCache($contact); if ($default_avatar && Proxy::isLocalImage($avatar)) { $fields = ['avatar' => $avatar, 'avatar-date' => DateTimeFormat::utcNow(), @@ -2032,7 +2158,7 @@ class Contact } } else { Photo::delete(['uid' => $uid, 'contact-id' => $cid, 'photo-type' => Photo::CONTACT_AVATAR]); - $fields = self::fetchAvatarContact($contact, $avatar); + $fields = Avatar::fetchAvatarContact($contact, $avatar, $force); $update = ($avatar . $fields['photo'] . $fields['thumb'] . $fields['micro'] != $contact['avatar'] . $contact['photo'] . $contact['thumb'] . $contact['micro']) || $force; } @@ -2064,138 +2190,6 @@ class Contact self::update($fields, ['id' => $cids]); } - /** - * Returns a field array with locally cached avatar pictures - * - * @param array $contact - * @param string $avatar - * @return array - */ - private static function fetchAvatarContact(array $contact, string $avatar): array - { - $fields = ['avatar' => $avatar, 'avatar-date' => DateTimeFormat::utcNow(), 'photo' => '', 'thumb' => '', 'micro' => '']; - - if (!DI::config()->get('system', 'avatar_cache')) { - self::deleteAvatarCache($contact['photo']); - self::deleteAvatarCache($contact['thumb']); - self::deleteAvatarCache($contact['micro']); - return $fields; - } - - if (Network::isLocalLink($avatar)) { - return $fields; - } - - if ($avatar != $contact['avatar']) { - self::deleteAvatarCache($contact['photo']); - self::deleteAvatarCache($contact['thumb']); - self::deleteAvatarCache($contact['micro']); - Logger::debug('Avatar file name changed', ['new' => $avatar, 'old' => $contact['avatar']]); - } elseif (self::getAvatarFile($contact['photo']) && self::getAvatarFile($contact['thumb']) && self::getAvatarFile($contact['micro'])) { - $fields['photo'] = $contact['photo']; - $fields['thumb'] = $contact['thumb']; - $fields['micro'] = $contact['micro']; - Logger::debug('Using existing cache files', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); - return $fields; - } - - $guid = Item::guidFromUri($contact['url'], parse_url($contact['url'], PHP_URL_HOST)); - - $filename = substr($guid, 0, 2) . '/' . substr($guid, 3, 2) . '/' . substr($guid, 5, 3) . '/' . - substr($guid, 9, 2) .'/' . substr($guid, 11, 2) . '/' . substr($guid, 13, 4). '/' . substr($guid, 18) . '-'; - - $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]); - $img_str = $fetchResult->getBody(); - if (empty($img_str)) { - Logger::debug('Avatar is invalid', ['avatar' => $avatar]); - return $fields; - } - - $image = new Image($img_str, Images::getMimeTypeByData($img_str)); - if (!$image->isValid()) { - Logger::debug('Avatar picture is invalid', ['avatar' => $avatar]); - return $fields; - } - - $fields['photo'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_SMALL); - $fields['thumb'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_THUMB); - $fields['micro'] = self::storeAvatarCache($image, $filename, Proxy::PIXEL_MICRO); - - Logger::debug('Storing new avatar cache', ['uri-id' => $contact['uri-id'], 'fields' => $fields]); - - return $fields; - } - - private static function storeAvatarCache(Image $image, string $filename, int $size): string - { - $image->scaleDown($size); - if (is_null($image) || !$image->isValid()) { - return ''; - } - - $path = '/avatar/' . $filename . $size . '.' . $image->getExt(); - - $filepath = DI::basePath() . $path; - - $dirpath = dirname($filepath); - - DI::profiler()->startRecording('file'); - - if (!file_exists($dirpath)) { - mkdir($dirpath, 0777, true); - } - - file_put_contents($filepath, $image->asString()); - DI::profiler()->stopRecording(); - - return DI::baseUrl() . $path; - } - - /** - * Fetch the name of locally cached avatar pictures - * - * @param string $avatar - * @return string - */ - private static function getAvatarFile(string $avatar): string - { - if (empty($avatar) || !Network::isLocalLink($avatar)) { - return ''; - } - - $path = Strings::normaliseLink(DI::baseUrl() . '/avatar'); - - if (Network::getUrlMatch($path, $avatar) != $path) { - return ''; - } - - $filename = str_replace($path, DI::basePath(). '/avatar/', Strings::normaliseLink($avatar)); - - DI::profiler()->startRecording('file'); - $exists = file_exists($filename); - DI::profiler()->stopRecording(); - - if (!$exists) { - return ''; - } - return $filename; - } - - /** - * Delete a locally cached avatar picture - * - * @param string $avatar - * @return void - */ - public static function deleteAvatarCache(string $avatar) - { - $localFile = self::getAvatarFile($avatar); - if (!empty($localFile)) { - unlink($localFile); - Logger::debug('Unlink avatar', ['avatar' => $avatar]); - } - } - public static function deleteContactByUrl(string $url) { // Update contact data for all users @@ -2444,9 +2438,12 @@ class Contact if ($uid == 0) { if ($ret['network'] == Protocol::ACTIVITYPUB) { - ActivityPub\Processor::fetchFeaturedPosts($ret['url']); + $apcontact = APContact::getByURL($ret['url'], false); + if (!empty($apcontact['featured'])) { + Worker::add(PRIORITY_LOW, 'FetchFeaturedPosts', $ret['url']); + } } - + $ret['last-item'] = Probe::getLastUpdate($ret); Logger::info('Fetched last item', ['id' => $id, 'probed_url' => $ret['url'], 'last-item' => $ret['last-item'], 'callstack' => System::callstack(20)]); } @@ -2654,6 +2651,11 @@ class Contact } else { $probed = true; $ret = Probe::uri($url, $network, $uid); + + // Ensure that the public contact exists + if ($ret['network'] != Protocol::PHANTOM) { + self::getIdForURL($url); + } } if (($network != '') && ($ret['network'] != $network)) { @@ -2832,6 +2834,8 @@ class Contact $contact = DBA::selectFirst('contact', [], ['id' => $cid]); } + self::clearFollowerFollowingEndpointCache($importer['uid']); + if (!empty($contact)) { if (!empty($contact['pending'])) { Logger::info('Pending contact request already exists.', ['url' => $url, 'uid' => $importer['uid']]); @@ -2955,6 +2959,8 @@ class Contact return; } + self::clearFollowerFollowingEndpointCache($contact['uid']); + $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]); @@ -2969,6 +2975,8 @@ class Contact */ public static function removeSharer(array $contact) { + self::clearFollowerFollowingEndpointCache($contact['uid']); + if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { self::remove($contact['id']); } else {