X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=src%2FModel%2FContact.php;h=f8ff91ec554112f4a4a19559f16b4e94ca8369b1;hb=f12cc9648ecf2bdd0e468e29be4164a844b96a46;hp=9d609786e96a1880ec0c63d0ab31f29fdc587be6;hpb=2b0c2a57a7170ce1a7d09441f94ff79fbdc24fd0;p=friendica.git diff --git a/src/Model/Contact.php b/src/Model/Contact.php index 9d609786e9..f8ff91ec55 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,13 +35,11 @@ use Friendica\Core\Worker; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; +use Friendica\Module\NoScrape; use Friendica\Network\HTTPException; use Friendica\Network\Probe; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; -use Friendica\Protocol\Diaspora; -use Friendica\Protocol\OStatus; -use Friendica\Protocol\Salmon; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; use Friendica\Util\Network; @@ -121,7 +119,7 @@ class Contact * @return array * @throws \Exception */ - public static function selectToArray(array $fields = [], array $condition = [], array $params = []) + public static function selectToArray(array $fields = [], array $condition = [], array $params = []): array { return DBA::selectToArray('contact', $fields, $condition, $params); } @@ -130,7 +128,7 @@ class Contact * @param array $fields Array of selected fields, empty for all * @param array $condition Array of fields for condition * @param array $params Array of several parameters - * @return array + * @return array|bool * @throws \Exception */ public static function selectFirst(array $fields = [], array $condition = [], array $params = []) @@ -150,7 +148,7 @@ class Contact * @return int id of the created contact * @throws \Exception */ - public static function insert(array $fields, int $duplicate_mode = Database::INSERT_DEFAULT) + public static function insert(array $fields, int $duplicate_mode = Database::INSERT_DEFAULT): int { if (!empty($fields['baseurl']) && empty($fields['gsid'])) { $fields['gsid'] = GServer::getID($fields['baseurl'], true); @@ -189,6 +187,7 @@ class Contact * * @return boolean was the update successfull? * @throws \Exception + * @todo Let's get rid of boolean type of $old_fields */ public static function update(array $fields, array $condition, $old_fields = []) { @@ -206,7 +205,7 @@ class Contact * @return array|boolean Contact record if it exists, false otherwise * @throws \Exception */ - public static function getById($id, $fields = []) + public static function getById(int $id, array $fields = []) { return DBA::selectFirst('contact', $fields, ['id' => $id]); } @@ -219,7 +218,7 @@ class Contact * @return array|boolean Contact record if it exists, false otherwise * @throws \Exception */ - public static function getByUriId($uri_id, $fields = []) + public static function getByUriId(int $uri_id, array $fields = []) { return DBA::selectFirst('contact', $fields, ['uri-id' => $uri_id], ['order' => ['uid']]); } @@ -233,7 +232,7 @@ class Contact * @param integer $uid User ID of the contact * @return array contact array */ - public static function getByURL(string $url, $update = null, array $fields = [], int $uid = 0) + public static function getByURL(string $url, $update = null, array $fields = [], int $uid = 0): array { if ($update || is_null($update)) { $cid = self::getIdForURL($url, $uid, $update); @@ -304,7 +303,7 @@ class Contact * @param array $fields Field list * @return array contact array */ - public static function getByURLForUser(string $url, int $uid = 0, $update = false, array $fields = []) + public static function getByURLForUser(string $url, int $uid = 0, $update = false, array $fields = []): array { if ($uid != 0) { $contact = self::getByURL($url, $update, $fields, $uid); @@ -335,7 +334,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isFollower($cid, $uid) + public static function isFollower(int $cid, int $uid): bool { if (Contact\User::isBlocked($cid, $uid)) { return false; @@ -360,7 +359,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isFollowerByURL($url, $uid) + public static function isFollowerByURL(string $url, uid $uid): bool { $cid = self::getIdForURL($url, $uid); @@ -372,16 +371,16 @@ class Contact } /** - * Tests if the given user follow the given contact + * Tests if the given user shares with the given contact * * @param int $cid Either public contact id or user's contact id * @param int $uid User ID * - * @return boolean is the contact url being followed? + * @return boolean is the contact sharing with given user? * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isSharing($cid, $uid) + public static function isSharing(int $cid, int $uid): bool { if (Contact\User::isBlocked($cid, $uid)) { return false; @@ -406,7 +405,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function isSharingByURL($url, $uid) + public static function isSharingByURL(string $url, int $uid): bool { $cid = self::getIdForURL($url, $uid); @@ -427,7 +426,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function getBasepath($url, $dont_update = false) + public static function getBasepath(string $url, bool $dont_update = false): string { $contact = DBA::selectFirst('contact', ['id', 'baseurl'], ['uid' => 0, 'nurl' => Strings::normaliseLink($url)]); if (!DBA::isResult($contact)) { @@ -461,7 +460,7 @@ class Contact * * @return boolean Is it the same server? */ - public static function isLocal($url) + public static function isLocal(string $url): bool { if (!parse_url($url, PHP_URL_SCHEME)) { $addr_parts = explode('@', $url); @@ -478,7 +477,7 @@ class Contact * * @return boolean Is it the same server? */ - public static function isLocalById(int $cid) + public static function isLocalById(int $cid): bool { $contact = DBA::selectFirst('contact', ['url', 'baseurl'], ['id' => $cid]); if (!DBA::isResult($contact)) { @@ -502,7 +501,7 @@ class Contact * @return integer|boolean Public contact id for given user id * @throws \Exception */ - public static function getPublicIdByUserId($uid) + public static function getPublicIdByUserId(int $uid) { $self = DBA::selectFirst('contact', ['url'], ['self' => true, 'uid' => $uid]); if (!DBA::isResult($self)) { @@ -521,7 +520,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function getPublicAndUserContactID($cid, $uid) + public static function getPublicAndUserContactID(int $cid, int $uid): array { // We have to use the legacy function as long as the post update hasn't finished if (DI::config()->get('system', 'post_update_version') < 1427) { @@ -562,7 +561,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - private static function legacyGetPublicAndUserContactID($cid, $uid) + private static function legacyGetPublicAndUserContactID(int $cid, int $uid): array { if (empty($uid) || empty($cid)) { return []; @@ -685,7 +684,7 @@ class Contact */ public static function updateSelfFromUserID($uid, $update_avatar = false) { - $fields = ['id', 'name', 'nick', 'location', 'about', 'keywords', 'avatar', 'prvkey', 'pubkey', + $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]); @@ -717,6 +716,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']; @@ -757,6 +757,7 @@ class Contact $fields['forum'] = $user['page-flags'] == User::PAGE_FLAGS_COMMUNITY; $fields['prv'] = $user['page-flags'] == User::PAGE_FLAGS_PRVGROUP; $fields['unsearchable'] = !$profile['net-publish']; + $fields['manually-approve'] = in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL, User::PAGE_FLAGS_PRVGROUP]); $update = false; @@ -773,10 +774,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 = [ @@ -799,50 +800,61 @@ 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', ['uid'], ['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])) { + Avatar::deleteCache($contact); + } + // Delete it in the background Worker::add(PRIORITY_MEDIUM, 'Contact\Remove', $id); } /** - * Sends an unfriend message. Removes the contact for two-way unfriending or sharing only protocols (feed an mail) + * Unfollow the remote contact * - * @param array $user User unfriending - * @param array $contact Contact (uid != 0) unfriended - * @param boolean $two_way Revoke eventual inbound follow as well - * @return bool|null true if successful, false if not, null if no remote action was performed + * @param array $contact Target user-specific contact (uid != 0) array * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function terminateFriendship(array $user, array $contact): ?bool + public static function unfollow(array $contact): void { - $result = Protocol::terminateFriendship($user, $contact); + if (empty($contact['network'])) { + throw new \InvalidArgumentException('Empty network in contact array'); + } - if ($contact['rel'] == Contact::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { - self::remove($contact['id']); - } else { - self::update(['rel' => Contact::FOLLOWER], ['id' => $contact['id']]); + if (empty($contact['uid'])) { + throw new \InvalidArgumentException('Unexpected public contact record'); } - return $result; + if (in_array($contact['rel'], [self::SHARING, self::FRIEND])) { + $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); + if (!empty($cdata['public'])) { + Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); + } + } + + self::removeSharer($contact); } /** * Revoke follow privileges of the remote user contact * - * @param array $contact Contact unfriended - * @return bool|null Whether the remote operation is successful or null if no remote operation was performed + * The local relationship is updated immediately, the eventual remote server is messaged in the background. + * + * @param array $contact User-specific contact array (uid != 0) to revoke the follow from * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function revokeFollow(array $contact): ?bool + public static function revokeFollow(array $contact): void { if (empty($contact['network'])) { throw new \InvalidArgumentException('Empty network in contact array'); @@ -852,21 +864,56 @@ class Contact throw new \InvalidArgumentException('Unexpected public contact record'); } - $result = Protocol::revokeFollow($contact); - - // A null value here means the remote network doesn't support explicit follow revocation, we can still - // break the locally recorded relationship - if ($result !== false) { - if ($contact['rel'] == self::FRIEND) { - self::update(['rel' => self::SHARING], ['id' => $contact['id']]); - } else { - self::remove($contact['id']); + if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND])) { + $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); + if (!empty($cdata['public'])) { + Worker::add(PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']); } } - return $result; + self::removeFollower($contact); } + /** + * Completely severs a relationship with a contact + * + * @param array $contact User-specific contact (uid != 0) array + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ + public static function terminateFriendship(array $contact) + { + if (empty($contact['network'])) { + throw new \InvalidArgumentException('Empty network in contact array'); + } + + if (empty($contact['uid'])) { + throw new \InvalidArgumentException('Unexpected public contact record'); + } + + $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); + + if (in_array($contact['rel'], [self::SHARING, self::FRIEND]) && !empty($cdata['public'])) { + Worker::add(PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']); + } + + if (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) && !empty($cdata['public'])) { + Worker::add(PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']); + } + + 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 @@ -1432,11 +1479,31 @@ class Contact } if ($thread_mode) { - $items = Post::toArray(Post::selectForUser(local_user(), ['uri-id', 'gravity', 'parent-uri-id', 'thr-parent-id', 'author-id'], $condition, $params)); + $fields = ['uri-id', 'thr-parent-id', 'gravity', 'author-id', 'commented']; + $items = Post::toArray(Post::selectForUser(local_user(), $fields, $condition, $params)); + + if ($pager->getStart() == 0) { + $cdata = self::getPublicAndUserContactID($cid, local_user()); + if (!empty($cdata['public'])) { + $pinned = Post\Collection::selectToArrayForContact($cdata['public'], Post\Collection::FEATURED, $fields); + $items = array_merge($items, $pinned); + } + } - $o .= DI::conversation()->create($items, 'contacts', $update, false, 'commented', local_user()); + $o .= DI::conversation()->create($items, 'contacts', $update, false, 'pinned_commented', local_user()); } else { - $items = Post::toArray(Post::selectForUser(local_user(), Item::DISPLAY_FIELDLIST, $condition, $params)); + $fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']); + $items = Post::toArray(Post::selectForUser(local_user(), $fields, $condition, $params)); + + if ($pager->getStart() == 0) { + $cdata = self::getPublicAndUserContactID($cid, local_user()); + if (!empty($cdata['public'])) { + $condition = ["`uri-id` IN (SELECT `uri-id` FROM `collection-view` WHERE `cid` = ? AND `type` = ?)", + $cdata['public'], Post\Collection::FEATURED]; + $pinned = Post::toArray(Post::selectForUser(local_user(), $fields, $condition, $params)); + $items = array_merge($pinned, $items); + } + } $o .= DI::conversation()->create($items, 'contact-posts', $update); } @@ -1457,34 +1524,11 @@ class Contact * * The function can be called with either the user or the contact array * - * @param array $contact contact or user array + * @param int $type type of contact or account * @return string */ - public static function getAccountType(array $contact) - { - // There are several fields that indicate that the contact or user is a forum - // "page-flags" is a field in the user table, - // "forum" and "prv" are used in the contact table. They stand for User::PAGE_FLAGS_COMMUNITY and User::PAGE_FLAGS_PRVGROUP. - if ((isset($contact['page-flags']) && (intval($contact['page-flags']) == User::PAGE_FLAGS_COMMUNITY)) - || (isset($contact['page-flags']) && (intval($contact['page-flags']) == User::PAGE_FLAGS_PRVGROUP)) - || (isset($contact['forum']) && intval($contact['forum'])) - || (isset($contact['prv']) && intval($contact['prv'])) - || (isset($contact['community']) && intval($contact['community'])) - ) { - $type = self::TYPE_COMMUNITY; - } else { - $type = self::TYPE_PERSON; - } - - // The "contact-type" (contact table) and "account-type" (user table) are more general then the chaos from above. - if (isset($contact["contact-type"])) { - $type = $contact["contact-type"]; - } - - if (isset($contact["account-type"])) { - $type = $contact["account-type"]; - } - + public static function getAccountType(int $type): string + { switch ($type) { case self::TYPE_ORGANISATION: $account_type = DI::l10n()->t("Organisation"); @@ -1509,11 +1553,11 @@ class Contact /** * Blocks a contact * - * @param int $cid - * @return bool - * @throws \Exception + * @param int $cid Contact id to block + * @param string $reason Block reason + * @return bool Whether it was successful */ - public static function block($cid, $reason = null) + public static function block(int $cid, string $reason = null): bool { $return = self::update(['blocked' => true, 'block_reason' => $reason], ['id' => $cid]); @@ -1523,11 +1567,10 @@ class Contact /** * Unblocks a contact * - * @param int $cid - * @return bool - * @throws \Exception + * @param int $cid Contact id to unblock + * @return bool Whether it was successfull */ - public static function unblock($cid) + public static function unblock(int $cid): bool { $return = self::update(['blocked' => false, 'block_reason' => null], ['id' => $cid]); @@ -1537,7 +1580,7 @@ class Contact /** * Ensure that cached avatar exist * - * @param integer $cid + * @param integer $cid Contact id */ public static function checkAvatarCache(int $cid) { @@ -1546,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 (!empty($contact['photo']) || !empty($contact['thumb']) || !empty($contact['micro'])) { - Logger::info('Removing 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; } } @@ -1569,9 +1620,30 @@ class Contact * @param bool $no_update Don't perfom an update if no cached avatar was found * @return string photo path */ - private static function getAvatarPath(array $contact, string $size, $no_update = false) + private static function getAvatarPath(array $contact, string $size, bool $no_update = false): string { $contact = self::checkAvatarCacheByArray($contact, $no_update); + + if (DI::config()->get('system', 'avatar_cache')) { + switch ($size) { + case Proxy::SIZE_MICRO: + if (!empty($contact['micro']) && !Photo::isPhotoURI($contact['micro'])) { + return $contact['micro']; + } + break; + case Proxy::SIZE_THUMB: + if (!empty($contact['thumb']) && !Photo::isPhotoURI($contact['thumb'])) { + return $contact['thumb']; + } + break; + case Proxy::SIZE_SMALL: + if (!empty($contact['photo']) && !Photo::isPhotoURI($contact['photo'])) { + return $contact['photo']; + } + break; + } + } + return self::getAvatarUrlForId($contact['id'], $size, $contact['updated'] ?? ''); } @@ -1582,7 +1654,7 @@ class Contact * @param bool $no_update Don't perfom an update if no cached avatar was found * @return string photo path */ - public static function getPhoto(array $contact, bool $no_update = false) + public static function getPhoto(array $contact, bool $no_update = false): string { return self::getAvatarPath($contact, Proxy::SIZE_SMALL, $no_update); } @@ -1594,7 +1666,7 @@ class Contact * @param bool $no_update Don't perfom an update if no cached avatar was found * @return string photo path */ - public static function getThumb(array $contact, bool $no_update = false) + public static function getThumb(array $contact, bool $no_update = false): string { return self::getAvatarPath($contact, Proxy::SIZE_THUMB, $no_update); } @@ -1606,7 +1678,7 @@ class Contact * @param bool $no_update Don't perfom an update if no cached avatar was found * @return string photo path */ - public static function getMicro(array $contact, bool $no_update = false) + public static function getMicro(array $contact, bool $no_update = false): string { return self::getAvatarPath($contact, Proxy::SIZE_MICRO, $no_update); } @@ -1618,7 +1690,7 @@ class Contact * @param bool $no_update Don't perfom an update if no cached avatar was found * @return array contact array with avatar cache fields */ - private static function checkAvatarCacheByArray(array $contact, bool $no_update = false) + private static function checkAvatarCacheByArray(array $contact, bool $no_update = false): array { $update = false; $contact_fields = []; @@ -1636,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); @@ -1644,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 @@ -1702,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 */ @@ -1720,7 +1796,7 @@ class Contact * @param string $size Size of the avatar picture * @return string avatar URL */ - public static function getDefaultAvatar(array $contact, string $size) + public static function getDefaultAvatar(array $contact, string $size): string { switch ($size) { case Proxy::SIZE_MICRO: @@ -1741,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; } @@ -1778,7 +1962,7 @@ class Contact { // We have to fetch the "updated" variable when it wasn't provided // The parameter can be provided to improve performance - if (empty($updated) || empty($guid)) { + if (empty($updated)) { $account = DBA::selectFirst('account-user-view', ['updated', 'guid'], ['id' => $cid]); $updated = $account['updated'] ?? ''; $guid = $account['guid'] ?? ''; @@ -1880,7 +2064,7 @@ class Contact */ public static function updateAvatar(int $cid, string $avatar, bool $force = false, bool $create_cache = false) { - $contact = DBA::selectFirst('contact', ['uid', 'avatar', 'photo', 'thumb', 'micro', 'xmpp', 'addr', 'nurl', 'url', 'network'], + $contact = DBA::selectFirst('contact', ['uid', 'avatar', 'photo', 'thumb', 'micro', 'xmpp', 'addr', 'nurl', 'url', 'network', 'uri-id'], ['id' => $cid, 'self' => false]); if (!DBA::isResult($contact)) { return; @@ -1913,7 +2097,16 @@ class Contact $avatar = self::getDefaultAvatar($contact, Proxy::SIZE_SMALL); } - if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) || DI::config()->get('system', 'cache_contact_avatar')) { + $cache_avatar = DI::config()->get('system', 'cache_contact_avatar'); + + // Local contact avatars don't need to be cached + if ($cache_avatar && Network::isLocalLink($contact['url'])) { + $cache_avatar = !DBA::exists('contact', ['nurl' => $contact['nurl'], 'self' => true]); + } + + if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) || $cache_avatar) { + Avatar::deleteCache($contact); + if ($default_avatar && Proxy::isLocalImage($avatar)) { $fields = ['avatar' => $avatar, 'avatar-date' => DateTimeFormat::utcNow(), 'photo' => $avatar, @@ -1965,9 +2158,8 @@ class Contact } } else { Photo::delete(['uid' => $uid, 'contact-id' => $cid, 'photo-type' => Photo::CONTACT_AVATAR]); - $fields = ['avatar' => $avatar, 'avatar-date' => DateTimeFormat::utcNow(), - 'photo' => '', 'thumb' => '', 'micro' => '']; - $update = ($avatar != $contact['avatar'] . $contact['photo'] . $contact['thumb'] . $contact['micro']) || $force; + $fields = Avatar::fetchAvatarContact($contact, $avatar, $force); + $update = ($avatar . $fields['photo'] . $fields['thumb'] . $fields['micro'] != $contact['avatar'] . $contact['photo'] . $contact['thumb'] . $contact['micro']) || $force; } if (!$update) { @@ -2245,6 +2437,13 @@ class Contact $new_pubkey = $ret['pubkey'] ?? ''; if ($uid == 0) { + if ($ret['network'] == Protocol::ACTIVITYPUB) { + $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)]); } @@ -2358,7 +2557,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function updateFromProbeByURL($url) + public static function updateFromProbeByURL(string $url): int { $id = self::getIdForURL($url); @@ -2379,7 +2578,7 @@ class Contact * @param string $network Network of that contact * @return string with protocol */ - public static function getProtocol($url, $network) + public static function getProtocol(string $url, string $network): string { if ($network != Protocol::DFRN) { return $network; @@ -2415,7 +2614,7 @@ class Contact * @throws HTTPException\NotFoundException * @throws \ImagickException */ - public static function createFromProbeForUser(int $uid, $url, $network = '') + public static function createFromProbeForUser(int $uid, string $url, string $network = ''): array { $result = ['cid' => -1, 'success' => false, 'message' => '']; @@ -2452,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)) { @@ -2589,28 +2793,6 @@ class Contact return $result; } - /** - * Unfollow a contact - * - * @param int $cid Public contact id - * @param int $uid User ID - * - * @return bool "true" if unfollowing had been successful - */ - public static function unfollow(int $cid, int $uid) - { - $cdata = self::getPublicAndUserContactID($cid, $uid); - if (empty($cdata['user'])) { - return false; - } - - $contact = self::getById($cdata['user']); - - self::removeSharer([], $contact); - - return true; - } - /** * @param array $importer Owner (local user) data * @param array $contact Existing owner-specific contact data we want to expand the relationship with. Optional. @@ -2621,14 +2803,14 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function addRelationship(array $importer, array $contact, array $datarray, $sharing = false, $note = '') + public static function addRelationship(array $importer, array $contact, array $datarray, bool $sharing = false, string $note = '') { // Should always be set if (empty($datarray['author-id'])) { return false; } - $fields = ['url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked']; + $fields = ['id', 'url', 'name', 'nick', 'avatar', 'photo', 'network', 'blocked']; $pub_contact = DBA::selectFirst('contact', $fields, ['id' => $datarray['author-id']]); if (!DBA::isResult($pub_contact)) { // Should never happen @@ -2652,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']]); @@ -2676,7 +2860,7 @@ class Contact // Ensure to always have the correct network type, independent from the connection request method self::updateFromProbe($contact['id']); - Post\UserNotification::insertNotification($contact['id'], Activity::FOLLOW, $importer['uid']); + Post\UserNotification::insertNotification($pub_contact['id'], Activity::FOLLOW, $importer['uid']); return true; } else { @@ -2707,7 +2891,7 @@ class Contact self::updateAvatar($contact_id, $photo, true); - Post\UserNotification::insertNotification($contact_id, Activity::FOLLOW, $importer['uid']); + Post\UserNotification::insertNotification($pub_contact['id'], Activity::FOLLOW, $importer['uid']); $contact_record = DBA::selectFirst('contact', ['id', 'network', 'name', 'url', 'photo'], ['id' => $contact_id]); @@ -2727,9 +2911,7 @@ class Contact Group::addMember(User::getDefaultGroup($importer['uid']), $contact_record['id']); - if (($user['notify-flags'] & Notification\Type::INTRO) && - in_array($user['page-flags'], [User::PAGE_FLAGS_NORMAL])) { - + if (($user['notify-flags'] & Notification\Type::INTRO) && $user['page-flags'] == User::PAGE_FLAGS_NORMAL) { DI::notify()->createFromArray([ 'type' => Notification\Type::INTRO, 'otype' => Notification\ObjectType::INTRO, @@ -2759,23 +2941,47 @@ class Contact return null; } + /** + * Update the local relationship when a local user loses a follower + * + * @param array $contact User-specific contact (uid != 0) array + * @return void + * @throws HTTPException\InternalServerErrorException + * @throws \ImagickException + */ public static function removeFollower(array $contact) { if (in_array($contact['rel'] ?? [], [self::FRIEND, self::SHARING])) { - DBA::update('contact', ['rel' => self::SHARING], ['id' => $contact['id']]); + self::update(['rel' => self::SHARING], ['id' => $contact['id']]); } elseif (!empty($contact['id'])) { self::remove($contact['id']); } else { DI::logger()->info('Couldn\'t remove follower because of invalid contact array', ['contact' => $contact, 'callstack' => System::callstack()]); + return; } + + self::clearFollowerFollowingEndpointCache($contact['uid']); + + $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']); + + DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]); } - public static function removeSharer($importer, $contact) + /** + * Update the local relationship when a local user unfollow a contact. + * Removes the contact for sharing-only protocols (feed and mail). + * + * @param array $contact User-specific contact (uid != 0) array + * @throws HTTPException\InternalServerErrorException + */ + public static function removeSharer(array $contact) { - if (($contact['rel'] == self::FRIEND) || ($contact['rel'] == self::FOLLOWER)) { - self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]); - } else { + self::clearFollowerFollowingEndpointCache($contact['uid']); + + if ($contact['rel'] == self::SHARING || in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) { self::remove($contact['id']); + } else { + self::update(['rel' => self::FOLLOWER], ['id' => $contact['id']]); } } @@ -2825,7 +3031,7 @@ class Contact * @return array * @throws \Exception */ - public static function pruneUnavailable(array $contact_ids) + public static function pruneUnavailable(array $contact_ids): array { if (empty($contact_ids)) { return []; @@ -2853,7 +3059,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function magicLink($contact_url, $url = '') + public static function magicLink(string $contact_url, string $url = ''): string { if (!Session::isAuthenticated()) { return $url ?: $contact_url; // Equivalent to: ($url != '') ? $url : $contact_url; @@ -2880,7 +3086,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function magicLinkById($cid, $url = '') + public static function magicLinkById(int $cid, string $url = ''): string { $contact = DBA::selectFirst('contact', ['id', 'network', 'url', 'uid'], ['id' => $cid]); @@ -2897,7 +3103,7 @@ class Contact * @throws HTTPException\InternalServerErrorException * @throws \ImagickException */ - public static function magicLinkByContact($contact, $url = '') + public static function magicLinkByContact(array $contact, string $url = ''): string { $destination = $url ?: $contact['url']; // Equivalent to ($url != '') ? $url : $contact['url']; @@ -2938,9 +3144,9 @@ class Contact * * @return boolean "true" if it is a forum */ - public static function isForum($contactid) + public static function isForum(int $contactid): bool { - $fields = ['forum', 'prv']; + $fields = ['contact-type']; $condition = ['id' => $contactid]; $contact = DBA::selectFirst('contact', $fields, $condition); if (!DBA::isResult($contact)) { @@ -2948,7 +3154,7 @@ class Contact } // Is it a forum? - return ($contact['forum'] || $contact['prv']); + return ($contact['contact-type'] == self::TYPE_COMMUNITY); } /** @@ -2957,7 +3163,7 @@ class Contact * @param array $contact * @return bool */ - public static function canReceivePrivateMessages(array $contact) + public static function canReceivePrivateMessages(array $contact): bool { $protocol = $contact['network'] ?? $contact['protocol'] ?? Protocol::PHANTOM; $self = $contact['self'] ?? false; @@ -2975,7 +3181,7 @@ class Contact * @return array with search results * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - public static function searchByName(string $search, string $mode = '', int $uid = 0) + public static function searchByName(string $search, string $mode = '', int $uid = 0): array { if (empty($search)) { return []; @@ -3018,7 +3224,7 @@ class Contact * @param array $urls * @return array result "count", "added" and "updated" */ - public static function addByUrls(array $urls) + public static function addByUrls(array $urls): array { $added = 0; $updated = 0; @@ -3051,7 +3257,7 @@ class Contact * @return array The profile array * @throws Exception */ - public static function getRandomContact() + public static function getRandomContact(): array { $contact = DBA::selectFirst('contact', ['id', 'network', 'url', 'uid'], [ "`uid` = ? AND `network` = ? AND NOT `failed` AND `last-item` > ?",