]> git.mxchange.org Git - friendica.git/blobdiff - src/Model/Contact.php
Separation between picture links
[friendica.git] / src / Model / Contact.php
index 1f5dbcdcab17e139cb15b4e1480456ebedf9a0ed..f35808f050c9843986cabf3be7f3f25d6bf0c1b7 100644 (file)
@@ -1,6 +1,6 @@
 <?php
 /**
- * @copyright Copyright (C) 2010-2022, the Friendica project
+ * @copyright Copyright (C) 2010-2023, the Friendica project
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -23,28 +23,32 @@ namespace Friendica\Model;
 
 use Friendica\Contact\Avatar;
 use Friendica\Contact\Introduction\Exception\IntroductionNotFoundException;
+use Friendica\Content\Conversation As ConversationContent;
 use Friendica\Content\Pager;
 use Friendica\Content\Text\HTML;
 use Friendica\Core\Hook;
 use Friendica\Core\Logger;
 use Friendica\Core\Protocol;
 use Friendica\Core\Renderer;
-use Friendica\Core\Session;
 use Friendica\Core\System;
 use Friendica\Core\Worker;
 use Friendica\Database\Database;
 use Friendica\Database\DBA;
 use Friendica\DI;
-use Friendica\Module\NoScrape;
+use Friendica\Network\HTTPClient\Client\HttpClientAccept;
+use Friendica\Network\HTTPClient\Client\HttpClientOptions;
 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;
 use Friendica\Util\Strings;
+use Friendica\Worker\UpdateContact;
 
 /**
  * functions for interacting with a contact
@@ -98,17 +102,17 @@ class Contact
         * Relationship types
         * @{
         */
-       const NOTHING  = 0;
-       const FOLLOWER = 1;
-       const SHARING  = 2;
-       const FRIEND   = 3;
-       const SELF     = 4;
+       const NOTHING  = 0; // There is no relationship between the contact and the user
+       const FOLLOWER = 1; // The contact is following this user (the contact is the subscriber)
+       const SHARING  = 2; // The contact shares their content with this user (the user is the subscriber)
+       const FRIEND   = 3; // There is a mutual relationship between the contact and the user
+       const SELF     = 4; // This is the user theirself
        /**
         * @}
         */
 
         const MIRROR_DEACTIVATED = 0;
-        const MIRROR_FORWARDED = 1;
+        const MIRROR_FORWARDED = 1; // Deprecated, now does the same like MIRROR_OWN_POST
         const MIRROR_OWN_POST = 2;
         const MIRROR_NATIVE_RESHARE = 3;
 
@@ -138,6 +142,30 @@ class Contact
                return $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
+        * @throws \Exception
+        */
+       public static function selectAccountToArray(array $fields = [], array $condition = [], array $params = []): array
+       {
+               return DBA::selectToArray('account-user-view', $fields, $condition, $params);
+       }
+
+       /**
+        * @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|bool
+        * @throws \Exception
+        */
+       public static function selectFirstAccount(array $fields = [], array $condition = [], array $params = [])
+       {
+               return DBA::selectFirst('account-view', $fields, $condition, $params);
+       }
+
        /**
         * Insert a row into the contact table
         * Important: You can't use DBA::lastInsertId() after this call since it will be set to 0.
@@ -169,16 +197,41 @@ class Contact
                        return 0;
                }
 
-               Contact\User::insertForContactArray($contact);
-
-               // Search for duplicated contacts and get rid of them
-               if (!$contact['self']) {
-                       self::removeDuplicates($contact['nurl'], $contact['uid']);
+               $fields = DI::dbaDefinition()->truncateFieldsForTable('account-user', $contact);
+               DBA::insert('account-user', $fields, Database::INSERT_IGNORE);
+               $account_user = DBA::selectFirst('account-user', ['id'], ['uid' => $contact['uid'], 'uri-id' => $contact['uri-id']]);
+               if (empty($account_user['id'])) {
+                       Logger::warning('Account-user entry not found', ['cid' => $contact['id'], 'uid' => $contact['uid'], 'uri-id' => $contact['uri-id'], 'url' => $contact['url']]);
+               } elseif ($account_user['id'] != $contact['id']) {
+                       $duplicate = DBA::selectFirst('contact', [], ['id' => $account_user['id'], 'deleted' => false]);
+                       if (!empty($duplicate['id'])) {
+                               $ret = Contact::deleteById($contact['id']);
+                               Logger::notice('Deleted duplicated contact', ['ret' => $ret, 'account-user' => $account_user, 'cid' => $duplicate['id'], 'uid' => $duplicate['uid'], 'uri-id' => $duplicate['uri-id'], 'url' => $duplicate['url']]);
+                               $contact = $duplicate;
+                       } else {
+                               $ret = DBA::update('account-user', ['id' => $contact['id']], ['uid' => $contact['uid'], 'uri-id' => $contact['uri-id']]);
+                               Logger::notice('Updated account-user', ['ret' => $ret, 'account-user' => $account_user, 'cid' => $contact['id'], 'uid' => $contact['uid'], 'uri-id' => $contact['uri-id'], 'url' => $contact['url']]);
+                       }
                }
 
+               Contact\User::insertForContactArray($contact);
+
                return $contact['id'];
        }
 
+       /**
+        * Delete contact by id
+        *
+        * @param integer $id
+        * @return boolean
+        */
+       public static function deleteById(int $id): bool
+       {
+               Logger::debug('Delete contact', ['id' => $id]);
+               DBA::delete('account-user', ['id' => $id]);
+               return DBA::delete('contact', ['id' => $id]);
+       }
+
        /**
         * Updates rows in the contact table
         *
@@ -190,15 +243,13 @@ class Contact
         * @throws \Exception
         * @todo Let's get rid of boolean type of $old_fields
         */
