chat: fix review findings on simplex_name persistence

- updateUserProfile no longer writes contact_profiles.simplex_name on
  the user's own row (the column is reserved for peer claims; the user's
  broadcastable name lives on contacts.simplex_name via uct.simplex_name).
- updateMemberContactProfile_'/Reset_' now write simplex_name; new
  updateMemberProfileWithConflict / updateContactMemberProfileWithConflict
  variants run conflict-clear and return the displaced name, with
  processMemberProfileUpdate emitting CEvtSimplexNameConflict.
- createContact_ runs conflict-clear before INSERT to avoid UNIQUE
  constraint violations on first-write peer collisions, returning the
  displaced name; createPreparedContact / createDirectContact thread it
  through to APIPrepareContact and saveConnInfo XInfo for event emission.
- groups conflict-clear takes ProfileId directly (avoids the NOT IN (NULL)
  silent-noop edge case when groups.group_profile_id is ON DELETE SET NULL).
- Moves clearConflictingContactProfileSimplexName_ to Shared.hs so
  createContact_ can call it without inducing a circular import.
This commit is contained in:
shum
2026-06-04 17:52:51 +00:00
parent cd0de96599
commit 27489f8919
6 changed files with 144 additions and 76 deletions
+5 -1
View File
@@ -2033,7 +2033,11 @@ processChatCommand vr nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq _) -> do
ct <- withStore $ \db -> createPreparedContact db vr user profile accLink welcomeSharedMsgId Nothing
(ct, displaced_) <- withStore $ \db -> createPreparedContact db vr user profile accLink welcomeSharedMsgId Nothing
let Profile {simplexName = pSimplexName} = profile
forM_ ((,) <$> pSimplexName <*> displaced_) $ \(ni, displaced) ->
let Contact {localDisplayName = newLDN} = ct
in toView $ CEvtSimplexNameConflict user ni SNCEContact newLDN displaced
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing
+15 -3
View File
@@ -2683,7 +2683,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
updateBusinessChatProfile gInfo
case memberContactId of
Nothing -> do
m' <- withStore $ \db -> updateMemberProfile db user m p'
(m', displaced_) <- withStore $ \db -> updateMemberProfileWithConflict db user m p'
let GroupMember {localDisplayName = newLDN} = m'
emitSimplexNameConflict newLDN displaced_
unless (muteEventInChannel gInfo m') $ do
forM_ msgTs_ $ createProfileUpdatedItem m'
toView $ CEvtGroupMemberUpdated user gInfo m m'
@@ -2692,7 +2694,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
mCt <- withStore $ \db -> getContact db vr user mContactId
if canUpdateProfile mCt
then do
(m', ct') <- withStore $ \db -> updateContactMemberProfile db user m mCt p'
(m', ct', displaced_) <- withStore $ \db -> updateContactMemberProfileWithConflict db user m mCt p'
let Contact {localDisplayName = newLDN} = ct'
emitSimplexNameConflict newLDN displaced_
unless (muteEventInChannel gInfo m') $ do
forM_ msgTs_ $ createProfileUpdatedItem m'
toView $ CEvtGroupMemberUpdated user gInfo m m'
@@ -2709,6 +2713,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
pure m
where
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo
Profile {simplexName = p'SimplexName} = p'
emitSimplexNameConflict newLDN displaced_ =
forM_ ((,) <$> p'SimplexName <*> displaced_) $ \(ni, displaced) ->
toView $ CEvtSimplexNameConflict user ni SNCEContact newLDN displaced
updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of
Just bc | isMainBusinessMember bc m -> do
g' <- withStore $ \db -> updateGroupProfileFromMember db user g p'
@@ -2950,7 +2958,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
case chatMsgEvent of
XInfo p -> do
let Connection {simplexName} = conn'
ct <- withStore $ \db -> createDirectContact db vr user conn' p simplexName
Profile {simplexName = pSimplexName} = p
(ct, displaced_) <- withStore $ \db -> createDirectContact db vr user conn' p simplexName
forM_ ((,) <$> pSimplexName <*> displaced_) $ \(ni, displaced) ->
let Contact {localDisplayName = newLDN} = ct
in toView $ CEvtSimplexNameConflict user ni SNCEContact newLDN displaced
toView $ CEvtContactConnecting user ct
pure (conn', Nothing)
XGrpLinkInv glInv -> do
+19 -44
View File
@@ -400,13 +400,18 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p
createPreparedContact :: DB.Connection -> VersionRangeChat -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
-- | Returns (contact, displaced) — displaced is Just the display_name of a
-- contact_profiles row whose peer-claimed simplex_name was cleared to make
-- room for the new contact's claim, so the caller can emit
-- CEvtSimplexNameConflict.
createPreparedContact :: DB.Connection -> VersionRangeChat -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
createPreparedContact db vr user p connLinkToConnect welcomeSharedMsgId simplexName = do
currentTs <- liftIO getCurrentTime
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
ctUserPreferences = newContactUserPrefs user p
contactId <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
getContact db vr user contactId
(contactId, displaced) <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
ct <- getContact db vr user contactId
pure (ct, displaced)
updatePreparedContactUser :: DB.Connection -> VersionRangeChat -> User -> Contact -> User -> ExceptT StoreError IO Contact
updatePreparedContactUser
@@ -446,13 +451,15 @@ updatePreparedContactUser
safeDeleteLDN db user oldLDN
getContact db vr newUser contactId
createDirectContact :: DB.Connection -> VersionRangeChat -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
-- | Returns (contact, displaced) — see createPreparedContact for displaced.
createDirectContact :: DB.Connection -> VersionRangeChat -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
createDirectContact db vr user Connection {connId, localAlias} p simplexName = do
currentTs <- liftIO getCurrentTime
let ctUserPreferences = newContactUserPrefs user p
contactId <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName
(contactId, displaced) <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
getContact db vr user contactId
ct <- getContact db vr user contactId
pure (ct, displaced)
deleteContactConnections :: DB.Connection -> User -> Contact -> IO ()
deleteContactConnections db User {userId} Contact {contactId} = do
@@ -726,38 +733,6 @@ updateContactProfile_' db userId profileId Profile {displayName, fullName, short
|]
((displayName, fullName, shortDescr, image, contactLink, simplexName, preferences, peerType, updatedAt) :. (userId, profileId))
-- | Clears simplex_name on any other contact_profiles row that holds the same
-- (user_id, simplex_name) so a subsequent UPDATE/INSERT setting that value
-- won't trip the partial UNIQUE index. Pass the profileId being updated to
-- exclude self; pass Nothing for the pre-INSERT case. Returns the displaced
-- row's display_name when a conflict was resolved, for the caller to surface
-- as CEvtSimplexNameConflict. Newer-claim-wins matches RSLV semantics: the
-- latest broadcast is the canonical assignment.
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe ContactName)
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure Nothing
clearConflictingContactProfileSimplexName_ db userId Nothing (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ?
RETURNING display_name
|]
(userId, simplexName)
clearConflictingContactProfileSimplexName_ db userId (Just profileId) (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ? AND contact_profile_id <> ?
RETURNING display_name
|]
(userId, simplexName, profileId)
-- update only member profile fields (when member doesn't have associated contact - we can reset contactLink and prefs)
updateMemberContactProfileReset_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
updateMemberContactProfileReset_ db userId profileId profile = do
@@ -765,15 +740,15 @@ updateMemberContactProfileReset_ db userId profileId profile = do
updateMemberContactProfileReset_' db userId profileId profile currentTs
updateMemberContactProfileReset_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO ()
updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image} updatedAt = do
updateMemberContactProfileReset_' db userId profileId Profile {displayName, fullName, shortDescr, image, simplexName} updatedAt = do
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, preferences = NULL, updated_at = ?
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, contact_link = NULL, simplex_name = ?, preferences = NULL, updated_at = ?
WHERE user_id = ? AND contact_profile_id = ?
|]
(displayName, fullName, shortDescr, image, updatedAt, userId, profileId)
(displayName, fullName, shortDescr, image, simplexName, updatedAt, userId, profileId)
-- update only member profile fields (when member has associated contact - we keep contactLink and prefs)
updateMemberContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO ()
@@ -782,15 +757,15 @@ updateMemberContactProfile_ db userId profileId profile = do
updateMemberContactProfile_' db userId profileId profile currentTs
updateMemberContactProfile_' :: DB.Connection -> UserId -> ProfileId -> Profile -> UTCTime -> IO ()
updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image} updatedAt = do
updateMemberContactProfile_' db userId profileId Profile {displayName, fullName, shortDescr, image, simplexName} updatedAt = do
DB.execute
db
[sql|
UPDATE contact_profiles
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, updated_at = ?
SET display_name = ?, full_name = ?, short_descr = ?, image = ?, simplex_name = ?, updated_at = ?
WHERE user_id = ? AND contact_profile_id = ?
|]
(displayName, fullName, shortDescr, image, updatedAt, userId, profileId)
(displayName, fullName, shortDescr, image, simplexName, updatedAt, userId, profileId)
updateContactLDN_ :: DB.Connection -> User -> Int64 -> ContactName -> ContactName -> UTCTime -> IO ()
updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt = do
+54 -24
View File
@@ -166,7 +166,9 @@ module Simplex.Chat.Store.Groups
setMemberContactStartedConnection,
resetMemberContactFields,
updateMemberProfile,
updateMemberProfileWithConflict,
updateContactMemberProfile,
updateContactMemberProfileWithConflict,
getXGrpLinkMemReceived,
setXGrpLinkMemReceived,
createNewUnknownGroupMember,
@@ -2379,13 +2381,15 @@ updateGroupProfileWithConflict :: DB.Connection -> User -> GroupInfo -> GroupPro
updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
displaced <- clearConflictingGroupProfileSimplexName_ db userId (Just groupId) simplexName
profileId_ <- getGroupProfileId_
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
pure ((g :: GroupInfo) {groupProfile = p', fullGroupPreferences}, displaced)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingGroupProfileSimplexName_ db userId (Just groupId) simplexName
profileId_ <- getGroupProfileId_
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
updateGroup_ ldn currentTs
pure $ Right ((g :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}, displaced)
@@ -2394,6 +2398,13 @@ updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, local
(groupType_, groupLink_) = case publicGroup of
Just PublicGroupProfile {groupType, groupLink} -> (Just groupType, Just groupLink)
Nothing -> (Nothing, Nothing)
-- groups.group_profile_id is ON DELETE SET NULL; treat the row as having
-- no profile to exclude (Nothing) when it has been nulled out, so the
-- conflict-clear sees every same-user row as a potential collision.
getGroupProfileId_ :: IO (Maybe ProfileId)
getGroupProfileId_ =
fmap (>>= fromOnly) . maybeFirstRow id $
DB.query db "SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId)
updateGroupProfile_ currentTs =
DB.execute
db
@@ -2419,9 +2430,12 @@ updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, local
safeDeleteLDN db user localDisplayName
-- | Mirror of clearConflictingContactProfileSimplexName_ for group_profiles.
-- Pass the groupId being updated to exclude its underlying group_profile_id
-- from the clear; pass Nothing for the pre-INSERT case. See that helper.
clearConflictingGroupProfileSimplexName_ :: DB.Connection -> UserId -> Maybe GroupId -> Maybe SimplexNameInfo -> IO (Maybe GroupName)
-- Pass the group_profile_id being updated to exclude self from the clear;
-- pass Nothing for the pre-INSERT case. The profileId is taken directly
-- (rather than derived from groupId via a NOT IN subquery) because
-- groups.group_profile_id is ON DELETE SET NULL, and NOT IN (NULL)
-- evaluates to UNKNOWN — which would silently no-op the clear.
clearConflictingGroupProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe GroupName)
clearConflictingGroupProfileSimplexName_ _ _ _ Nothing = pure Nothing
clearConflictingGroupProfileSimplexName_ db userId Nothing (Just simplexName) =
maybeFirstRow fromOnly $
@@ -2434,21 +2448,17 @@ clearConflictingGroupProfileSimplexName_ db userId Nothing (Just simplexName) =
RETURNING display_name
|]
(userId, simplexName)
clearConflictingGroupProfileSimplexName_ db userId (Just groupId) (Just simplexName) =
clearConflictingGroupProfileSimplexName_ db userId (Just profileId) (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE group_profiles
SET simplex_name = NULL
WHERE user_id = ?
AND simplex_name = ?
AND group_profile_id NOT IN (
SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?
)
WHERE user_id = ? AND simplex_name = ? AND group_profile_id <> ?
RETURNING display_name
|]
(userId, simplexName, userId, groupId)
(userId, simplexName, profileId)
updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo
updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do
@@ -3076,39 +3086,59 @@ setMemberContactStartedConnection db Contact {contactId} = do
(BI True, currentTs, contactId)
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
updateMemberProfile db user@User {userId} m p'
| displayName == newName = do
liftIO $ updateMemberContactProfileReset_ db userId profileId p'
pure m {memberProfile = profile}
updateMemberProfile db user m p' = fst <$> updateMemberProfileWithConflict db user m p'
-- | Like updateMemberProfile but additionally clears the simplex_name on any
-- other contact_profiles row in the same user that already holds the same
-- (user_id, simplex_name) — returning that row's display_name so the caller
-- can emit CEvtSimplexNameConflict. Used by the incoming XInfo (member) path.
updateMemberProfileWithConflict :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO (GroupMember, Maybe ContactName)
updateMemberProfileWithConflict db user@User {userId} m p'
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfileReset_' db userId profileId p' currentTs
pure (m {memberProfile = profile}, displaced)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfileReset_' db userId profileId p' currentTs
DB.execute
db
"UPDATE group_members SET local_display_name = ?, updated_at = ? WHERE user_id = ? AND group_member_id = ?"
(ldn, currentTs, userId, groupMemberId)
safeDeleteLDN db user localDisplayName
pure $ Right m {localDisplayName = ldn, memberProfile = profile}
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, displaced)
where
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
Profile {displayName = newName} = p'
Profile {displayName = newName, simplexName = profileSimplexName} = p'
profile = toLocalProfile profileId p' localAlias
updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact)
updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p'
| displayName == newName = do
liftIO $ updateMemberContactProfile_ db userId profileId p'
pure (m {memberProfile = profile}, ct {profile} :: Contact)
updateContactMemberProfile db user m ct p' = (\(m', ct', _) -> (m', ct')) <$> updateContactMemberProfileWithConflict db user m ct p'
-- | Like updateContactMemberProfile but additionally clears the simplex_name
-- on any other contact_profiles row in the same user that already holds the
-- same (user_id, simplex_name) — returning that row's display_name so the
-- caller can emit CEvtSimplexNameConflict.
updateContactMemberProfileWithConflict :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, Contact, Maybe ContactName)
updateContactMemberProfileWithConflict db user@User {userId} m ct@Contact {contactId} p'
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfile_' db userId profileId p' currentTs
pure (m {memberProfile = profile}, ct {profile} :: Contact, displaced)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact)
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact, displaced)
where
GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
Profile {displayName = newName} = p'
Profile {displayName = newName, simplexName = profileSimplexName} = p'
profile = toLocalProfile profileId p' localAlias
getXGrpLinkMemReceived :: DB.Connection -> GroupMemberId -> ExceptT StoreError IO Bool
+8 -2
View File
@@ -317,7 +317,7 @@ updateUserAutoAcceptMemberContacts db User {userId} autoAccept =
updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User
updateUserProfile db user p'
| displayName == newName = liftIO $ do
updateContactProfile_ db userId profileId p'
updateContactProfile_ db userId profileId pNoSimplexName
currentTs <- getCurrentTime
userMemberProfileUpdatedAt' <- updateUserMemberProfileUpdatedAt_ currentTs
pure user {profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
@@ -330,7 +330,7 @@ updateUserProfile db user p'
db
"INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?)"
(newName, newName, userId, currentTs, currentTs)
updateContactProfile_' db userId profileId p' currentTs
updateContactProfile_' db userId profileId pNoSimplexName currentTs
updateContactLDN_ db user userContactId localDisplayName newName currentTs
pure user {localDisplayName = newName, profile, fullPreferences, userMemberProfileUpdatedAt = userMemberProfileUpdatedAt'}
where
@@ -342,6 +342,12 @@ updateUserProfile db user p'
userMemberProfileChanged = newName /= displayName || fn' /= fullName || d' /= shortDescr || img' /= image
User {userId, userContactId, localDisplayName, profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, localAlias}, userMemberProfileUpdatedAt} = user
Profile {displayName = newName, fullName = fn', shortDescr = d', image = img', preferences} = p'
-- contact_profiles.simplex_name is reserved for peer claims received via XInfo.
-- The user's own broadcastable simplex_name lives on contacts.simplex_name
-- (loaded by toUser into User.profile.simplexName via uct.simplex_name);
-- writing it here would (a) collide with peer claims on the partial UNIQUE
-- index, and (b) make a subsequent peer claim displace the user's own row.
pNoSimplexName = (p' :: Profile) {simplexName = Nothing}
profile = toLocalProfile profileId p' localAlias
fullPreferences = fullPreferences' preferences
+43 -2
View File
@@ -421,9 +421,50 @@ createContact db user profile = do
currentTs <- liftIO getCurrentTime
void $ createContact_ db user profile emptyChatPrefs Nothing "" currentTs Nothing
createContact_ :: DB.Connection -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> Maybe SimplexNameInfo -> ExceptT StoreError IO ContactId
-- | Clears simplex_name on any other contact_profiles row that holds the same
-- (user_id, simplex_name) so a subsequent UPDATE/INSERT setting that value
-- won't trip the partial UNIQUE index. Pass the profileId being updated to
-- exclude self; pass Nothing for the pre-INSERT case. Returns the displaced
-- row's display_name when a conflict was resolved, for the caller to surface
-- as CEvtSimplexNameConflict. Newer-claim-wins matches RSLV semantics: the
-- latest broadcast is the canonical assignment.
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe ContactName)
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure Nothing
clearConflictingContactProfileSimplexName_ db userId Nothing (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ?
RETURNING display_name
|]
(userId, simplexName)
clearConflictingContactProfileSimplexName_ db userId (Just profileId) (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ? AND contact_profile_id <> ?
RETURNING display_name
|]
(userId, simplexName, profileId)
-- | Inserts a new contact and its profile. Returns the new contactId and,
-- if the peer-claimed Profile.simplexName collided with an existing row
-- (the partial UNIQUE index on contact_profiles.(user_id, simplex_name)),
-- the display_name of the displaced row — newer-claim-wins. The caller
-- is responsible for emitting CEvtSimplexNameConflict on displacement.
createContact_ :: DB.Connection -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> Maybe SimplexNameInfo -> ExceptT StoreError IO (ContactId, Maybe ContactName)
createContact_ db User {userId} Profile {displayName, fullName, shortDescr, image, contactLink, simplexName = profileSimplexName, peerType, preferences} ctUserPreferences prepared localAlias currentTs simplexName =
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
-- Clear any existing peer claim on the same simplex_name before INSERT
-- so the partial UNIQUE index doesn't reject the new row. Pass Nothing
-- as the excluded profileId — there's no self-row yet.
displaced <- clearConflictingContactProfileSimplexName_ db userId Nothing profileSimplexName
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, short_descr, image, contact_link, chat_peer_type, user_id, local_alias, preferences, simplex_name, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
@@ -434,7 +475,7 @@ createContact_ db User {userId} Profile {displayName, fullName, shortDescr, imag
"INSERT INTO contacts (contact_profile_id, user_preferences, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect, welcome_shared_msg_id, simplex_name) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
((profileId, ctUserPreferences, ldn, userId, currentTs, currentTs, currentTs, BI True) :. toPreparedContactRow prepared :. Only simplexName)
contactId <- insertedRowId db
pure $ Right contactId
pure $ Right (contactId, displaced)
newContactUserPrefs :: User -> Profile -> Preferences
newContactUserPrefs User {fullPreferences = FullPreferences {timedMessages = userTM}} Profile {preferences} =