refactor(names): agent resolution + one error type

Adopt the simplexmq names rework (PR #7045): name resolution is now
owned by the agent (resolveSimplexName picks a names-role server), so
the chat-side iteration is removed - delete ResolveError,
iterateResolvers, resolveOnUserServers, enabledSMPServersForUser and
resolveErrorToChatError.

One error type: resolver/agent failures flow through ChatErrorAgent;
remove the CEvtSimplexName* events, SimplexNameVerifyFailReason,
SimplexNameConflictEntity and CESimplexNameResolverUnavailable.
APIVerifySimplexName returns CRSimplexNameVerified (verified::Bool),
mirroring CRConnectionVerified. connectPlan handles the name target
directly; updateProfile WithConflict aliases collapsed into the plain
functions.

Add the per-operator "names" SMP server role (migration
20260612_smp_role_names, official operator on by default) feeding
ServerRoles.names -> UserServers.nameSrvs.

Bump simplexmq pin to ce69adfd and regenerate sha256map.nix.
This commit is contained in:
shum
2026-06-13 07:40:36 +00:00
parent fa75978a10
commit 69dee10bd7
38 changed files with 371 additions and 838 deletions
+3 -45
View File
@@ -494,8 +494,8 @@ data ChatCommand
| Connect {incognito :: IncognitoEnabled, connTarget_ :: Maybe ConnectTarget}
| -- Resolves the simplex_name claim on the chat row (contact or group) via
-- RSLV and compares the resolved link to the peer's stored connection link.
-- On match: writes simplex_name_verified_at and emits CEvtSimplexNameVerified.
-- On link mismatch or resolver failure: emits CEvtSimplexNameVerifyFailed.
-- Returns CRSimplexNameVerified with the boolean result (a match writes
-- simplex_name_verified_at); resolver / agent failures surface as ChatErrorAgent.
APIVerifySimplexName {chatRef :: ChatRef}
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
@@ -733,6 +733,7 @@ data ChatResponse
| CRContactCode {user :: User, contact :: Contact, connectionCode :: Text}
| CRGroupMemberCode {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionCode :: Text}
| CRConnectionVerified {user :: User, verified :: Bool, expectedCode :: Text}
| CRSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verified :: Bool}
| CRTagsUpdated {user :: User, userTags :: [ChatTag], chatTags :: [ChatTagId]}
| CRNewChatItems {user :: User, chatItems :: [AChatItem]}
| CRChatItemUpdated {user :: User, chatItem :: AChatItem}
@@ -957,46 +958,8 @@ data ChatEvent
| CEvtTimedAction {action :: String, durationMilliseconds :: Int64}
| CEvtTerminalEvent TerminalEvent
| CEvtCustomChatEvent {user_ :: Maybe User, response :: Text}
| -- Emitted when an incoming peer Profile / GroupProfile carrying a
-- simplexName collides with another row in the same user's DB that
-- already holds that name. The older row's simplex_name is NULLed
-- (newer-claim-wins per RSLV); displacedFrom is the old row's local
-- display_name, claimedBy is the peer / group whose claim won.
CEvtSimplexNameConflict {user :: User, simplexName :: SimplexNameInfo, entity :: SimplexNameConflictEntity, claimedBy :: ContactName, displacedFrom :: ContactName}
| -- Emitted by APIVerifySimplexName when the RSLV-resolved link for the
-- claimed name matches the peer's stored connection link. simplex_name_verified_at
-- has been written; UI should clear the unverified indicator.
CEvtSimplexNameVerified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, verifiedAt :: UTCTime}
| -- Emitted by APIVerifySimplexName when verification did not succeed.
-- The simplex_name claim is NOT cleared; the user may still wish to keep
-- the contact/group. UI should surface the failure reason.
CEvtSimplexNameVerifyFailed {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo, reason :: SimplexNameVerifyFailReason}
| -- Passive warning emitted when an incoming XInfo / XGrpInfo carries a
-- simplex_name claim that the user has not (yet) verified — i.e.
-- simplex_name_verified_at is NULL. The UI is expected to show an
-- unverified indicator; the user can invoke APIVerifySimplexName to clear it.
CEvtSimplexNameUnverified {user :: User, chatRef :: ChatRef, simplexName :: SimplexNameInfo}
deriving (Show)
data SimplexNameConflictEntity = SNCEContact | SNCEGroup
deriving (Show)
-- | Why APIVerifySimplexName failed. The resolved record is not stashed: we
-- intentionally do not allow a "verified to point at a DIFFERENT contact"
-- state; the user must decide whether to keep the existing contact or start
-- a fresh connection with the resolved link.
data SimplexNameVerifyFailReason
= -- | Resolver returned a NameRecord but its link for this entity's type
-- (nrSimplexContact for contacts, nrSimplexChannel for groups) differs
-- from the link stored locally for the peer.
SNVFLinkMismatch
| -- | RSLV returned AUTH (NameNotRegistered): no on-chain record exists.
SNVFNameNotRegistered
| -- | Transport / proxy / other resolver-side failure. The agent error is
-- surfaced verbatim so the UI can reuse existing agent-error rendering.
SNVFResolverError {agentError :: AgentErrorType}
deriving (Eq, Show)
data TerminalEvent
= TEGroupLinkRejected {user :: User, groupInfo :: GroupInfo, groupRejectionReason :: GroupRejectionReason}
| TERelayRejected {user :: User, groupInfo :: GroupInfo, relayRejectionReason :: RelayRejectionReason}
@@ -1419,7 +1382,6 @@ data ChatErrorType
| CEInvalidConnReq
| CESimplexNameNotFound {simplexName :: SimplexNameInfo}
| CESimplexNameUnprepared {simplexName :: SimplexNameInfo}
| CESimplexNameResolverUnavailable {simplexName :: SimplexNameInfo}
| CEUnsupportedConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEConnReqMessageProhibited
@@ -1821,10 +1783,6 @@ $(JQ.deriveJSON defaultJSON ''RelayTestFailure)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CR") ''ChatResponse)
$(JQ.deriveJSON (enumJSON $ dropPrefix "SNCE") ''SimplexNameConflictEntity)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SNVF") ''SimplexNameVerifyFailReason)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CEvt") ''ChatEvent)
$(JQ.deriveFromJSON defaultJSON ''ArchiveConfig)
+38 -149
View File
@@ -98,7 +98,7 @@ import Simplex.Messaging.Agent.Store.Interface (execSQL)
import Simplex.Messaging.Agent.Store.Shared (upMigration)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations)
import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), ProxyClientError (..), SMPWebPortServers (..), SocksMode (SMAlways), pattern NRMInteractive, textToHostMode)
import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode)
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.ShortLink as SL
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
@@ -107,8 +107,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), patt
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SMPServer, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), MsgFlags (..), NameRecord (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
@@ -2034,10 +2033,7 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq _) -> do
(ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId Nothing
let Profile {simplexName = pSimplexName} = profile
Contact {localDisplayName = newLDN} = ct
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId Nothing
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing
@@ -2239,16 +2235,15 @@ processChatCommand cxt nm = \case
CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct
CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito (Just (CTLink cLink@(ACL m cLink'))) -> withUser $ \user -> do
Connect incognito (Just ct) -> withUser $ \user -> do
-- TODO [relays] member: /c api to support groups with relays
-- TODO - possibly by going through APIPrepareGroup -> APIConnectPreparedGroup
(ccLink, plan) <- connectPlanLink user cLink False Nothing `catchAllErrors` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing)); _ -> throwError e
connectWithPlan user incognito ccLink plan
Connect incognito (Just (CTName ni)) -> withUser $ \user -> do
(ccLink, plan) <- connectPlanName user ni
(ccLink, plan) <- connectPlan user ct False Nothing `catchAllErrors` \e -> case ct of
CTLink (ACL m (CLFull cReq)) -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing Nothing))
_ -> throwError e
connectWithPlan user incognito ccLink plan
Connect _ Nothing -> throwChatError CEInvalidConnReq
APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user chatRef
APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user nm chatRef
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId
ccLink <- case contactLink of
@@ -4203,10 +4198,12 @@ processChatCommand cxt nm = \case
Nothing -> resolveAndDispatch
where
resolveAndDispatch :: CM (ACreatedConnLink, ConnectionPlan)
resolveAndDispatch =
resolveOnUserServers user nameDomain >>= \case
resolveAndDispatch = do
a <- asks smpAgent
let User {userId} = user
liftIO (runExceptT $ resolveSimplexName a nm userId nameDomain) >>= \case
Right nr -> dispatchResolvedRecord cxt nm user ni nr
Left re -> throwError $ resolveErrorToChatError ni re
Left e -> throwError $ chatErrorAgent e
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do
@@ -4631,58 +4628,6 @@ processChatCommand cxt nm = \case
gVar <- asks random
liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12
-- | Failure modes for 'resolveOnUserServers' / 'iterateResolvers'.
data ResolveError
= -- | No enabled server can resolve: every candidate answered CMD UNKNOWN
-- (predates RSLV) or CMD PROHIBITED (speaks RSLV but has no resolver
-- configured), or none were configured / reachable. Iterating across these is
-- safe: a CMD UNKNOWN relay never received the name (the client degrades RSLV
-- to a no-op below namesSMPVersion, see Protocol.hs); a CMD PROHIBITED relay
-- did receive it but is one the user already trusts as an SMP server.
ResolverUnavailable
| -- | AUTH from a name-capable server. Every name server reads the same on-chain state, so we trust the first one's no.
NameNotRegistered
| -- | First server returned a definite non-transport error (proxy, protocol, etc).
-- Surface to user so they can retry; do not iterate, for the same privacy reason.
ResolverTransport AgentErrorType
deriving (Eq, Show)
-- | Return the user's enabled SMP servers (preset and custom, excluding deleted).
-- Applies the same operator-aware filter the agent uses ('agentServerCfgs'): a
-- server is included only if it is enabled, not deleted, and either custom (no
-- operator) or owned by an enabled operator. Disabling an operator thus removes
-- its servers from name resolution, matching the rest of the app.
enabledSMPServersForUser :: User -> CM [SMPServer]
enabledSMPServersForUser user = do
ops <- serverOperators <$> withFastStore getServerOperators
smpSrvs <- withFastStore' $ \db -> getProtocolServers db SPSMP user
let opDomains = operatorDomains ops
cfgs = agentServerCfgs SPSMP opDomains $ filter (\UserServer {deleted} -> not deleted) smpSrvs
pure $ mapMaybe enabledSrv cfgs
where
enabledSrv ServerCfg {server = ProtoServerWithAuth srv _, enabled}
| enabled = Just srv
| otherwise = Nothing
-- | Resolve a SimpleX name by trying the user's enabled SMP servers in order.
-- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and servers that
-- cannot resolve (CMD UNKNOWN -- predates RSLV; or CMD PROHIBITED -- speaks RSLV
-- but has no resolver configured) all fall through to the next server. A CMD
-- UNKNOWN relay never received the name (the client degrades RSLV below
-- namesSMPVersion); a CMD PROHIBITED relay did, but it is one the user already
-- trusts as an SMP server. A definitive answer from a name-capable relay
-- terminates iteration: AUTH is definitive NotFound (every name server reads the
-- same on-chain state); any other definite error (e.g. INTERNAL on a resolver
-- backend failure) surfaces as ResolverTransport.
-- Privacy: a name-capable relay does see the queried name, so once one has
-- answered we do not broadcast the miss to every other operator the user has.
resolveOnUserServers :: User -> SimplexNameDomain -> CM (Either ResolveError NameRecord)
resolveOnUserServers user@User {userId} domain = do
srvs <- enabledSMPServersForUser user
a <- asks smpAgent
iterateResolvers srvs $ \srv ->
liftIO . runExceptT $ resolveSimplexName a NRMInteractive userId srv domain
-- | Dispatch a resolved NameRecord by eagerly preparing a contact/group row
-- with @simplex_name@ set, then returning the same plan shape ('CAPKnown' /
-- 'GLPKnown') the local-store-hit branch of 'connectPlanName' returns. The
@@ -4714,10 +4659,7 @@ dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrS
liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode contact profile from RSLV link") pure
let ccLink = CCLink cReq (Just l')
accLink = ACCL SCMContact ccLink
(ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing (Just ni)
let Profile {simplexName = pSimplexName} = profile
Contact {localDisplayName = newLDN} = ct
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
ct <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing (Just ni)
pure (accLink, CPContactAddress (CAPKnown ct))
prepareGroup :: ConnShortLink 'CMContact -> CM (ACreatedConnLink, ConnectionPlan)
prepareGroup l = do
@@ -4756,17 +4698,6 @@ firstNameLink nameType simplexChannel simplexContact ni =
NTPublicGroup -> simplexChannel
NTContact -> simplexContact
-- | Map a resolver failure to the corresponding ChatError surfaced to the user.
-- AUTH (NameNotRegistered) collapses to the same UX as a local-store miss, so
-- the user can't tell from the error whether their device knew the name.
-- Transport failures are forwarded through 'chatErrorAgent' so they reuse the
-- existing agent-error reporting in the UI.
resolveErrorToChatError :: SimplexNameInfo -> ResolveError -> ChatError
resolveErrorToChatError ni = \case
NameNotRegistered -> ChatError $ CESimplexNameNotFound ni
ResolverUnavailable -> ChatError $ CESimplexNameResolverUnavailable ni
ResolverTransport e -> chatErrorAgent e
-- | Best-effort comparison between an RSLV-resolved link (a 'Text' from the
-- name record) and the peer's stored connection link. Both are normalized via
-- 'strDecode' + 'strEncode' so scheme drift (simplex:/ vs https://simplex.chat)
@@ -4787,37 +4718,31 @@ linksMatch resolved stored = case strDecode (encodeUtf8 resolved) :: Either Stri
CLShort (CSLContact _ ct srv linkKey) ->
strEncode (CSLContact SLSServer ct srv linkKey :: ConnShortLink 'CMContact)
-- | Resolves the chat row's simplex_name claim via RSLV and compares the
-- resolved per-type link to the peer's stored connection link. On match,
-- timestamps the contact/group row and emits CEvtSimplexNameVerified.
-- On mismatch / RSLV failure, emits CEvtSimplexNameVerifyFailed.
-- Throws CESimplexNameNotFound when the row has no claim to verify.
apiVerifySimplexName :: User -> ChatRef -> CM ChatResponse
apiVerifySimplexName user chatRef = do
-- | Resolves the chat row's simplex_name claim via RSLV (the agent picks a
-- names server) and compares the resolved per-type link to the peer's stored
-- connection link. On match, timestamps the contact/group row. Returns
-- CRSimplexNameVerified with the boolean result (mirrors CRConnectionVerified);
-- resolver / agent failures propagate as the usual ChatErrorAgent.
-- Throws a command error when the row has no claim to verify.
apiVerifySimplexName :: User -> NetworkRequestMode -> ChatRef -> CM ChatResponse
apiVerifySimplexName user nm chatRef = do
cxt <- chatStoreCxt
(claim, storedLink, persistVerified) <- loadClaimAndLink cxt
let domain = (\SimplexNameInfo {nameDomain} -> nameDomain) claim
nameType' = (\SimplexNameInfo {nameType} -> nameType) claim
resolveOnUserServers user domain >>= \case
Right NameRecord {nrSimplexContact, nrSimplexChannel} -> do
let resolvedLinks = case nameType' of
NTContact -> nrSimplexContact
NTPublicGroup -> nrSimplexChannel
let SimplexNameInfo {nameType = nameType', nameDomain = domain} = claim
User {userId} = user
a <- asks smpAgent
NameRecord {nrSimplexContact, nrSimplexChannel} <-
liftIO (runExceptT $ resolveSimplexName a nm userId domain) >>= either (throwError . chatErrorAgent) pure
let resolvedLinks = case nameType' of
NTContact -> nrSimplexContact
NTPublicGroup -> nrSimplexChannel
-- The peer's stored link verifies if it matches ANY advertised link
-- (primary or fallback); an empty list never matches.
if any (`linksMatch` storedLink) resolvedLinks
then do
ts <- liftIO getCurrentTime
withStore' $ \db -> persistVerified db ts
toView $ CEvtSimplexNameVerified user chatRef claim ts
else toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFLinkMismatch
Left NameNotRegistered ->
toView $ CEvtSimplexNameVerifyFailed user chatRef claim SNVFNameNotRegistered
Left ResolverUnavailable ->
throwChatError $ CESimplexNameResolverUnavailable claim
Left (ResolverTransport e) ->
toView $ CEvtSimplexNameVerifyFailed user chatRef claim (SNVFResolverError e)
pure $ CRCmdOk (Just user)
verified = any (`linksMatch` storedLink) resolvedLinks
when verified $ do
ts <- liftIO getCurrentTime
withStore' $ \db -> persistVerified db ts
pure $ CRSimplexNameVerified user chatRef claim verified
where
-- Returns the claim to verify, the peer's stored link, and a callback that
-- persists the verified_at timestamp to the appropriate table. Throws a
@@ -4840,42 +4765,6 @@ apiVerifySimplexName user chatRef = do
pure (claim, lnk, \db ts -> setGroupSimplexNameVerifiedAt db user groupId ts)
_ -> throwCmdError "APIVerifySimplexName supports only direct and group chat refs"
-- | Pure iteration logic for 'resolveOnUserServers'. Extracted so tests can
-- supply a stub resolver without standing up a real agent / proxy.
iterateResolvers ::
Monad m =>
[SMPServer] ->
(SMPServer -> m (Either AgentErrorType NameRecord)) ->
m (Either ResolveError NameRecord)
iterateResolvers servers resolve = go servers
where
go [] = pure $ Left ResolverUnavailable
go (srv : rest) =
resolve srv >>= \case
Right nr -> pure $ Right nr
Left e
| isNotRegistered e -> pure $ Left NameNotRegistered
| isUnsupported e -> go rest
| temporaryOrHostError e -> go rest
| otherwise -> pure $ Left $ ResolverTransport e
isNotRegistered = \case
SMP _ SMP.AUTH -> True
_ -> False
-- A server that cannot resolve answers CMD UNKNOWN -- it predates RSLV (e.g.
-- an old official server), and the client degraded RSLV to a no-op below
-- namesSMPVersion so it never received the name -- or CMD PROHIBITED -- it
-- speaks RSLV but has no resolver configured (names role off), so it did
-- receive the name but cannot help. Either form may arrive directly or wrapped
-- by a proxy. We skip it and try the next server; ResolverUnavailable is
-- returned only when no server can resolve. A resolver-backed server's
-- transient failure is INTERNAL (-> ResolverTransport), not handled here.
isUnsupported = \case
SMP _ (SMP.CMD SMP.UNKNOWN) -> True
SMP _ (SMP.CMD SMP.PROHIBITED) -> True
PROXY _ _ (ProxyProtocolError (SMP.CMD SMP.UNKNOWN)) -> True
PROXY _ _ (ProxyProtocolError (SMP.CMD SMP.PROHIBITED)) -> True
_ -> False
data ConnectViaContactResult
= CVRConnectedContact Contact
| CVRSentInvitation Connection (Maybe Profile)
@@ -5811,9 +5700,9 @@ chatCommandP =
srvRolesP = srvRoles <$?> A.takeTill (\c -> c == ':' || c == ',')
where
srvRoles = \case
"off" -> Right $ ServerRoles False False
"proxy" -> Right ServerRoles {storage = False, proxy = True}
"storage" -> Right ServerRoles {storage = True, proxy = False}
"off" -> Right $ ServerRoles False False False
"proxy" -> Right ServerRoles {storage = False, proxy = True, names = False}
"storage" -> Right ServerRoles {storage = True, proxy = False, names = False}
"on" -> Right allRoles
_ -> Left "bad ServerRoles"
netCfgP = do
-9
View File
@@ -201,15 +201,6 @@ toggleNtf m ntfOn =
forM_ (memberConnId m) $ \connId ->
withAgent (\a -> toggleConnectionNtfs a connId ntfOn) `catchAllErrors` eToView
-- | Emit CEvtSimplexNameConflict when an incoming claim displaced an older
-- simplex_name binding. No-op when either piece of context is absent:
-- claim Nothing = peer did not assert a simplex_name; displaced Nothing =
-- no prior holder for the claim, so nothing was displaced.
surfaceSimplexNameConflict :: User -> Maybe SimplexNameInfo -> Maybe ContactName -> SimplexNameConflictEntity -> ContactName -> CM ()
surfaceSimplexNameConflict user claim_ displaced_ entity claimedBy =
forM_ ((,) <$> claim_ <*> displaced_) $ \(ni, displaced) ->
toView $ CEvtSimplexNameConflict user ni entity claimedBy displaced
prepareGroupMsg :: DB.Connection -> User -> GroupInfo -> Maybe MsgScope -> ShowGroupAsSender -> MsgContent -> Map MemberName MsgMention -> Maybe ChatItemId -> Maybe CIForwardedFrom -> Maybe FileInvitation -> Maybe CITimed -> Bool -> ExceptT StoreError IO (ChatMsgEvent 'Json, Maybe (CIQuote 'CTGroup))
prepareGroupMsg db user g@GroupInfo {membership} msgScope showGroupAsSender mc mentions quotedItemId_ itemForwarded fInv_ timed_ live = do
(mc', quotedItem_) <- case (quotedItemId_, itemForwarded) of
+9 -32
View File
@@ -2560,21 +2560,12 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
processContactProfileUpdate :: Contact -> Profile -> Bool -> CM Contact
processContactProfileUpdate c@Contact {profile = lp} p' createItems
| p /= p' = do
(c', displaced_) <- withStore $ \db ->
c' <- withStore $ \db ->
if userTTL == rcvTTL
then updateContactProfileWithConflict db user c p'
then updateContactProfile db user c p'
else do
c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs'
updateContactProfileWithConflict db user c' p'
let Contact {contactId = ctId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = c'
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
-- Passive unverified warning: a non-empty incoming claim that the user
-- has not verified (or whose prior verification was cleared by the
-- claim transition in updateContactProfileWithConflict) should be
-- surfaced so the UI can prompt the user to invoke APIVerifySimplexName.
forM_ p'SimplexName $ \ni ->
when (isNothing vAt) $
toView $ CEvtSimplexNameUnverified user (ChatRef CTDirect ctId Nothing) ni
updateContactProfile db user c' p'
when (directOrUsed c' && createItems) $ do
createProfileUpdatedItem c'
lift $ createRcvFeatureItems user c c'
@@ -2586,7 +2577,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
p = fromLocalProfile lp
Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c
userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs
Profile {preferences = rcvPrefs_, simplexName = p'SimplexName} = p'
Profile {preferences = rcvPrefs_} = p'
rcvTTL = prefParam $ getPreference SCFTimedMessages rcvPrefs_
ctUserPrefs' =
let userDefault = getPreference SCFTimedMessages (fullPreferences user)
@@ -2689,9 +2680,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
updateBusinessChatProfile gInfo
case memberContactId of
Nothing -> do
(m', displaced_) <- withStore $ \db -> updateMemberProfileWithConflict db user m p'
let GroupMember {localDisplayName = newLDN} = m'
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
m' <- withStore $ \db -> updateMemberProfile db user m p'
unless (muteEventInChannel gInfo m') $ do
forM_ msgTs_ $ createProfileUpdatedItem m'
toView $ CEvtGroupMemberUpdated user gInfo m m'
@@ -2700,9 +2689,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
mCt <- withStore $ \db -> getContact db cxt user mContactId
if canUpdateProfile mCt
then do
(m', ct', displaced_) <- withStore $ \db -> updateContactMemberProfileWithConflict db user m mCt p'
let Contact {localDisplayName = newLDN} = ct'
surfaceSimplexNameConflict user p'SimplexName displaced_ SNCEContact newLDN
(m', ct') <- withStore $ \db -> updateContactMemberProfile db user m mCt p'
unless (muteEventInChannel gInfo m') $ do
forM_ msgTs_ $ createProfileUpdatedItem m'
toView $ CEvtGroupMemberUpdated user gInfo m m'
@@ -2719,7 +2706,6 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
pure m
where
allowSimplexLinks = groupFeatureMemberAllowed SGFSimplexLinks m gInfo
Profile {simplexName = p'SimplexName} = p'
updateBusinessChatProfile g@GroupInfo {businessChat} = case businessChat of
Just bc | isMainBusinessMember bc m -> do
g' <- withStore $ \db -> updateGroupProfileFromMember db user g p'
@@ -2965,10 +2951,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
-- to createDirectContact; after this point contacts.simplex_name is
-- the source of truth.
let Connection {simplexName} = conn'
Profile {simplexName = pSimplexName} = p
(ct, displaced_) <- withStore $ \db -> createDirectContact db cxt user conn' p simplexName
let Contact {localDisplayName = newLDN} = ct
surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN
ct <- withStore $ \db -> createDirectContact db cxt user conn' p simplexName
toView $ CEvtContactConnecting user ct
pure (conn', Nothing)
XGrpLinkInv glInv -> do
@@ -3305,7 +3288,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' msgSigned
xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope)
xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg', simplexName = p'GroupSimplexName} msg@RcvMessage {msgSigned} brokerTs
xGrpInfo g@GroupInfo {groupProfile = p@GroupProfile {publicGroup = pg}, businessChat} m@GroupMember {memberRole} p'@GroupProfile {publicGroup = pg'} msg@RcvMessage {msgSigned} brokerTs
| memberRole < GROwner = messageError "x.grp.info with insufficient member permissions" $> Nothing
| let pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId),
useRelays' g && (isNothing pg' || pgId pg' /= pgId pg) = messageError "x.grp.info: publicGroupId mismatch for channel" $> Nothing
@@ -3313,13 +3296,7 @@ processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage =
| otherwise = do
case businessChat of
Nothing -> unless (p == p') $ do
(g', displaced_) <- withStore $ \db -> updateGroupProfileWithConflict db user g p'
let GroupInfo {groupId = gId, localDisplayName = newLDN, simplexNameVerifiedAt = vAt} = g'
surfaceSimplexNameConflict user p'GroupSimplexName displaced_ SNCEGroup newLDN
-- Passive unverified warning, mirrors processContactProfileUpdate.
forM_ p'GroupSimplexName $ \ni ->
when (isNothing vAt) $
toView $ CEvtSimplexNameUnverified user (ChatRef CTGroup gId Nothing) ni
g' <- withStore $ \db -> updateGroupProfile db user g p'
(g'', m', scopeInfo) <- mkGroupChatScope g' m
toView $ CEvtGroupUpdated user g g'' (Just m') msgSigned
let cd = CDGroupRcv g'' scopeInfo m'
+2 -2
View File
@@ -39,8 +39,8 @@ operatorFlux =
serverDomains = ["simplexonflux.com"],
conditionsAcceptance = CARequired Nothing,
enabled = True,
smpRoles = ServerRoles {storage = False, proxy = True},
xftpRoles = ServerRoles {storage = False, proxy = True}
smpRoles = ServerRoles {storage = False, proxy = True, names = False},
xftpRoles = ServerRoles {storage = False, proxy = True, names = False}
}
-- Please note: if any servers are removed from the lists below, they MUST be added here.
+17 -29
View File
@@ -53,7 +53,6 @@ module Simplex.Chat.Store.Direct
getContactIdByName,
getContactIdBySimplexName,
updateContactProfile,
updateContactProfileWithConflict,
setContactSimplexNameVerifiedAt,
updateContactUserPreferences,
updateContactAlias,
@@ -401,18 +400,13 @@ createIncognitoProfile db User {userId} p = do
createdAt <- getCurrentTime
createIncognitoProfile_ db userId createdAt p
-- | 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 -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId simplexName = do
currentTs <- liftIO getCurrentTime
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
ctUserPreferences = newContactUserPrefs user p
(contactId, displaced) <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
ct <- getContact db cxt user contactId
pure (ct, displaced)
contactId <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName
getContact db cxt user contactId
updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact
updatePreparedContactUser
@@ -452,15 +446,13 @@ updatePreparedContactUser
safeDeleteLDN db user oldLDN
getContact db cxt newUser contactId
-- | Returns (contact, displaced) — see createPreparedContact for displaced.
createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName)
createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO Contact
createDirectContact db cxt user Connection {connId, localAlias} p simplexName = do
currentTs <- liftIO getCurrentTime
let ctUserPreferences = newContactUserPrefs user p
(contactId, displaced) <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName
contactId <- 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)
ct <- getContact db cxt user contactId
pure (ct, displaced)
getContact db cxt user contactId
deleteContactConnections :: DB.Connection -> User -> Contact -> IO ()
deleteContactConnections db User {userId} Contact {contactId} = do
@@ -566,33 +558,29 @@ deleteUnusedProfile_ db userId profileId =
:. (userId, profileId, userId, profileId, profileId)
)
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
updateContactProfile db user c p' = fst <$> updateContactProfileWithConflict db user c p'
-- | Like updateContactProfile 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 path; local
-- updates that don't expect conflicts can continue to use updateContactProfile.
-- | Updates the contact profile, also clearing the simplex_name on any other
-- contact_profiles row in the same user that already holds the same
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE
-- index.
--
-- Also clears contacts.simplex_name_verified_at when the peer's simplex_name
-- claim changes (any value transition, including Nothing<->Just): the prior
-- verification was tied to the prior claim and must be re-issued by the user.
updateContactProfileWithConflict :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO (Contact, Maybe ContactName)
updateContactProfileWithConflict db user@User {userId} c p'
updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact
updateContactProfile db user@User {userId} c p'
| displayName == newName = do
displaced <- liftIO $ clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
liftIO $ clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
liftIO $ updateContactProfile_ db userId profileId p'
liftIO clearVerifiedAtIfClaimChanged
pure (c' {profile, mergedPreferences}, displaced)
pure $ c' {profile, mergedPreferences}
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateContactProfile_' db userId profileId p' currentTs
updateContactLDN_ db user contactId localDisplayName ldn currentTs
clearVerifiedAtIfClaimChanged
pure $ Right (c' {localDisplayName = ldn, profile, mergedPreferences}, displaced)
pure $ Right c' {localDisplayName = ldn, profile, mergedPreferences}
where
Contact {contactId, localDisplayName, profile = LocalProfile {profileId, displayName, localAlias, simplexName = prevClaim}, userPreferences} = c
Profile {displayName = newName, simplexName = profileSimplexName, preferences} = p'
@@ -606,7 +594,7 @@ updateContactProfileWithConflict db user@User {userId} c p'
-- | Records that the user successfully RSLV-verified the peer's simplex_name
-- claim against the contact's stored connection link. Cleared back to NULL by
-- updateContactProfileWithConflict whenever the peer's claim transitions.
-- updateContactProfile whenever the peer's claim transitions.
setContactSimplexNameVerifiedAt :: DB.Connection -> User -> ContactId -> UTCTime -> IO ()
setContactSimplexNameVerifiedAt db User {userId} contactId ts =
DB.execute
+43 -62
View File
@@ -46,7 +46,6 @@ module Simplex.Chat.Store.Groups
getGroupViaShortLinkToConnect,
getGroupInfoByGroupLinkHash,
updateGroupProfile,
updateGroupProfileWithConflict,
setGroupSimplexNameVerifiedAt,
clearConflictingGroupProfileSimplexName_,
updateGroupPreferences,
@@ -167,9 +166,7 @@ module Simplex.Chat.Store.Groups
setMemberContactStartedConnection,
resetMemberContactFields,
updateMemberProfile,
updateMemberProfileWithConflict,
updateContactMemberProfile,
updateContactMemberProfileWithConflict,
getXGrpLinkMemReceived,
setXGrpLinkMemReceived,
createNewUnknownGroupMember,
@@ -2388,36 +2385,32 @@ createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Version
createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode =
createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff Nothing
-- | Updates the group profile, also clearing the simplex_name on any other
-- group_profiles row (for the same user) that already holds the same
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo
updateGroupProfile db user g p' = fst <$> updateGroupProfileWithConflict db user g p'
-- | Like updateGroupProfile but additionally clears the simplex_name on any
-- other group_profiles row (for 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 XGrpInfo path.
updateGroupProfileWithConflict :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO (GroupInfo, Maybe GroupName)
updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, simplexName = prevClaim}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName, simplexName = prevClaim}} p'@GroupProfile {displayName = newName, fullName, shortDescr, description, image, publicGroup, simplexName, groupPreferences, memberAdmission}
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
profileId_ <- getGroupProfileId_
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
clearVerifiedAtIfClaimChanged
pure ((g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}, displaced)
pure $ (g' :: GroupInfo) {groupProfile = p', fullGroupPreferences}
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
profileId_ <- getGroupProfileId_
displaced <- clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
clearConflictingGroupProfileSimplexName_ db userId profileId_ simplexName
updateGroupProfile_ currentTs
updateGroup_ ldn currentTs
clearVerifiedAtIfClaimChanged
pure $ Right ((g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}, displaced)
pure $ Right $ (g' :: GroupInfo) {localDisplayName = ldn, groupProfile = p', fullGroupPreferences}
where
fullGroupPreferences = mergeGroupPreferences groupPreferences
claimChanged = prevClaim /= simplexName
g' = if claimChanged then (g :: GroupInfo) {simplexNameVerifiedAt = Nothing} else g
-- Mirrors updateContactProfileWithConflict: clear the verification when
-- Mirrors updateContactProfile: clear the verification when
-- the peer's claim transitions to/from/between values; prior verification
-- was bound to the prior claim.
clearVerifiedAtIfClaimChanged =
@@ -2463,30 +2456,26 @@ updateGroupProfileWithConflict db user@User {userId} g@GroupInfo {groupId, local
-- (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.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO ()
clearConflictingGroupProfileSimplexName_ _ _ _ Nothing = pure ()
clearConflictingGroupProfileSimplexName_ db userId Nothing (Just simplexName) =
maybeFirstRow fromOnly $
DB.query
db
[sql|
UPDATE group_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ?
RETURNING display_name
|]
(userId, simplexName)
DB.execute
db
[sql|
UPDATE group_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ?
|]
(userId, 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 <> ?
RETURNING display_name
|]
(userId, simplexName, profileId)
DB.execute
db
[sql|
UPDATE group_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ? AND group_profile_id <> ?
|]
(userId, simplexName, profileId)
-- | Mirror of setContactSimplexNameVerifiedAt for groups.
setGroupSimplexNameVerifiedAt :: DB.Connection -> User -> GroupId -> UTCTime -> IO ()
@@ -3121,57 +3110,49 @@ setMemberContactStartedConnection db Contact {contactId} = do
"UPDATE contacts SET grp_direct_inv_started_connection = ?, updated_at = ? WHERE contact_id = ?"
(BI True, currentTs, contactId)
-- | Updates the member profile, also clearing the simplex_name on any other
-- contact_profiles row in the same user that already holds the same
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember
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'
updateMemberProfile db user@User {userId} m p'
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfileReset_' db userId profileId p' currentTs
pure (m {memberProfile = profile}, displaced)
pure m {memberProfile = profile}
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
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}, displaced)
pure $ Right m {localDisplayName = ldn, memberProfile = profile}
where
GroupMember {groupMemberId, localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
Profile {displayName = newName, simplexName = profileSimplexName} = p'
profile = toLocalProfile profileId p' localAlias
-- | Updates the member's contact profile, also clearing the simplex_name on any
-- other contact_profiles row in the same user that already holds the same
-- (user_id, simplex_name) — newer-claim-wins, required by the partial UNIQUE index.
updateContactMemberProfile :: DB.Connection -> User -> GroupMember -> Contact -> Profile -> ExceptT StoreError IO (GroupMember, 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'
updateContactMemberProfile db user@User {userId} m ct@Contact {contactId} p'
| displayName == newName = liftIO $ do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
updateMemberContactProfile_' db userId profileId p' currentTs
pure (m {memberProfile = profile}, ct {profile} :: Contact, displaced)
pure (m {memberProfile = profile}, ct {profile} :: Contact)
| otherwise =
ExceptT . withLocalDisplayName db userId newName $ \ldn -> do
currentTs <- getCurrentTime
displaced <- clearConflictingContactProfileSimplexName_ db userId (Just profileId) profileSimplexName
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, displaced)
pure $ Right (m {localDisplayName = ldn, memberProfile = profile}, ct {localDisplayName = ldn, profile} :: Contact)
where
GroupMember {localDisplayName, memberProfile = LocalProfile {profileId, displayName, localAlias}} = m
Profile {displayName = newName, simplexName = profileSimplexName} = p'
@@ -38,6 +38,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.Postgres.Migrations.M20260603_simplex_name
import Simplex.Chat.Store.Postgres.Migrations.M20260604_simplex_name_profiles
import Simplex.Chat.Store.Postgres.Migrations.M20260606_simplex_name_verified
import Simplex.Chat.Store.Postgres.Migrations.M20260612_smp_role_names
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -75,7 +76,8 @@ schemaMigrations =
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified),
("20260612_smp_role_names", m20260612_smp_role_names, Just down_m20260612_smp_role_names)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,23 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260612_smp_role_names where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
m20260612_smp_role_names :: Text
m20260612_smp_role_names =
T.pack
[r|
ALTER TABLE server_operators ADD COLUMN smp_role_names SMALLINT NOT NULL DEFAULT 0;
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex';
|]
down_m20260612_smp_role_names :: Text
down_m20260612_smp_role_names =
T.pack
[r|
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|]
@@ -1334,7 +1334,8 @@ CREATE TABLE test_chat_schema.server_operators (
xftp_role_storage smallint DEFAULT 1 NOT NULL,
xftp_role_proxy smallint DEFAULT 1 NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,
updated_at timestamp with time zone DEFAULT now() NOT NULL
updated_at timestamp with time zone DEFAULT now() NOT NULL,
smp_role_names smallint DEFAULT 0 NOT NULL
);
+14 -12
View File
@@ -719,10 +719,10 @@ updateServerOperator db currentTs ServerOperator {operatorId, enabled, smpRoles,
db
[sql|
UPDATE server_operators
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
SET enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?, updated_at = ?
WHERE server_operator_id = ?
|]
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
(BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), currentTs, operatorId)
getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [(Maybe PresetOperator, Maybe ServerOperator)]
getUpdateServerOperators db presetOps newUser = do
@@ -757,20 +757,20 @@ getUpdateServerOperators db presetOps newUser = do
db
[sql|
UPDATE server_operators
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, xftp_role_storage = ?, xftp_role_proxy = ?
SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, smp_role_storage = ?, smp_role_proxy = ?, smp_role_names = ?, xftp_role_storage = ?, xftp_role_proxy = ?
WHERE server_operator_id = ?
|]
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
(tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles), operatorId)
insertOperator :: NewServerOperator -> IO ServerOperator
insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, smpRoles, xftpRoles} = do
DB.execute
db
[sql|
INSERT INTO server_operators
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?)
(server_operator_tag, trade_name, legal_name, server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy)
VALUES (?,?,?,?,?,?,?,?,?,?)
|]
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
(operatorTag, tradeName, legalName, T.intercalate "," serverDomains, BI enabled, BI (storage smpRoles), BI (proxy smpRoles), BI (names smpRoles), BI (storage xftpRoles), BI (proxy xftpRoles))
opId <- insertedRowId db
pure op {operatorId = DBEntityId opId}
autoAcceptConditions op UsageConditions {conditionsCommit} now =
@@ -781,14 +781,14 @@ serverOperatorQuery :: Query
serverOperatorQuery =
[sql|
SELECT server_operator_id, server_operator_tag, trade_name, legal_name,
server_domains, enabled, smp_role_storage, smp_role_proxy, xftp_role_storage, xftp_role_proxy
server_domains, enabled, smp_role_storage, smp_role_proxy, smp_role_names, xftp_role_storage, xftp_role_proxy
FROM server_operators
|]
getServerOperators_ :: DB.Connection -> IO [ServerOperator]
getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, BoolInt) :. (BoolInt, BoolInt, BoolInt) :. (BoolInt, BoolInt) -> ServerOperator
toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI enabled) :. smpRoles' :. xftpRoles') =
ServerOperator
{ operatorId,
@@ -798,11 +798,13 @@ toServerOperator ((operatorId, operatorTag, tradeName, legalName, domains, BI en
serverDomains = T.splitOn "," domains,
conditionsAcceptance = CARequired Nothing,
enabled,
smpRoles = serverRoles smpRoles',
xftpRoles = serverRoles xftpRoles'
smpRoles = serverRolesSMP smpRoles',
xftpRoles = serverRolesXFTP xftpRoles'
}
where
serverRoles (BI storage, BI proxy) = ServerRoles {storage, proxy}
serverRolesSMP (BI storage, BI proxy, BI names) = ServerRoles {storage, proxy, names}
-- XFTP has no names role; the column is SMP-only.
serverRolesXFTP (BI storage, BI proxy) = ServerRoles {storage, proxy, names = False}
getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance
getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do
+3 -1
View File
@@ -161,6 +161,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260531_member_removed_at
import Simplex.Chat.Store.SQLite.Migrations.M20260603_simplex_name
import Simplex.Chat.Store.SQLite.Migrations.M20260604_simplex_name_profiles
import Simplex.Chat.Store.SQLite.Migrations.M20260606_simplex_name_verified
import Simplex.Chat.Store.SQLite.Migrations.M20260612_smp_role_names
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -321,7 +322,8 @@ schemaMigrations =
("20260531_member_removed_at", m20260531_member_removed_at, Just down_m20260531_member_removed_at),
("20260603_simplex_name", m20260603_simplex_name, Just down_m20260603_simplex_name),
("20260604_simplex_name_profiles", m20260604_simplex_name_profiles, Just down_m20260604_simplex_name_profiles),
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified)
("20260606_simplex_name_verified", m20260606_simplex_name_verified, Just down_m20260606_simplex_name_verified),
("20260612_smp_role_names", m20260612_smp_role_names, Just down_m20260612_smp_role_names)
]
-- | The list of migrations in ascending order by date
@@ -10,7 +10,7 @@ import Database.SQLite.Simple.QQ (sql)
-- resolves (via RSLV) to the link stored locally for the contact/group.
-- NULL means the claim is unverified and the UI should show an indicator.
-- The column is cleared back to NULL whenever the simplex_name claim changes
-- (updateContactProfileWithConflict / updateGroupProfileWithConflict).
-- (updateContactProfile / updateGroupProfile).
m20260606_simplex_name_verified :: Query
m20260606_simplex_name_verified =
[sql|
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260612_smp_role_names where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260612_smp_role_names :: Query
m20260612_smp_role_names =
[sql|
ALTER TABLE server_operators ADD COLUMN smp_role_names INTEGER NOT NULL DEFAULT 0;
UPDATE server_operators SET smp_role_names = 1 WHERE server_operator_tag = 'simplex';
|]
down_m20260612_smp_role_names :: Query
down_m20260612_smp_role_names =
[sql|
ALTER TABLE server_operators DROP COLUMN smp_role_names;
|]
@@ -687,6 +687,8 @@ CREATE TABLE server_operators(
xftp_role_proxy INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
smp_role_names INTEGER NOT NULL DEFAULT 0
) STRICT;
CREATE TABLE usage_conditions(
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
+29 -34
View File
@@ -433,52 +433,47 @@ createContact db user profile = do
-- | 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.
-- exclude self; pass Nothing for the pre-INSERT case. Newer-claim-wins matches
-- RSLV semantics: the latest broadcast is the canonical assignment. The partial
-- UNIQUE index on (user_id, simplex_name) requires the prior holder be cleared
-- before the new row can set the name.
--
-- Cross-table collision with group_profiles.simplex_name is structurally
-- impossible: strEncode SimplexNameInfo prefixes contact names with '@' and
-- group names with '#', so the encoded bytes stored in the column never
-- overlap between the two tables.
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO (Maybe ContactName)
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure Nothing
clearConflictingContactProfileSimplexName_ :: DB.Connection -> UserId -> Maybe ProfileId -> Maybe SimplexNameInfo -> IO ()
clearConflictingContactProfileSimplexName_ _ _ _ Nothing = pure ()
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)
DB.execute
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_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)
DB.execute
db
[sql|
UPDATE contact_profiles
SET simplex_name = NULL
WHERE user_id = ? AND simplex_name = ? AND contact_profile_id <> ?
|]
(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)
-- | Inserts a new contact and its profile, returning the new contactId. A
-- peer-claimed Profile.simplexName that collides with an existing row (the
-- partial UNIQUE index on contact_profiles.(user_id, simplex_name)) displaces
-- the prior holder's name — newer-claim-wins.
createContact_ :: DB.Connection -> User -> Profile -> Preferences -> Maybe (ACreatedConnLink, Maybe SharedMsgId) -> LocalAlias -> UTCTime -> Maybe SimplexNameInfo -> ExceptT StoreError IO ContactId
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
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 (?,?,?,?,?,?,?,?,?,?,?,?)"
@@ -489,7 +484,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, displaced)
pure $ Right contactId
newContactUserPrefs :: User -> Profile -> Preferences
newContactUserPrefs User {fullPreferences = FullPreferences {timedMessages = userTM}} Profile {preferences} =
+2 -2
View File
@@ -214,7 +214,7 @@ data Contact = Contact
-- | Timestamp of the most recent successful RSLV verification of the peer's
-- simplex_name claim against this contact's connection link. NULL means the
-- claim is unverified (UI should surface an indicator). Cleared back to NULL
-- whenever simplex_name changes in updateContactProfileWithConflict.
-- whenever simplex_name changes in updateContactProfile.
simplexNameVerifiedAt :: Maybe UTCTime
}
deriving (Eq, Show)
@@ -498,7 +498,7 @@ data GroupInfo = GroupInfo
groupKeys :: Maybe GroupKeys,
simplexName :: Maybe SimplexNameInfo,
-- | See 'Contact.simplexNameVerifiedAt'. Verified against the channel link
-- stored for the group; cleared by updateGroupProfileWithConflict.
-- stored for the group; cleared by updateGroupProfile.
simplexNameVerifiedAt :: Maybe UTCTime
}
deriving (Eq, Show)
+1 -23
View File
@@ -146,6 +146,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRContactRatchetSyncStarted {} -> ["connection synchronization started"]
CRGroupMemberRatchetSyncStarted {} -> ["connection synchronization started"]
CRConnectionVerified u verified code -> ttyUser u [plain $ if verified then "connection verified" else "connection not verified, current code is " <> code]
CRSimplexNameVerified u _chatRef ni verified -> ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> if verified then " verified" else " not verified"]
CRContactCode u ct code -> ttyUser u $ viewContactCode ct code testView
CRGroupMemberCode u g m code -> ttyUser u $ viewGroupMemberCode g m code testView
CRNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz testView
@@ -555,28 +556,6 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
TEContactVerificationReset u ct -> ttyUser u $ viewContactVerificationReset ct
TEGroupMemberVerificationReset u g m -> ttyUser u $ viewGroupMemberVerificationReset g m
CEvtCustomChatEvent u r -> ttyUser' u $ map plain $ T.lines r
CEvtSimplexNameConflict u ni entity claimedBy displacedFrom ->
ttyUser u
[ "simplex name "
<> plain (shortNameInfoStr ni)
<> " now claimed by "
<> plain (entityLabel entity claimedBy)
<> ", was "
<> plain (entityLabel entity displacedFrom)
]
where
entityLabel SNCEContact n = "@" <> n
entityLabel SNCEGroup n = "#" <> n
CEvtSimplexNameVerified u _chatRef ni _ts ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verified"]
CEvtSimplexNameVerifyFailed u _chatRef ni r ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " verification failed: " <> reason r]
where
reason SNVFLinkMismatch = "link mismatch"
reason SNVFNameNotRegistered = "name not registered"
reason (SNVFResolverError e) = "resolver error: " <> sShow e
CEvtSimplexNameUnverified u _chatRef ni ->
ttyUser u ["simplex name " <> plain (shortNameInfoStr ni) <> " is unverified"]
where
ttyUser :: User -> [StyledString] -> [StyledString]
ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss
@@ -2649,7 +2628,6 @@ viewChatError isCmd logLevel testView = \case
CEInvalidConnReq -> viewInvalidConnReq
CESimplexNameNotFound ni -> ["no contact or group with simplex name " <> plain (shortNameInfoStr ni)]
CESimplexNameUnprepared ni -> [plain (shortNameInfoStr ni) <> " is a known contact/group but has no link to reconnect via"]
CESimplexNameResolverUnavailable ni -> ["no SMP server in your list supports name resolution for " <> plain (shortNameInfoStr ni)]
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
[ plain $