-       public static function update(array $fields, array $condition, $old_fields = [])
+       public static function update(array $fields, array $condition, $old_fields = []): bool
        {
-               $fields = DI::dbaDefinition()->truncateFieldsForTable('contact', $fields);
-               $ret = DBA::update('contact', $fields, $condition, $old_fields);
-
                // Apply changes to the "user-contact" table on dedicated fields
                Contact\User::updateByContactUpdate($fields, $condition);
 
-               return $ret;
+               $fields = DI::dbaDefinition()->truncateFieldsForTable('contact', $fields);
+               return DBA::update('contact', $fields, $condition, $old_fields);
        }
 
        /**
@@ -225,6 +276,32 @@ class Contact
                return DBA::selectFirst('contact', $fields, ['uri-id' => $uri_id], ['order' => ['uid']]);
        }
 
+       /**
+        * Fetch all remote contacts for a given contact url
+        *
+        * @param string $url The URL of the contact
+        * @param array  $fields The wanted fields
+        *
+        * @return array all remote contacts
+        *
+        * @throws \Exception
+        */
+       public static function getVisitorByUrl(string $url, array $fields = ['id', 'uid']): array
+       {
+               $remote = [];
+
+               $remote_contacts = DBA::select('contact', ['id', 'uid'], ['nurl' => Strings::normaliseLink($url), 'rel' => [Contact::FOLLOWER, Contact::FRIEND], 'self' => false]);
+               while ($contact = DBA::fetch($remote_contacts)) {
+                       if (($contact['uid'] == 0) || Contact\User::isBlocked($contact['id'], $contact['uid'])) {
+                               continue;
+                       }
+                       $remote[$contact['uid']] = $contact['id'];
+               }
+               DBA::close($remote_contacts);
+
+               return $remote;
+       }
+
        /**
         * Fetches a contact by a given url
         *
@@ -252,7 +329,7 @@ class Contact
                // Add internal fields
                $removal = [];
                if (!empty($fields)) {
-                       foreach (['id', 'next-update', 'network'] as $internal) {
+                       foreach (['id', 'next-update', 'network', 'local-data'] as $internal) {
                                if (!in_array($internal, $fields)) {
                                        $fields[] = $internal;
                                        $removal[] = $internal;
@@ -281,9 +358,15 @@ class Contact
                        return [];
                }
 
+               $background_update = DI::config()->get('system', 'update_active_contacts') ? $contact['local-data'] : true;
+
                // Update the contact in the background if needed
-               if (Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
-                       Worker::add(['priority' => PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
+               if ($background_update && !self::isLocal($url) && Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                       try {
+                               UpdateContact::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], $contact['id']);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['contact' => $contact]);
+                       }
                }
 
                // Remove the internal fields
@@ -325,6 +408,18 @@ class Contact
                return $contact;
        }
 
+       /**
+        * Checks if a contact uses a specific platform
+        *
+        * @param string $url
+        * @param string $platform
+        * @return boolean
+        */
+       public static function isPlatform(string $url, string $platform): bool
+       {
+               return DBA::exists('account-view', ['nurl' => Strings::normaliseLink($url), 'platform' => $platform]);
+       }
+
        /**
         * Tests if the given contact is a follower
         *
@@ -475,7 +570,7 @@ class Contact
        {
                if (!parse_url($url, PHP_URL_SCHEME)) {
                        $addr_parts = explode('@', $url);
-                       return (count($addr_parts) == 2) && ($addr_parts[1] == DI::baseUrl()->getHostname());
+                       return (count($addr_parts) == 2) && ($addr_parts[1] == DI::baseUrl()->getHost());
                }
 
                return Strings::compareLink(self::getBasepath($url, true), DI::baseUrl());
@@ -533,7 +628,7 @@ class Contact
        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) {
+               if (DI::keyValue()->get('post_update_version') < 1427) {
                        return self::legacyGetPublicAndUserContactID($cid, $uid);
                }
 
@@ -656,7 +751,6 @@ class Contact
                        'notify'      => DI::baseUrl() . '/dfrn_notify/'  . $user['nickname'],
                        'poll'        => DI::baseUrl() . '/dfrn_poll/'    . $user['nickname'],
                        'confirm'     => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
-                       'poco'        => DI::baseUrl() . '/poco/'         . $user['nickname'],
                        'name-date'   => DateTimeFormat::utcNow(),
                        'uri-date'    => DateTimeFormat::utcNow(),
                        'avatar-date' => DateTimeFormat::utcNow(),
@@ -738,7 +832,6 @@ class Contact
                        'notify'       => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'],
                        'poll'         => DI::baseUrl() . '/dfrn_poll/'. $user['nickname'],
                        'confirm'      => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
-                       'poco'         => DI::baseUrl() . '/poco/' . $user['nickname'],
                ];
 
 
@@ -824,17 +917,19 @@ class Contact
                        return;
                }
 
+               DBA::delete('account-user', ['id' => $id]);
+
                self::clearFollowerFollowingEndpointCache($contact['uid']);
 
                // Archive the contact
-               self::update(['archive' => true, 'network' => Protocol::PHANTOM, 'deleted' => true], ['id' => $id]);
+               self::update(['archive' => true, 'network' => Protocol::PHANTOM, 'rel' => self::NOTHING, '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);
+               Worker::add(Worker::PRIORITY_MEDIUM, 'Contact\Remove', $id);
        }
 
        /**
@@ -858,7 +953,7 @@ class Contact
                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']);
+                               Worker::add(Worker::PRIORITY_HIGH, 'Contact\Unfollow', $cdata['public'], $contact['uid']);
                        }
                }
 
@@ -888,7 +983,7 @@ class Contact
                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']);
+                               Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
                        }
                }
 
@@ -916,11 +1011,11 @@ class Contact
                $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']);
+                       Worker::add(Worker::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']);
+                       Worker::add(Worker::PRIORITY_HIGH, 'Contact\RevokeFollow', $cdata['public'], $contact['uid']);
                }
 
                self::remove($contact['id']);
@@ -1041,40 +1136,26 @@ class Contact
         * Returns the data array for the photo menu of a given contact
         *
         * @param array $contact contact
-        * @param int   $uid     optional, default 0
+        * @param int   $uid     Visitor user id
         * @return array
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function photoMenu(array $contact, int $uid = 0): array
+       public static function photoMenu(array $contact, int $uid): array
        {
-               $pm_url = '';
-               $status_link = '';
-               $photos_link = '';
-
-               if ($uid == 0) {
-                       $uid = local_user();
+               // Anonymous visitor
+               if (!$uid) {
+                       return ['profile' => [DI::l10n()->t('View Profile'), self::magicLinkByContact($contact), true]];
                }
 
-               if (empty($contact['uid']) || ($contact['uid'] != $uid)) {
-                       if ($uid == 0) {
-                               $profile_link = self::magicLinkByContact($contact);
-                               $menu = ['profile' => [DI::l10n()->t('View Profile'), $profile_link, true]];
-
-                               return $menu;
-                       }
-
-                       // Look for our own contact if the uid doesn't match and isn't public
-                       $contact_own = DBA::selectFirst('contact', [], ['nurl' => $contact['nurl'], 'network' => $contact['network'], 'uid' => $uid]);
-                       if (DBA::isResult($contact_own)) {
-                               return self::photoMenu($contact_own, $uid);
-                       }
-               }
+               $pm_url      = '';
+               $status_link = '';
+               $photos_link = '';
 
                $sparkle = false;
                if (($contact['network'] === Protocol::DFRN) && !$contact['self'] && empty($contact['pending'])) {
-                       $sparkle = true;
-                       $profile_link = DI::baseUrl() . '/redir/' . $contact['id'];
+                       $sparkle      = true;
+                       $profile_link = 'contact/redir/' . $contact['id'];
                } else {
                        $profile_link = $contact['url'];
                }
@@ -1084,26 +1165,25 @@ class Contact
                }
 
                if ($sparkle) {
-                       $status_link = $profile_link . '/status';
-                       $photos_link = str_replace('/profile/', '/photos/', $profile_link);
+                       $status_link  = $profile_link . '/status';
+                       $photos_link  = $profile_link . '/photos';
                        $profile_link = $profile_link . '/profile';
                }
 
                if (self::canReceivePrivateMessages($contact) && empty($contact['pending'])) {
-                       $pm_url = DI::baseUrl() . '/message/new/' . $contact['id'];
+                       $pm_url = 'message/new/' . $contact['id'];
                }
 
-               $contact_url = DI::baseUrl() . '/contact/' . $contact['id'];
-
-               $posts_link = DI::baseUrl() . '/contact/' . $contact['id'] . '/conversations';
+               $contact_url = 'contact/' . $contact['id'];
+               $posts_link = 'contact/' . $contact['id'] . '/conversations';
 
-               $follow_link = '';
+               $follow_link   = '';
                $unfollow_link = '';
                if (!$contact['self'] && Protocol::supportsFollow($contact['network'])) {
                        if ($contact['uid'] && in_array($contact['rel'], [self::SHARING, self::FRIEND])) {
-                               $unfollow_link = 'unfollow?url=' . urlencode($contact['url']) . '&auto=1';
-                       } elseif(!$contact['pending']) {
-                               $follow_link = 'follow?url=' . urlencode($contact['url']) . '&auto=1';
+                               $unfollow_link = 'contact/unfollow?url=' . urlencode($contact['url']) . '&auto=1';
+                       } elseif (!$contact['pending']) {
+                               $follow_link = 'contact/follow?url=' . urlencode($contact['url']) . '&auto=1';
                        }
                }
 
@@ -1113,27 +1193,27 @@ class Contact
                 */
                if (empty($contact['uid'])) {
                        $menu = [
-                               'profile' => [DI::l10n()->t('View Profile')  , $profile_link , true],
-                               'network' => [DI::l10n()->t('Network Posts') , $posts_link   , false],
-                               'edit'    => [DI::l10n()->t('View Contact')  , $contact_url  , false],
-                               'follow'  => [DI::l10n()->t('Connect/Follow'), $follow_link  , true],
-                               'unfollow'=> [DI::l10n()->t('UnFollow')      , $unfollow_link, true],
+                               'profile'  => [DI::l10n()->t('View Profile')  , $profile_link , true ],
+                               'network'  => [DI::l10n()->t('Network Posts') , $posts_link   , false],
+                               'edit'     => [DI::l10n()->t('View Contact')  , $contact_url  , false],
+                               'follow'   => [DI::l10n()->t('Connect/Follow'), $follow_link  , true ],
+                               'unfollow' => [DI::l10n()->t('Unfollow')      , $unfollow_link, true ],
                        ];
                } else {
                        $menu = [
-                               'status'  => [DI::l10n()->t('View Status')   , $status_link      , true],
-                               'profile' => [DI::l10n()->t('View Profile')  , $profile_link     , true],
-                               'photos'  => [DI::l10n()->t('View Photos')   , $photos_link      , true],
-                               'network' => [DI::l10n()->t('Network Posts') , $posts_link       , false],
-                               'edit'    => [DI::l10n()->t('View Contact')  , $contact_url      , false],
-                               'pm'      => [DI::l10n()->t('Send PM')       , $pm_url           , false],
-                               'follow'  => [DI::l10n()->t('Connect/Follow'), $follow_link      , true],
-                               'unfollow'=> [DI::l10n()->t('UnFollow')      , $unfollow_link    , true],
+                               'status'   => [DI::l10n()->t('View Status')   , $status_link  , true ],
+                               'profile'  => [DI::l10n()->t('View Profile')  , $profile_link , true ],
+                               'photos'   => [DI::l10n()->t('View Photos')   , $photos_link  , true ],
+                               'network'  => [DI::l10n()->t('Network Posts') , $posts_link   , false],
+                               'edit'     => [DI::l10n()->t('View Contact')  , $contact_url  , false],
+                               'pm'       => [DI::l10n()->t('Send PM')       , $pm_url       , false],
+                               'follow'   => [DI::l10n()->t('Connect/Follow'), $follow_link  , true ],
+                               'unfollow' => [DI::l10n()->t('Unfollow')      , $unfollow_link, true ],
                        ];
 
                        if (!empty($contact['pending'])) {
                                try {
-                                       $intro = DI::intro()->selectForContact($contact['id']);
+                                       $intro          = DI::intro()->selectForContact($contact['id']);
                                        $menu['follow'] = [DI::l10n()->t('Approve'), 'notifications/intros/' . $intro->id, true];
                                } catch (IntroductionNotFoundException $exception) {
                                        DI::logger()->error('Pending contact doesn\'t have an introduction.', ['exception' => $exception]);
@@ -1192,13 +1272,19 @@ class Contact
                        return 0;
                }
 
-               $contact = self::getByURL($url, false, ['id', 'network', 'uri-id', 'next-update'], $uid);
+               $contact = self::getByURL($url, false, ['id', 'network', 'uri-id', 'next-update', 'local-data'], $uid);
 
                if (!empty($contact)) {
                        $contact_id = $contact['id'];
 
-                       if (Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
-                               Worker::add(['priority' => PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
+                       $background_update = DI::config()->get('system', 'update_active_contacts') ? $contact['local-data'] : true;
+
+                       if ($background_update && !self::isLocal($url) && Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                               try {
+                                       UpdateContact::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], $contact['id']);
+                               } catch (\InvalidArgumentException $e) {
+                                       Logger::notice($e->getMessage(), ['contact' => $contact]);
+                               }
                        }
 
                        if (empty($update) && (!empty($contact['uri-id']) || is_bool($update))) {
@@ -1255,6 +1341,11 @@ class Contact
                        return 0;
                }
 
+               if (!$contact_id && !empty($data['account-type']) && $data['account-type'] == User::ACCOUNT_TYPE_DELETED) {
+                       Logger::info('Contact is a tombstone. It will not be inserted', ['url' => $url, 'uid' => $uid]);
+                       return 0;
+               }
+
                if (!$contact_id) {
                        $urls = [Strings::normaliseLink($url), Strings::normaliseLink($data['url'])];
                        if (!empty($data['alias'])) {
@@ -1279,25 +1370,21 @@ class Contact
                                'writable'  => 1,
                                'blocked'   => 0,
                                'readonly'  => 0,
-                               'pending'   => 0];
+                               'pending'   => 0,
+                       ];
 
-                       $condition = ['nurl' => Strings::normaliseLink($data["url"]), 'uid' => $uid, 'deleted' => false];
+                       $condition = ['nurl' => Strings::normaliseLink($data['url']), 'uid' => $uid, 'deleted' => false];
 
                        // Before inserting we do check if the entry does exist now.
-                       if (DI::lock()->acquire(self::LOCK_INSERT, 0)) {
-                               $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
-                               if (DBA::isResult($contact)) {
-                                       $contact_id = $contact['id'];
-                                       Logger::notice('Contact had been created (shortly) before', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
-                               } else {
-                                       $contact_id = self::insert($fields);
-                                       if ($contact_id) {
-                                               Logger::info('Contact inserted', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
-                                       }
-                               }
-                               DI::lock()->release(self::LOCK_INSERT);
+                       $contact = DBA::selectFirst('contact', ['id'], $condition, ['order' => ['id']]);
+                       if (DBA::isResult($contact)) {
+                               $contact_id = $contact['id'];
+                               Logger::notice('Contact had been created (shortly) before', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
                        } else {
-                               Logger::warning('Contact lock had not been acquired');
+                               $contact_id = self::insert($fields);
+                               if ($contact_id) {
+                                       Logger::info('Contact inserted', ['id' => $contact_id, 'url' => $url, 'uid' => $uid]);
+                               }
                        }
 
                        if (!$contact_id) {
@@ -1309,7 +1396,21 @@ class Contact
                }
 
                if ($data['network'] == Protocol::DIASPORA) {
-                       FContact::updateFromProbeArray($data);
+                       try {
+                               DI::dsprContact()->updateFromProbeArray($data);
+                       } catch (HTTPException\NotFoundException $e) {
+                               Logger::notice($e->getMessage(), ['url' => $url, 'data' => $data]);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['url' => $url, 'data' => $data]);
+                       }
+               } elseif (!empty($data['networks'][Protocol::DIASPORA])) {
+                       try {
+                               DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]);
+                       } catch (HTTPException\NotFoundException $e) {
+                               Logger::notice($e->getMessage(), ['url' => $url, 'data' => $data['networks'][Protocol::DIASPORA]]);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['url' => $url, 'data' => $data['networks'][Protocol::DIASPORA]]);
+                       }
                }
 
                self::updateFromProbeArray($contact_id, $data);
@@ -1455,11 +1556,11 @@ class Contact
                $contact_field = ((($contact["contact-type"] == self::TYPE_COMMUNITY) || ($contact['network'] == Protocol::MAIL)) ? 'owner-id' : 'author-id');
 
                if ($thread_mode) {
-                       $condition = ["((`$contact_field` = ? AND `gravity` = ?) OR (`author-id` = ? AND `gravity` = ? AND `vid` = ?)) AND " . $sql,
-                               $cid, GRAVITY_PARENT, $cid, GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), local_user()];
+                       $condition = ["((`$contact_field` = ? AND `gravity` = ?) OR (`author-id` = ? AND `gravity` = ? AND `vid` = ? AND `protocol` != ? AND `thr-parent-id` = `parent-uri-id`)) AND " . $sql,
+                               $cid, Item::GRAVITY_PARENT, $cid, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), Conversation::PARCEL_DIASPORA, DI::userSession()->getLocalUserId()];
                } else {
                        $condition = ["`$contact_field` = ? AND `gravity` IN (?, ?) AND " . $sql,
-                               $cid, GRAVITY_PARENT, GRAVITY_COMMENT, local_user()];
+                               $cid, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT, DI::userSession()->getLocalUserId()];
                }
 
                if (!empty($parent)) {
@@ -1477,10 +1578,10 @@ class Contact
                }
 
                if (DI::mode()->isMobile()) {
-                       $itemsPerPage = DI::pConfig()->get(local_user(), 'system', 'itemspage_mobile_network',
+                       $itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_mobile_network',
                                DI::config()->get('system', 'itemspage_network_mobile'));
                } else {
-                       $itemsPerPage = DI::pConfig()->get(local_user(), 'system', 'itemspage_network',
+                       $itemsPerPage = DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'itemspage_network',
                                DI::config()->get('system', 'itemspage_network'));
                }
 
@@ -1488,7 +1589,7 @@ class Contact
 
                $params = ['order' => ['received' => true], 'limit' => [$pager->getStart(), $pager->getItemsPerPage()]];
 
-               if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll')) {
+               if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) {
                        $tpl = Renderer::getMarkupTemplate('infinite_scroll_head.tpl');
                        $o = Renderer::replaceMacros($tpl, ['$reload_uri' => DI::args()->getQueryString()]);
                } else {
@@ -1497,36 +1598,36 @@ class Contact
 
                if ($thread_mode) {
                        $fields = ['uri-id', 'thr-parent-id', 'gravity', 'author-id', 'commented'];
-                       $items = Post::toArray(Post::selectForUser(local_user(), $fields, $condition, $params));
+                       $items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), $fields, $condition, $params));
 
                        if ($pager->getStart() == 0) {
-                               $cdata = self::getPublicAndUserContactID($cid, local_user());
+                               $cdata = self::getPublicAndUserContactID($cid, DI::userSession()->getLocalUserId());
                                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, 'pinned_commented', local_user());
