mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-17 21:42:17 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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} =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 $
|
||||
|
||||
Reference in New Issue
Block a user