]> git.mxchange.org Git - friendica.git/blobdiff - src/Model/Contact.php
Merge pull request #12040 from nupplaphil/feat/usersession_Model
[friendica.git] / src / Model / Contact.php
index 51e5cf1510365030a98003c1f4c0058e9445a449..c5b7017a391abe31e1e265908fbf55e543841697 100644 (file)
@@ -29,13 +29,11 @@ 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\HTTPException;
 use Friendica\Network\Probe;
 use Friendica\Protocol\Activity;
@@ -138,6 +136,18 @@ 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|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.
@@ -160,6 +170,7 @@ class Contact
                        $fields['created'] = DateTimeFormat::utcNow();
                }
 
+               $fields = DI::dbaDefinition()->truncateFieldsForTable('contact', $fields);
                DBA::insert('contact', $fields, $duplicate_mode);
                $contact = DBA::selectFirst('contact', [], ['id' => DBA::lastInsertId()]);
                if (!DBA::isResult($contact)) {
@@ -168,16 +179,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
         *
@@ -191,6 +227,7 @@ class Contact
         */
        public static function update(array $fields, array $condition, $old_fields = [])
        {
+               $fields = DI::dbaDefinition()->truncateFieldsForTable('contact', $fields);
                $ret = DBA::update('contact', $fields, $condition, $old_fields);
 
                // Apply changes to the "user-contact" table on dedicated fields
@@ -223,6 +260,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
         *
@@ -250,7 +313,7 @@ class Contact
                // Add internal fields
                $removal = [];
                if (!empty($fields)) {
-                       foreach (['id', 'avatar', 'created', 'updated', 'last-update', 'success_update', 'failure_update', 'network'] as $internal) {
+                       foreach (['id', 'next-update', 'network'] as $internal) {
                                if (!in_array($internal, $fields)) {
                                        $fields[] = $internal;
                                        $removal[] = $internal;
@@ -280,9 +343,8 @@ class Contact
                }
 
                // Update the contact in the background if needed
-               $updated = max($contact['success_update'], $contact['created'], $contact['updated'], $contact['last-update'], $contact['failure_update']);
-               if (($updated < DateTimeFormat::utc('now -7 days')) && in_array($contact['network'], Protocol::FEDERATED) && !self::isLocalById($contact['id'])) {
-                       Worker::add(PRIORITY_LOW, "UpdateContact", $contact['id']);
+               if (Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                       Worker::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
                }
 
                // Remove the internal fields
@@ -324,17 +386,30 @@ 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
         *
-        * @param int $cid Either public contact id or user's contact id
-        * @param int $uid User ID
+        * @param int  $cid    Either public contact id or user's contact id
+        * @param int  $uid    User ID
+        * @param bool $strict If "true" then contact mustn't be set to pending or readonly
         *
         * @return boolean is the contact id a follower?
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function isFollower(int $cid, int $uid): bool
+       public static function isFollower(int $cid, int $uid, bool $strict = false): bool
        {
                if (Contact\User::isBlocked($cid, $uid)) {
                        return false;
@@ -346,20 +421,24 @@ class Contact
                }
 
                $condition = ['id' => $cdata['user'], 'rel' => [self::FOLLOWER, self::FRIEND]];
+               if ($strict) {
+                       $condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
+               }
                return DBA::exists('contact', $condition);
        }
 
        /**
         * Tests if the given contact url is a follower
         *
-        * @param string $url Contact URL
-        * @param int    $uid User ID
+        * @param string $url    Contact URL
+        * @param int    $uid    User ID
+        * @param bool   $strict If "true" then contact mustn't be set to pending or readonly
         *
         * @return boolean is the contact id a follower?
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function isFollowerByURL(string $url, uid $uid): bool
+       public static function isFollowerByURL(string $url, int $uid, bool $strict = false): bool
        {
                $cid = self::getIdForURL($url, $uid);
 
@@ -367,20 +446,21 @@ class Contact
                        return false;
                }
 
-               return self::isFollower($cid, $uid);
+               return self::isFollower($cid, $uid, $strict);
        }
 
        /**
         * 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
+        * @param int  $cid    Either public contact id or user's contact id
+        * @param int  $uid    User ID
+        * @param bool $strict If "true" then contact mustn't be set to pending or readonly
         *
         * @return boolean is the contact sharing with given user?
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function isSharing(int $cid, int $uid): bool
+       public static function isSharing(int $cid, int $uid, bool $strict = false): bool
        {
                if (Contact\User::isBlocked($cid, $uid)) {
                        return false;
@@ -392,20 +472,24 @@ class Contact
                }
 
                $condition = ['id' => $cdata['user'], 'rel' => [self::SHARING, self::FRIEND]];
+               if ($strict) {
+                       $condition = array_merge($condition, ['pending' => false, 'readonly' => false, 'blocked' => false]);
+               }
                return DBA::exists('contact', $condition);
        }
 
        /**
         * Tests if the given user follow the given contact url
         *
-        * @param string $url Contact URL
-        * @param int    $uid User ID
+        * @param string $url    Contact URL
+        * @param int    $uid    User ID
+        * @param bool   $strict If "true" then contact mustn't be set to pending or readonly
         *
         * @return boolean is the contact url being followed?
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function isSharingByURL(string $url, int $uid): bool
+       public static function isSharingByURL(string $url, int $uid, bool $strict = false): bool
        {
                $cid = self::getIdForURL($url, $uid);
 
@@ -413,7 +497,7 @@ class Contact
                        return false;
                }
 
-               return self::isSharing($cid, $uid);
+               return self::isSharing($cid, $uid, $strict);
        }
 
        /**
@@ -474,7 +558,6 @@ class Contact
         * Check if the given contact ID is on the same server
         *
         * @param string $url The contact link
-        *
         * @return boolean Is it the same server?
         */
        public static function isLocalById(int $cid): bool
@@ -556,7 +639,6 @@ class Contact
         *
         * @param int $cid Either public contact id or user's contact id
         * @param int $uid User ID
-        *
         * @return array with public and user's contact id
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
@@ -597,12 +679,11 @@ class Contact
         * @param int $cid A contact ID
         * @param int $uid The User ID
         * @param array $fields The selected fields for the contact
-        *
         * @return array The contact details
         *
         * @throws \Exception
         */
-       public static function getContactForUser($cid, $uid, array $fields = [])
+       public static function getContactForUser(int $cid, int $uid, array $fields = []): array
        {
                $contact = DBA::selectFirst('contact', $fields, ['id' => $cid, 'uid' => $uid]);
 
@@ -620,7 +701,7 @@ class Contact
         * @return bool Operation success
         * @throws HTTPException\InternalServerErrorException
         */
-       public static function createSelfFromUserId($uid)
+       public static function createSelfFromUserId(int $uid): bool
        {
                $user = DBA::selectFirst('user', ['uid', 'username', 'nickname', 'pubkey', 'prvkey'],
                        ['uid' => $uid, 'account_expired' => false]);
@@ -677,12 +758,12 @@ class Contact
        /**
         * Updates the self-contact for the provided user id
         *
-        * @param int     $uid
-        * @param boolean $update_avatar Force the avatar update
-        * @return bool   "true" if updated
+        * @param int   $uid
+        * @param bool  $update_avatar Force the avatar update
+        * @return bool "true" if updated
         * @throws HTTPException\InternalServerErrorException
         */
-       public static function updateSelfFromUserID($uid, $update_avatar = false)
+       public static function updateSelfFromUserID(int $uid, bool $update_avatar = false): bool
        {
                $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',
@@ -706,23 +787,33 @@ class Contact
                }
 
                $file_suffix = 'jpg';
+               $url = DI::baseUrl() . '/profile/' . $user['nickname'];
+
+               $fields = [
+                       'name'         => $profile['name'],
+                       'nick'         => $user['nickname'],
+                       'avatar-date'  => $self['avatar-date'],
+                       'location'     => Profile::formatLocation($profile),
+                       'about'        => $profile['about'],
+                       'keywords'     => $profile['pub_keywords'],
+                       'contact-type' => $user['account-type'],
+                       'prvkey'       => $user['prvkey'],
+                       'pubkey'       => $user['pubkey'],
+                       'xmpp'         => $profile['xmpp'],
+                       'matrix'       => $profile['matrix'],
+                       'network'      => Protocol::DFRN,
+                       'url'          => $url,
+                       // it seems as if ported accounts can have wrong values, so we make sure that now everything is fine.
+                       'nurl'         => Strings::normaliseLink($url),
+                       'uri-id'       => ItemURI::getIdByURI($url),
+                       'addr'         => $user['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3),
+                       'request'      => DI::baseUrl() . '/dfrn_request/' . $user['nickname'],
+                       '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'],
+               ];
 
-               $fields = ['name' => $profile['name'], 'nick' => $user['nickname'],
-                       'avatar-date' => $self['avatar-date'], 'location' => Profile::formatLocation($profile),
-                       'about' => $profile['about'], 'keywords' => $profile['pub_keywords'],
-                       'contact-type' => $user['account-type'], 'prvkey' => $user['prvkey'],
-                       'pubkey' => $user['pubkey'], 'xmpp' => $profile['xmpp'], 'matrix' => $profile['matrix'], 'network' => Protocol::DFRN];
-
-               // 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'];
-               $fields['poll'] = DI::baseUrl() . '/dfrn_poll/'. $user['nickname'];
-               $fields['confirm'] = DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'];
-               $fields['poco'] = DI::baseUrl() . '/poco/' . $user['nickname'];
 
                $avatar = Photo::selectFirst(['resource-id', 'type'], ['uid' => $uid, 'profile' => true]);
                if (DBA::isResult($avatar)) {
@@ -795,9 +886,10 @@ class Contact
         * Marks a contact for removal
         *
         * @param int $id contact id
+        * @return void
         * @throws HTTPException\InternalServerErrorException
         */
-       public static function remove($id)
+       public static function remove(int $id)
        {
                // We want just to make sure that we don't delete our "self" contact
                $contact = DBA::selectFirst('contact', ['uri-id', 'photo', 'thumb', 'micro', 'uid'], ['id' => $id, 'self' => false]);
@@ -805,6 +897,8 @@ class Contact
                        return;
                }
 
+               DBA::delete('account-user', ['id' => $id]);
+
                self::clearFollowerFollowingEndpointCache($contact['uid']);
 
                // Archive the contact
@@ -815,13 +909,14 @@ class Contact
                }
 
                // Delete it in the background
-               Worker::add(PRIORITY_MEDIUM, 'Contact\Remove', $id);
+               Worker::add(Worker::PRIORITY_MEDIUM, 'Contact\Remove', $id);
        }
 
        /**
         * Unfollow the remote contact
         *
         * @param array $contact Target user-specific contact (uid != 0) array
+        * @return void
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
@@ -838,7 +933,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']);
                        }
                }
 
@@ -851,6 +946,7 @@ class Contact
         * 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
+        * @return void
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
@@ -867,7 +963,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']);
                        }
                }
 
@@ -878,6 +974,7 @@ class Contact
         * Completely severs a relationship with a contact
         *
         * @param array $contact User-specific contact (uid != 0) array
+        * @return void
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
@@ -894,11 +991,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']);
@@ -912,7 +1009,6 @@ class Contact
 
                DI::cache()->delete(ActivityPub\Transmitter::CACHEKEY_CONTACTS . 'followers:' . $uid);
                DI::cache()->delete(ActivityPub\Transmitter::CACHEKEY_CONTACTS . 'following:' . $uid);
-               DI::cache()->delete(NoScrape::CACHEKEY . $uid);
        }
 
        /**
@@ -925,7 +1021,7 @@ class Contact
         * up or some other transient event and that there's a possibility we could recover from it.
         *
         * @param array $contact contact to mark for archival
-        * @return null
+        * @return void
         * @throws HTTPException\InternalServerErrorException
         */
        public static function markForArchival(array $contact)
@@ -978,7 +1074,7 @@ class Contact
         * @see   Contact::markForArchival()
         *
         * @param array $contact contact to be unmarked for archival
-        * @return null
+        * @return void
         * @throws \Exception
         */
        public static function unmarkForArchival(array $contact)
@@ -1025,15 +1121,14 @@ class Contact
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function photoMenu(array $contact, $uid = 0)
+       public static function photoMenu(array $contact, int $uid = 0): array
        {
                $pm_url = '';
                $status_link = '';
                $photos_link = '';
-               $poke_link = '';
 
                if ($uid == 0) {
-                       $uid = local_user();
+                       $uid = DI::userSession()->getLocalUserId();
                }
 
                if (empty($contact['uid']) || ($contact['uid'] != $uid)) {
@@ -1073,10 +1168,6 @@ class Contact
                        $pm_url = DI::baseUrl() . '/message/new/' . $contact['id'];
                }
 
-               if (($contact['network'] == Protocol::DFRN) && !$contact['self'] && empty($contact['pending'])) {
-                       $poke_link = 'contact/' . $contact['id'] . '/poke';
-               }
-
                $contact_url = DI::baseUrl() . '/contact/' . $contact['id'];
 
                $posts_link = DI::baseUrl() . '/contact/' . $contact['id'] . '/conversations';
@@ -1111,7 +1202,6 @@ class Contact
                                '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],
-                               'poke'    => [DI::l10n()->t('Poke')          , $poke_link        , false],
                                'follow'  => [DI::l10n()->t('Connect/Follow'), $follow_link      , true],
                                'unfollow'=> [DI::l10n()->t('UnFollow')      , $unfollow_link    , true],
                        ];
@@ -1168,19 +1258,23 @@ class Contact
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function getIdForURL($url, $uid = 0, $update = null, $default = [])
+       public static function getIdForURL(string $url = null, int $uid = 0, $update = null, array $default = []): int
        {
                $contact_id = 0;
 
-               if ($url == '') {
+               if (empty($url)) {
                        Logger::notice('Empty url, quitting', ['url' => $url, 'user' => $uid, 'default' => $default]);
                        return 0;
                }
 
-               $contact = self::getByURL($url, false, ['id', 'network', 'uri-id'], $uid);
+               $contact = self::getByURL($url, false, ['id', 'network', 'uri-id', 'next-update'], $uid);
 
                if (!empty($contact)) {
-                       $contact_id = $contact["id"];
+                       $contact_id = $contact['id'];
+
+                       if (Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                               Worker::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
+                       }
 
                        if (empty($update) && (!empty($contact['uri-id']) || is_bool($update))) {
                                Logger::debug('Contact found', ['url' => $url, 'uid' => $uid, 'update' => $update, 'cid' => $contact_id]);
@@ -1197,10 +1291,10 @@ class Contact
                $data = [];
 
                if (empty($default['network']) || $update) {
-                       $data = Probe::uri($url, "", $uid);
+                       $data = Probe::uri($url, '', $uid);
 
                        // Take the default values when probing failed
-                       if (!empty($default) && !in_array($data["network"], array_merge(Protocol::NATIVE_SUPPORT, [Protocol::PUMPIO]))) {
+                       if (!empty($default) && !in_array($data['network'], array_merge(Protocol::NATIVE_SUPPORT, [Protocol::PUMPIO]))) {
                                $data = array_merge($data, $default);
                        }
                } elseif (!empty($default['network'])) {
@@ -1236,6 +1330,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'])) {
@@ -1265,24 +1364,19 @@ class Contact
                        $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) {
-                               Logger::info('Contact was not inserted', ['url' => $url, 'uid' => $uid]);
+                               Logger::warning('Contact was not inserted', ['url' => $url, 'uid' => $uid]);
                                return 0;
                        }
                } else {
@@ -1312,7 +1406,7 @@ class Contact
         * @return boolean Is the contact archived?
         * @throws HTTPException\InternalServerErrorException
         */
-       public static function isArchived(int $cid)
+       public static function isArchived(int $cid): bool
        {
                if ($cid == 0) {
                        return false;
@@ -1352,11 +1446,10 @@ class Contact
         * Checks if the contact is blocked
         *
         * @param int $cid contact id
-        *
         * @return boolean Is the contact blocked?
         * @throws HTTPException\InternalServerErrorException
         */
-       public static function isBlocked($cid)
+       public static function isBlocked(int $cid): bool
        {
                if ($cid == 0) {
                        return false;
@@ -1378,11 +1471,10 @@ class Contact
         * Checks if the contact is hidden
         *
         * @param int $cid contact id
-        *
         * @return boolean Is the contact hidden?
         * @throws \Exception
         */
-       public static function isHidden($cid)
+       public static function isHidden(int $cid): bool
        {
                if ($cid == 0) {
                        return false;
@@ -1406,7 +1498,7 @@ class Contact
         * @return string posts in HTML
         * @throws \Exception
         */
-       public static function getPostsFromUrl($contact_url, $thread_mode = false, $update = 0, $parent = 0, bool $only_media = false)
+       public static function getPostsFromUrl(string $contact_url, bool $thread_mode = false, int $update = 0, int $parent = 0, bool $only_media = false): string
        {
                return self::getPostsFromId(self::getIdForURL($contact_url), $thread_mode, $update, $parent, $only_media);
        }
@@ -1422,7 +1514,7 @@ class Contact
         * @return string posts in HTML
         * @throws \Exception
         */
-       public static function getPostsFromId($cid, $thread_mode = false, $update = 0, $parent = 0, bool $only_media = false)
+       public static function getPostsFromId(int $cid, bool $thread_mode = false, int $update = 0, int $parent = 0, bool $only_media = false): string
        {
                $contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]);
                if (!DBA::isResult($contact)) {
@@ -1438,11 +1530,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 `thr-parent-id` = `parent-uri-id`)) AND " . $sql,
+                               $cid, Item::GRAVITY_PARENT, $cid, Item::GRAVITY_ACTIVITY, Verb::getID(Activity::ANNOUNCE), 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)) {
@@ -1460,10 +1552,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'));
                }
 
@@ -1471,7 +1563,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 {
@@ -1480,27 +1572,27 @@ 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, '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);
                                }
                        }
@@ -1509,7 +1601,7 @@ class Contact
                }
 
                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));
@@ -1553,11 +1645,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]);
 
@@ -1567,11 +1659,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]);
 
@@ -1581,7 +1672,7 @@ class Contact
        /**
         * Ensure that cached avatar exist
         *
-        * @param integer $cid
+        * @param integer $cid Contact id
         */
        public static function checkAvatarCache(int $cid)
        {
@@ -1614,14 +1705,12 @@ 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, $no_update = false)
+       private static function getAvatarPath(array $contact, string $size, bool $no_update = false): string
        {
                $contact = self::checkAvatarCacheByArray($contact, $no_update);
 
@@ -1645,7 +1734,7 @@ class Contact
                        }
                }
 
-               return self::getAvatarUrlForId($contact['id'], $size, $contact['updated'] ?? '');
+               return self::getAvatarUrlForId($contact['id'] ?? 0, $size, $contact['updated'] ?? '');
        }
 
        /**
@@ -1655,7 +1744,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);
        }
@@ -1667,7 +1756,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);
        }
@@ -1679,7 +1768,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);
        }
@@ -1691,7 +1780,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 = [];
@@ -1797,7 +1886,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:
@@ -1959,7 +2048,7 @@ class Contact
         * @param string  $updated Contact update date
         * @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 = ''): string
        {
                // We have to fetch the "updated" variable when it wasn't provided
                // The parameter can be provided to improve performance
@@ -2000,7 +2089,7 @@ class Contact
         * @param string  $size One of the Proxy::SIZE_* constants
         * @return string avatar link
         */
-       public static function getAvatarUrlForUrl(string $url, int $uid, string $size = ''):string
+       public static function getAvatarUrlForUrl(string $url, int $uid, string $size = ''): string
        {
                $condition = ["`nurl` = ? AND ((`uid` = ? AND `network` IN (?, ?)) OR `uid` = ?)",
                        Strings::normaliseLink($url), $uid, Protocol::FEED, Protocol::MAIL, 0];
@@ -2016,7 +2105,7 @@ class Contact
         * @param string  $updated Contact update date
         * @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 = ''): string
        {
                // We have to fetch the "updated" variable when it wasn't provided
                // The parameter can be provided to improve performance
@@ -2205,25 +2294,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]);
@@ -2245,7 +2331,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);
@@ -2267,6 +2353,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
         *
@@ -2291,11 +2422,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];
@@ -2305,7 +2431,7 @@ 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)]);
@@ -2313,6 +2439,8 @@ class Contact
        }
 
        /**
+        * Updates contact record by provided id and optional network
+        *
         * @param integer $id      contact id
         * @param string  $network Optional network we are probing for
         * @return boolean
@@ -2336,13 +2464,56 @@ class Contact
        }
 
        /**
+        * Checks if the given contact has got local data
+        *
+        * @param int   $id
+        * @param array $contact
+        *
+        * @return boolean
+        */
+       private static function hasLocalData(int $id, array $contact): bool
+       {
+               if (!empty($contact['uri-id']) && DBA::exists('contact', ["`uri-id` = ? AND `uid` != ?", $contact['uri-id'], 0])) {
+                       // User contacts with the same uri-id exist
+                       return true;
+               } elseif (DBA::exists('contact', ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($contact['url']), 0])) {
+                       // User contacts with the same nurl exists (compatibility mode for systems with missing uri-id values)
+                       return true;
+               }
+               if (DBA::exists('post-tag', ['cid' => $id])) {
+                       // Is tagged in a post
+                       return true;
+               }
+               if (DBA::exists('user-contact', ['cid' => $id])) {
+                       // Has got user-contact data
+                       return true;
+               }
+               if (Post::exists(['author-id' => $id])) {
+                       // Posts with this author exist
+                       return true;
+               }
+               if (Post::exists(['owner-id' => $id])) {
+                       // Posts with this owner exist
+                       return true;
+               }
+               if (Post::exists(['causer-id' => $id])) {
+                       // Posts with this causer exist
+                       return true;
+               }
+               // We don't have got this contact locally
+               return false;
+       }
+
+       /**
+        * Updates contact record by provided id and probed data
+        *
         * @param integer $id      contact id
         * @param array   $ret     Probed data
         * @return boolean
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       private static function updateFromProbeArray(int $id, array $ret)
+       private static function updateFromProbeArray(int $id, array $ret): bool
        {
                /*
                  Warning: Never ever fetch the public key via Probe::uri and write it into the contacts.
@@ -2354,7 +2525,8 @@ class Contact
 
                $fields = ['uid', 'uri-id', 'avatar', 'header', 'name', 'nick', 'location', 'keywords', 'about', 'subscribe',
                        'manually-approve', 'unsearchable', 'url', 'addr', 'batch', 'notify', 'poll', 'request', 'confirm', 'poco',
-                       'network', 'alias', 'baseurl', 'gsid', 'forum', 'prv', 'contact-type', 'pubkey', 'last-item', 'xmpp', 'matrix'];
+                       'network', 'alias', 'baseurl', 'gsid', 'forum', 'prv', 'contact-type', 'pubkey', 'last-item', 'xmpp', 'matrix',
+                       'created', 'last-update'];
                $contact = DBA::selectFirst('contact', $fields, ['id' => $id]);
                if (!DBA::isResult($contact)) {
                        return false;
@@ -2387,14 +2559,34 @@ class Contact
                $pubkey = $contact['pubkey'];
                unset($contact['pubkey']);
 
+               $created = $contact['created'];
+               unset($contact['created']);
+
+               $last_update = $contact['last-update'];
+               unset($contact['last-update']);
+
                $contact['photo'] = $contact['avatar'];
                unset($contact['avatar']);
 
                $updated = DateTimeFormat::utcNow();
 
+               $has_local_data = self::hasLocalData($id, $contact);
+
+               if (!Probe::isProbable($ret['network'])) {
+                       // Periodical checks are only done on federated contacts
+                       $failed_next_update  = null;
+                       $success_next_update = null;
+               } elseif ($has_local_data) {
+                       $failed_next_update  = GServer::getNextUpdateDate(false, $created, $last_update, !in_array($contact['network'], Protocol::FEDERATED));
+                       $success_next_update = GServer::getNextUpdateDate(true, $created, $last_update, !in_array($contact['network'], Protocol::FEDERATED));
+               } else {
+                       $failed_next_update  = DateTimeFormat::utc('now +6 month');
+                       $success_next_update = DateTimeFormat::utc('now +1 month');
+               }
+
                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, 'last-update' => $updated, '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;
                }
 
@@ -2402,14 +2594,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, 'last-update' => $updated, '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, 'last-update' => $updated, '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;
                }
 
@@ -2437,11 +2629,11 @@ class Contact
 
                $new_pubkey = $ret['pubkey'] ?? '';
 
-               if ($uid == 0) {
+               if ($uid == 0 && DI::config()->get('system', 'fetch_featured_posts')) {
                        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']);
                                }
                        }
 
@@ -2478,13 +2670,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, 'last-update' => $updated, '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
@@ -2498,10 +2688,12 @@ class Contact
                        return true;
                }
 
-               $ret['uri-id']  = $uriid;
-               $ret['nurl']    = Strings::normaliseLink($ret['url']);
-               $ret['updated'] = $updated;
-               $ret['failed']  = false;
+               $ret['uri-id']      = ItemURI::insert(['uri' => $ret['url'], 'guid' => $guid]);
+               $ret['nurl']        = Strings::normaliseLink($ret['url']);
+               $ret['updated']     = $updated;
+               $ret['failed']      = false;
+               $ret['next-update'] = $success_next_update;
+               $ret['local-data']  = $has_local_data;
 
                // Only fill the pubkey if it had been empty before. We have to prevent identity theft.
                if (empty($pubkey) && !empty($new_pubkey)) {
@@ -2523,10 +2715,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;
@@ -2553,12 +2745,14 @@ class Contact
        }
 
        /**
+        * Updates contact record by provided URL
+        *
         * @param integer $url contact url
         * @return integer Contact id
         * @throws HTTPException\InternalServerErrorException
         * @throws \ImagickException
         */