+                       $o .= DI::conversation()->create($items, ConversationContent::MODE_CONTACTS, $update, false, 'pinned_commented', DI::userSession()->getLocalUserId());
                } else {
                        $fields = array_merge(Item::DISPLAY_FIELDLIST, ['featured']);
-                       $items = Post::toArray(Post::selectForUser(local_user(), $fields, $condition, $params));
+                       $items = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), $fields, $condition, $params));
 
                        if ($pager->getStart() == 0) {
-                               $cdata = self::getPublicAndUserContactID($cid, local_user());
+                               $cdata = self::getPublicAndUserContactID($cid, DI::userSession()->getLocalUserId());
                                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));
+                                       $pinned = Post::toArray(Post::selectForUser(DI::userSession()->getLocalUserId(), $fields, $condition, $params));
                                        $items = array_merge($pinned, $items);
                                }
                        }
 
-                       $o .= DI::conversation()->create($items, 'contact-posts', $update);
+                       $o .= DI::conversation()->create($items, ConversationContent::MODE_CONTACT_POSTS, $update);
                }
 
                if (!$update) {
-                       if (DI::pConfig()->get(local_user(), 'system', 'infinite_scroll')) {
+                       if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'infinite_scroll')) {
                                $o .= HTML::scrollLoader();
                        } else {
                                $o .= $pager->renderMinimal(count($items));
@@ -1630,11 +1731,9 @@ class Contact
        /**
         * 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 array  $contact   contact array
         * @param string $size      Size of the avatar picture
-        * @param string $avatar    Avatar path that is displayed when no photo had been found
-        * @param bool  $no_update Don't perfom an update if no cached avatar was found
+        * @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, bool $no_update = false): string
@@ -1661,7 +1760,7 @@ class Contact
                        }
                }
 
-               return self::getAvatarUrlForId($contact['id'], $size, $contact['updated'] ?? '');
+               return self::getAvatarUrlForId($contact['id'] ?? 0, $size, $contact['updated'] ?? '');
        }
 
        /**
@@ -1973,9 +2072,10 @@ class Contact
         * @param integer $cid     contact id
         * @param string  $size    One of the Proxy::SIZE_* constants
         * @param string  $updated Contact update date
+        * @param bool    $static  If "true" a parameter is added to convert the avatar to a static one
         * @return string avatar link
         */
-       public static function getAvatarUrlForId(int $cid, string $size = '', string $updated = '', string $guid = ''): string
+       public static function getAvatarUrlForId(int $cid, string $size = '', string $updated = '', string $guid = '', bool $static = false): string
        {
                // We have to fetch the "updated" variable when it wasn't provided
                // The parameter can be provided to improve performance
@@ -2005,7 +2105,15 @@ class Contact
                                $url .= Proxy::PIXEL_LARGE . '/';
                                break;
                }
-               return $url . ($guid ?: $cid) . ($updated ? '?ts=' . strtotime($updated) : '');
+               $query_params = [];
+               if ($updated) {
+                       $query_params['ts'] = strtotime($updated);
+               }
+               if ($static) {
+                       $query_params['static'] = true;
+               }
+
+               return $url . ($guid ?: $cid) . (!empty($query_params) ? '?' . http_build_query($query_params) : '');
        }
 
        /**
@@ -2030,9 +2138,10 @@ class Contact
         * @param integer $cid     contact id
         * @param string  $size    One of the Proxy::SIZE_* constants
         * @param string  $updated Contact update date
+        * @param bool    $static  If "true" a parameter is added to convert the header to a static one
         * @return string header link
         */
-       public static function getHeaderUrlForId(int $cid, string $size = '', string $updated = '', string $guid = ''): string
+       public static function getHeaderUrlForId(int $cid, string $size = '', string $updated = '', string $guid = '', bool $static = false): string
        {
                // We have to fetch the "updated" variable when it wasn't provided
                // The parameter can be provided to improve performance
@@ -2063,7 +2172,15 @@ class Contact
                                break;
                }
 
-               return $url . ($guid ?: $cid) . ($updated ? '?ts=' . strtotime($updated) : '');
+               $query_params = [];
+               if ($updated) {
+                       $query_params['ts'] = strtotime($updated);
+               }
+               if ($static) {
+                       $query_params['static'] = true;
+               }
+
+               return $url . ($guid ?: $cid) . (!empty($query_params) ? '?' . http_build_query($query_params) : '');
        }
 
        /**
@@ -2081,18 +2198,47 @@ 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', 'uri-id'],
+               $contact = DBA::selectFirst('contact', ['uid', 'avatar', 'photo', 'thumb', 'micro', 'blurhash', 'xmpp', 'addr', 'nurl', 'url', 'network', 'uri-id'],
                        ['id' => $cid, 'self' => false]);
                if (!DBA::isResult($contact)) {
                        return;
                }
 
+               if (!Network::isValidHttpUrl($avatar)) {
+                       Logger::warning('Invalid avatar', ['cid' => $cid, 'avatar' => $avatar]);
+                       $avatar = '';
+               }
+
                $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']) && !$create_cache) {
-                       if ($contact['avatar'] != $avatar) {
-                               self::update(['avatar' => $avatar], ['id' => $cid]);
+                       if (($contact['avatar'] != $avatar) || empty($contact['blurhash'])) {
+                               $update_fields = ['avatar' => $avatar];
+                               if (!Network::isLocalLink($avatar)) {
+                                       try {
+                                               $fetchResult = HTTPSignature::fetchRaw($avatar, 0, [HttpClientOptions::ACCEPT_CONTENT => [HttpClientAccept::IMAGE]]);
+
+                                               $img_str = $fetchResult->getBody();
+                                               if (!empty($img_str)) {
+                                                       $image = new Image($img_str, Images::getMimeTypeByData($img_str));
+                                                       if ($image->isValid()) {
+                                                               $update_fields['blurhash'] = $image->getBlurHash();
+                                                       } else {
+                                                               return;
+                                                       }
+                                               }
+                                       } catch (\Exception $exception) {
+                                               Logger::notice('Error fetching avatar', ['avatar' => $avatar, 'exception' => $exception]);
+                                               return;
+                                       }
+                               } elseif (!empty($contact['blurhash'])) {
+                                       $update_fields['blurhash'] = null;
+                               } else {
+                                       return;
+                               }
+
+                               self::update($update_fields, ['id' => $cid]);
                                Logger::info('Only update the avatar', ['id' => $cid, 'avatar' => $avatar, 'contact' => $contact]);
                        }
                        return;
@@ -2122,7 +2268,9 @@ class Contact
                }
 
                if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL]) || $cache_avatar) {
-                       Avatar::deleteCache($contact);
+                       if (Avatar::deleteCache($contact)) {
+                               $force = true;
+                       }
 
                        if ($default_avatar && Proxy::isLocalImage($avatar)) {
                                $fields = ['avatar' => $avatar, 'avatar-date' => DateTimeFormat::utcNow(),
@@ -2163,7 +2311,7 @@ class Contact
                                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()];
+                                               $fields = ['avatar' => $avatar, 'photo' => $photos[0], 'thumb' => $photos[1], 'micro' => $photos[2], 'blurhash' => $photos[3], 'avatar-date' => DateTimeFormat::utcNow()];
                                                $update = !empty($fields);
                                                Logger::debug('Created new cached avatars', ['id' => $cid, 'uid' => $uid, 'owner-uid' => $local_uid]);
                                        } else {
@@ -2221,25 +2369,22 @@ class Contact
        /**
         * Helper function for "updateFromProbe". Updates personal and public contact
         *
-        * @param integer $id      contact id
-        * @param integer $uid     user id
-        * @param string  $old_url The previous profile URL of the contact
-        * @param string  $new_url The profile URL of the contact
-        * @param array   $fields  The fields that are updated
+        * @param integer $id     contact id
+        * @param integer $uid    user id
+        * @param integer $uri_id Uri-Id
+        * @param string  $url    The profile URL of the contact
+        * @param array   $fields The fields that are updated
         *
         * @throws \Exception
         */