-       public static function updateFromProbeByURL($url)
+       public static function updateFromProbeByURL(string $url): int
        {
                $id = self::getIdForURL($url);
 
@@ -2579,7 +2773,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;
@@ -2684,30 +2878,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);
@@ -2766,7 +2960,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;
                }
 
@@ -2780,13 +2974,13 @@ 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);
+                       Worker::add(Worker::PRIORITY_HIGH, 'UpdateContact', $contact_id);
                }
 
                $result['success'] = Protocol::follow($uid, $contact, $protocol);
@@ -2946,6 +3140,7 @@ class Contact
         * 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
         */
@@ -3061,7 +3256,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;
                }
 
@@ -3107,7 +3302,7 @@ class Contact
        {
                $destination = $url ?: $contact['url']; // Equivalent to ($url != '') ? $url : $contact['url'];
 
-               if (!Session::isAuthenticated()) {
+               if (!DI::userSession()->isAuthenticated()) {
                        return $destination;
                }
 
@@ -3116,7 +3311,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';
                }
 
@@ -3235,12 +3430,12 @@ class Contact
                        if (empty($url) || !is_string($url)) {
                                continue;
                        }
-                       $contact = self::getByURL($url, false, ['id', 'updated']);
-                       if (empty($contact['id'])) {
-                               Worker::add(PRIORITY_LOW, 'AddContact', 0, $url);
+                       $contact = self::getByURL($url, false, ['id', 'network', 'next-update']);
+                       if (empty($contact['id']) && Network::isValidHttpUrl($url)) {
+                               Worker::add(Worker::PRIORITY_LOW, 'AddContact', 0, $url);
                                ++$added;
-                       } elseif ($contact['updated'] < DateTimeFormat::utc('now -7 days')) {
-                               Worker::add(PRIORITY_LOW, 'UpdateContact', $contact['id']);
+                       } elseif (!empty($contact['network']) && Probe::isProbable($contact['network']) && ($contact['next-update'] < DateTimeFormat::utcNow())) {
+                               Worker::add(['priority' => Worker::PRIORITY_LOW, 'dont_fork' => true], 'UpdateContact', $contact['id']);
                                ++$updated;
                        } else {
                                ++$unchanged;