-       private static function updateContact(int $id, int $uid, string $old_url, string $new_url, array $fields)
+       private static function updateContact(int $id, int $uid, int $uri_id, string $url, array $fields)
        {
                if (!self::update($fields, ['id' => $id])) {
                        Logger::info('Couldn\'t update contact.', ['id' => $id, 'fields' => $fields]);
                        return;
                }
 
-               // Search for duplicated contacts and get rid of them
-               if (self::removeDuplicates(Strings::normaliseLink($new_url), $uid)) {
-                       return;
-               }
+               self::setAccountUser($id, $uid, $uri_id, $url);
 
                // Archive or unarchive the contact.
                $contact = DBA::selectFirst('contact', [], ['id' => $id]);
@@ -2261,7 +2406,7 @@ class Contact
                }
 
                // Update contact data for all users
-               $condition = ['self' => false, 'nurl' => Strings::normaliseLink($old_url)];
+               $condition = ['self' => false, 'nurl' => Strings::normaliseLink($url)];
 
                $condition['network'] = [Protocol::DFRN, Protocol::DIASPORA, Protocol::ACTIVITYPUB];
                self::update($fields, $condition);
@@ -2283,6 +2428,51 @@ class Contact
                self::update($fields, $condition);
        }
 
+       /**
+        * Create or update an "account-user" entry
+        *
+        * @param integer $id
+        * @param integer $uid
+        * @param integer $uri_id
+        * @param string $url
+        * @return void
+        */
+       public static function setAccountUser(int $id, int $uid, int $uri_id, string $url)
+       {
+               if (empty($uri_id)) {
+                       return;
+               }
+
+               $account_user = DBA::selectFirst('account-user', ['id', 'uid', 'uri-id'], ['id' => $id]);
+               if (!empty($account_user['uri-id']) && ($account_user['uri-id'] != $uri_id)) {
+                       if ($account_user['uid'] == $uid) {
+                               $ret = DBA::update('account-user', ['uri-id' => $uri_id], ['id' => $id]);
+                               Logger::notice('Updated account-user uri-id', ['ret' => $ret, 'account-user' => $account_user, 'cid' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+                       } else {
+                               // This should never happen
+                               Logger::warning('account-user exists for a different uri-id and uid', ['account_user' => $account_user, 'id' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+                       }
+               }
+
+               $account_user = DBA::selectFirst('account-user', ['id', 'uid', 'uri-id'], ['uid' => $uid, 'uri-id' => $uri_id]);
+               if (!empty($account_user['id'])) {
+                       if ($account_user['id'] == $id) {
+                               Logger::debug('account-user already exists', ['id' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+                               return;
+                       } elseif (!DBA::exists('contact', ['id' => $account_user['id'], 'deleted' => false])) {
+                               $ret = DBA::update('account-user', ['id' => $id], ['uid' => $uid, 'uri-id' => $uri_id]);
+                               Logger::notice('Updated account-user', ['ret' => $ret, 'account-user' => $account_user, 'cid' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+                               return;
+                       }
+                       Logger::warning('account-user exists for a different contact id', ['account_user' => $account_user, 'id' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+                       Worker::add(Worker::PRIORITY_HIGH, 'MergeContact', $account_user['id'], $id, $uid);
+               } elseif (DBA::insert('account-user', ['id' => $id, 'uri-id' => $uri_id, 'uid' => $uid], Database::INSERT_IGNORE)) {
+                       Logger::notice('account-user was added', ['id' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+               } else {
+                       Logger::warning('account-user was not added', ['id' => $id, 'uid' => $uid, 'uri-id' => $uri_id, 'url' => $url]);
+               }
+       }
+
        /**
         * Remove duplicated contacts
         *
@@ -2307,11 +2497,6 @@ class Contact
 
                $first = $first_contact['id'];
                Logger::info('Found duplicates', ['count' => $count, 'first' => $first, 'uid' => $uid, 'nurl' => $nurl]);
-               if (($uid != 0 && ($first_contact['network'] == Protocol::DFRN))) {
-                       // Don't handle non public DFRN duplicates by now (legacy DFRN is very special because of the key handling)
-                       Logger::info('Not handling non public DFRN duplicate', ['uid' => $uid, 'nurl' => $nurl]);
-                       return false;
-               }
 
                // Find all duplicates
                $condition = ["`nurl` = ? AND `uid` = ? AND `id` != ? AND NOT `self` AND NOT `deleted`", $nurl, $uid, $first];
@@ -2321,13 +2506,51 @@ class Contact
                                continue;
                        }
 
-                       Worker::add(PRIORITY_HIGH, 'MergeContact', $first, $duplicate['id'], $uid);
+                       Worker::add(Worker::PRIORITY_HIGH, 'MergeContact', $first, $duplicate['id'], $uid);
                }
                DBA::close($duplicates);
                Logger::info('Duplicates handled', ['uid' => $uid, 'nurl' => $nurl, 'callstack' => System::callstack(20)]);
                return true;
        }
 
+       /**
+        * Perform a contact update if the contact is outdated
+        *
+        * @param integer $id contact id
+        * @return bool
+        */
+       public static function updateByIdIfNeeded(int $id): bool
+       {
+               $contact = self::selectFirst(['url'], ["`id` = ? AND `next-update` < ?", $id, DateTimeFormat::utcNow()]);
+               if (empty($contact['url'])) {
+                       return false;
+               }
+
+               if (self::isLocal($contact['url'])) {
+                       return true;
+               }
+
+               $stamp = (float)microtime(true);
+               self::updateFromProbe($id);
+               Logger::debug('Contact data is updated.', ['duration' => round((float)microtime(true) - $stamp, 3), 'id' => $id, 'url' => $contact['url'], 'callstack' => System::callstack(20)]);
+               return true;
+       }
+
+       /**
+        * Perform a contact update if the contact is outdated
+        *
+        * @param string $url contact url
+        * @return bool
+        */
+       public static function updateByUrlIfNeeded(string $url): bool
+       {
+               $id = self::getIdForURL($url, 0, false);
+               if (!empty($id)) {
+                       return self::updateByIdIfNeeded($id);
+               }
+               return (bool)self::getIdForURL($url);
+       }
+
        /**
         * Updates contact record by provided id and optional network
         *
@@ -2344,13 +2567,27 @@ class Contact
                        return false;
                }
 
-               $ret = Probe::uri($contact['url'], $network, $contact['uid']);
+               $data = Probe::uri($contact['url'], $network, $contact['uid']);
 
-               if ($ret['network'] == Protocol::DIASPORA) {
-                       FContact::updateFromProbeArray($ret);
+               if ($data['network'] == Protocol::DIASPORA) {
+                       try {
+                               DI::dsprContact()->updateFromProbeArray($data);
+                       } catch (HTTPException\NotFoundException $e) {
+                               Logger::notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
+                       }
+               } elseif (!empty($data['networks'][Protocol::DIASPORA])) {
+                       try {
+                               DI::dsprContact()->updateFromProbeArray($data['networks'][Protocol::DIASPORA]);
+                       } catch (HTTPException\NotFoundException $e) {
+                               Logger::notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['id' => $id, 'network' => $network, 'contact' => $contact, 'data' => $data]);
+                       }
                }
 
-               return self::updateFromProbeArray($id, $ret);
+               return self::updateFromProbeArray($id, $data);
        }
 
        /**
@@ -2476,7 +2713,7 @@ class Contact
 
                if (Strings::normaliseLink($contact['url']) != Strings::normaliseLink($ret['url'])) {
                        Logger::notice('New URL differs from old URL', ['id' => $id, 'uid' => $uid, 'old' => $contact['url'], 'new' => $ret['url']]);
-                       self::updateContact($id, $uid, $contact['url'], $ret['url'], ['failed' => true, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $failed_next_update, 'failure_update' => $updated]);
+                       self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => true, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $failed_next_update, 'failure_update' => $updated]);
                        return false;
                }
 
@@ -2484,14 +2721,14 @@ 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'], $contact['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated]);
+                       self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated]);
                        Logger::info('Not updating relais', ['id' => $id, 'url' => $contact['url']]);
                        return true;
                }
 
                // If Probe::uri fails the network code will be different ("feed" or "unkn")
                if (($ret['network'] == Protocol::PHANTOM) || (($ret['network'] == Protocol::FEED) && ($ret['network'] != $contact['network']))) {
-                       self::updateContact($id, $uid, $contact['url'], $ret['url'], ['failed' => true, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $failed_next_update, 'failure_update' => $updated]);
+                       self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => true, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $failed_next_update, 'failure_update' => $updated]);
                        return false;
                }
 
@@ -2523,7 +2760,7 @@ class Contact
                        if ($ret['network'] == Protocol::ACTIVITYPUB) {
                                $apcontact = APContact::getByURL($ret['url'], false);
                                if (!empty($apcontact['featured'])) {
-                                       Worker::add(PRIORITY_LOW, 'FetchFeaturedPosts', $ret['url']);
+                                       Worker::add(Worker::PRIORITY_LOW, 'FetchFeaturedPosts', $ret['url']);
                                }
                        }
 
@@ -2532,7 +2769,7 @@ class Contact
                }
 
                $update = false;
-               $guid = ($ret['guid'] ?? '') ?: Item::guidFromUri($ret['url'], parse_url($ret['url'], PHP_URL_HOST));
+               $guid = ($ret['guid'] ?? '') ?: Item::guidFromUri($ret['url']);
 
                // make sure to not overwrite existing values with blank entries except some technical fields
                $keep = ['batch', 'notify', 'poll', 'request', 'confirm', 'poco', 'baseurl'];
@@ -2560,13 +2797,11 @@ class Contact
                        self::updateAvatar($id, $ret['photo'], $update);
                }
 
-               $uriid = ItemURI::insert(['uri' => $ret['url'], 'guid' => $guid]);
-
                if (!$update) {
-                       self::updateContact($id, $uid, $contact['url'], $ret['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated]);
+                       self::updateContact($id, $uid, $uriid, $contact['url'], ['failed' => false, 'local-data' => $has_local_data, 'last-update' => $updated, 'next-update' => $success_next_update, 'success_update' => $updated]);
 
                        if (Contact\Relation::isDiscoverable($ret['url'])) {
-                               Worker::add(PRIORITY_LOW, 'ContactDiscovery', $ret['url']);
+                               Worker::add(Worker::PRIORITY_LOW, 'ContactDiscovery', $ret['url']);
                        }
 
                        // Update the public contact
@@ -2580,7 +2815,7 @@ class Contact
                        return true;
                }
 
-               $ret['uri-id']      = $uriid;
+               $ret['uri-id']      = ItemURI::insert(['uri' => $ret['url'], 'guid' => $guid]);
                $ret['nurl']        = Strings::normaliseLink($ret['url']);
                $ret['updated']     = $updated;
                $ret['failed']      = false;
@@ -2607,10 +2842,10 @@ class Contact
 
                unset($ret['photo']);
 
-               self::updateContact($id, $uid, $contact['url'], $ret['url'], $ret);
+               self::updateContact($id, $uid, $ret['uri-id'], $ret['url'], $ret);
 
                if (Contact\Relation::isDiscoverable($ret['url'])) {
-                       Worker::add(PRIORITY_LOW, 'ContactDiscovery', $ret['url']);
+                       Worker::add(Worker::PRIORITY_LOW, 'ContactDiscovery', $ret['url']);
                }
 
                return true;
@@ -2746,20 +2981,13 @@ class Contact
                }
 
                if (($network != '') && ($ret['network'] != $network)) {
-                       Logger::notice('Expected network ' . $network . ' does not match actual network ' . $ret['network']);
+                       $result['message'] = DI::l10n()->t('Expected network %s does not match actual network %s', $network, $ret['network']);
                        return $result;
                }
 
                // check if we already have a contact
-               // the poll url is more reliable than the profile url, as we may have
-               // indirect links or webfinger links
-
-               $condition = ['uid' => $uid, 'poll' => [$ret['poll'], Strings::normaliseLink($ret['poll'])], 'network' => $ret['network'], 'pending' => false];
-               $contact = DBA::selectFirst('contact', ['id', 'rel'], $condition);
-               if (!DBA::isResult($contact)) {
-                       $condition = ['uid' => $uid, 'nurl' => Strings::normaliseLink($ret['url']), 'network' => $ret['network'], 'pending' => false];
-                       $contact = DBA::selectFirst('contact', ['id', 'rel'], $condition);
-               }
+               $condition = ['uid' => $uid, 'nurl' => Strings::normaliseLink($ret['url']), 'deleted' => false];
+               $contact = DBA::selectFirst('contact', ['id', 'rel', 'url', 'pending', 'hub-verify'], $condition);
 
                $protocol = self::getProtocol($ret['url'], $ret['network']);
 
@@ -2770,30 +2998,30 @@ class Contact
 
                // do we have enough information?
                if (empty($protocol) || ($protocol == Protocol::PHANTOM) || (empty($ret['url']) && empty($ret['addr']))) {
-                       $result['message'] .= DI::l10n()->t('The profile address specified does not provide adequate information.') . EOL;
+                       $result['message'] .= DI::l10n()->t('The profile address specified does not provide adequate information.') . '<br />';
                        if (empty($ret['poll'])) {
-                               $result['message'] .= DI::l10n()->t('No compatible communication protocols or feeds were discovered.') . EOL;
+                               $result['message'] .= DI::l10n()->t('No compatible communication protocols or feeds were discovered.') . '<br />';
                        }
                        if (empty($ret['name'])) {
-                               $result['message'] .= DI::l10n()->t('An author or name was not found.') . EOL;
+                               $result['message'] .= DI::l10n()->t('An author or name was not found.') . '<br />';
                        }
                        if (empty($ret['url'])) {
-                               $result['message'] .= DI::l10n()->t('No browser URL could be matched to this address.') . EOL;
+                               $result['message'] .= DI::l10n()->t('No browser URL could be matched to this address.') . '<br />';
                        }
                        if (strpos($ret['url'], '@') !== false) {
-                               $result['message'] .= DI::l10n()->t('Unable to match @-style Identity Address with a known protocol or email contact.') . EOL;
-                               $result['message'] .= DI::l10n()->t('Use mailto: in front of address to force email check.') . EOL;
+                               $result['message'] .= DI::l10n()->t('Unable to match @-style Identity Address with a known protocol or email contact.') . '<br />';
+                               $result['message'] .= DI::l10n()->t('Use mailto: in front of address to force email check.') . '<br />';
                        }
                        return $result;
                }
 
                if ($protocol === Protocol::OSTATUS && DI::config()->get('system', 'ostatus_disabled')) {
-                       $result['message'] .= DI::l10n()->t('The profile address specified belongs to a network which has been disabled on this site.') . EOL;
+                       $result['message'] .= DI::l10n()->t('The profile address specified belongs to a network which has been disabled on this site.') . '<br />';
                        $ret['notify'] = '';
                }
 
                if (!$ret['notify']) {
-                       $result['message'] .= DI::l10n()->t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . EOL;
+                       $result['message'] .= DI::l10n()->t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . '<br />';
                }
 
                $writeable = ((($protocol === Protocol::OSTATUS) && ($ret['notify'])) ? 1 : 0);
@@ -2815,7 +3043,13 @@ class Contact
                        // update contact
                        $new_relation = (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) ? self::FRIEND : self::SHARING);
 
-                       $fields = ['rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false];
+                       $fields = ['rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false, 'network' => $ret['network']];
+
+                       if ($contact['pending'] && !empty($contact['hub-verify'])) {
+                               ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $uid);
+                               $fields['pending'] = false;
+                       }
+
                        self::update($fields, ['id' => $contact['id']]);
                } else {
                        $new_relation = (in_array($protocol, [Protocol::MAIL]) ? self::FRIEND : self::SHARING);
@@ -2852,7 +3086,7 @@ class Contact
 
                $contact = DBA::selectFirst('contact', [], ['url' => $ret['url'], 'network' => $ret['network'], 'uid' => $uid]);
                if (!DBA::isResult($contact)) {
-                       $result['message'] .= DI::l10n()->t('Unable to retrieve contact information.') . EOL;
+                       $result['message'] .= DI::l10n()->t('Unable to retrieve contact information.') . '<br />';
                        return $result;
                }
 
@@ -2866,13 +3100,17 @@ class Contact
 
                // pull feed and consume it, which should subscribe to the hub.
                if ($contact['network'] == Protocol::OSTATUS) {
-                       Worker::add(PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
+                       Worker::add(Worker::PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
                }
 
                if ($probed) {
                        self::updateFromProbeArray($contact_id, $ret);
                } else {
-                       Worker::add(PRIORITY_HIGH, 'UpdateContact', $contact_id);
+                       try {
+                               UpdateContact::add(Worker::PRIORITY_HIGH, $contact['id']);
+                       } catch (\InvalidArgumentException $e) {
+                               Logger::notice($e->getMessage(), ['contact' => $contact]);
+                       }
                }
 
                $result['success'] = Protocol::follow($uid, $contact, $protocol);
@@ -3047,11 +3285,14 @@ class Contact
                        return;
                }
 
+               Worker::add(Worker::PRIORITY_LOW, 'ContactDiscoveryForUser', $contact['uid']);
+
                self::clearFollowerFollowingEndpointCache($contact['uid']);
 
                $cdata = self::getPublicAndUserContactID($contact['id'], $contact['uid']);
-
-               DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]);
+               if (!empty($cdata['public'])) {
+                       DI::notification()->deleteForUserByVerb($contact['uid'], Activity::FOLLOW, ['actor-id' => $cdata['public']]);
+               }
        }
 
        /**
@@ -3068,8 +3309,10 @@ class Contact
                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']]);
+                       self::update(['rel' => self::FOLLOWER, 'pending' => false], ['id' => $contact['id']]);
                }
+
+               Worker::add(Worker::PRIORITY_LOW, 'ContactDiscoveryForUser', $contact['uid']);
        }
 
        /**
@@ -3148,7 +3391,7 @@ class Contact
         */
        public static function magicLink(string $contact_url, string $url = ''): string
        {
-               if (!Session::isAuthenticated()) {
+               if (!DI::userSession()->isAuthenticated()) {
                        return $url ?: $contact_url; // Equivalent to: ($url != '') ? $url : $contact_url;
                }
 
@@ -3194,7 +3437,7 @@ class Contact
        {
                $destination = $url ?: $contact['url']; // Equivalent to ($url != '') ? $url : $contact['url'];
 
-               if (!Session::isAuthenticated()) {
+               if (!DI::userSession()->isAuthenticated()) {
                        return $destination;
                }
 
@@ -3203,7 +3446,7 @@ class Contact
                        return $url;
                }
 
-               if (DI::pConfig()->get(local_user(), 'system', 'stay_local') && ($url == '')) {
+               if (DI::pConfig()->get(DI::userSession()->getLocalUserId(), 'system', 'stay_local') && ($url == '')) {
                        return 'contact/' . $contact['id'] . '/conversations';
                }
 
@@ -3215,7 +3458,7 @@ class Contact
                        return $destination;
                }
 
-               $redirect = 'redir/' . $contact['id'];
+               $redirect = 'contact/redir/' . $contact['id'];
 
                if (($url != '') && !Strings::compareLink($contact['url'], $url)) {
                        $redirect .= '?url=' . $url;
@@ -3264,11 +3507,13 @@ class Contact
         * @param string $search Name or nick
         * @param string $mode   Search mode (e.g. "community")
         * @param int    $uid    User ID
+        * @param int    $limit  Maximum amount of returned values
+        * @param int    $offset Limit offset
         *
         * @return array with search results
         * @throws \Friendica\Network\HTTPException\InternalServerErrorException
         */
-       public static function searchByName(string $search, string $mode = '', int $uid = 0): array
+       public static function searchByName(string $search, string $mode = '', int $uid = 0, int $limit = 0, int $offset = 0): array
        {
                if (empty($search)) {
                        return [];
@@ -3288,6 +3533,8 @@ class Contact
 
                if ($uid == 0) {
                        $condition['blocked'] = false;
+               } else {
+                       $condition['rel'] = [Contact::SHARING, Contact::FRIEND];
                }
 
                // check if we search only communities or every contact
@@ -3297,12 +3544,19 @@ class Contact
 
                $search .= '%';
 
+               $params = [];
+
+               if (!empty($limit) && !empty($offset)) {
+                       $params['limit'] = [$offset, $limit];
+               } elseif (!empty($limit)) {
+                       $params['limit'] = $limit;
+               }
+
                $condition = DBA::mergeConditions($condition,
                        ["(NOT `unsearchable` OR `nurl` IN (SELECT `nurl` FROM `owner-view` WHERE `publish` OR `net-publish`))
                        AND (`addr` LIKE ? OR `name` LIKE ? OR `nick` LIKE ?)", $search, $search, $search]);
 
-               $contacts = self::selectToArray([], $condition);
-               return $contacts;
+               return self::selectToArray([], $condition, $params);
        }
 
        /**
@@ -3324,11 +3578,15 @@ class Contact
                        }
                        $contact = self::getByURL($url, false, ['id', 'network', 'next-update']);
                        if (empty($contact['id']) && Network::isValidHttpUrl($url)) {
-                               Worker::add(PRIORITY_LOW, 'AddContact', 0, $url);
+                               Worker::add(Worker::PRIORITY_LOW, 'AddContact', 0, $url);
                                ++$added;
-                       } elseif (Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
-                               Worker::add(['priority' => PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
-                               ++$updated;
+                       } elseif (!empty($contact['network']) && Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                               try {
+                                       UpdateContact::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], $contact['id']);
+                                       ++$updated;
+                               } catch (\InvalidArgumentException $e) {
+                                       Logger::notice($e->getMessage(), ['contact' => $contact]);
+                               }
                        } else {
                                ++$unchanged;
                        }
@@ -3357,4 +3615,17 @@ class Contact
 
                return [];
        }
+
+       /**
+        * Checks, if contacts with the given condition exists
+        *
+        * @param array $condition
+        *
+        * @return bool
+        * @throws \Exception
+        */
+       public static function exists(array $condition): bool
+       {
+               return DBA::exists('contact', $condition);
+       }
 }