core: alerts with information about registering names (#7653)

* minimal api change

* embed status. reduce

* drop fake owner change

* no-valid-link fix

* add UX flow for name lookups

* diagram edits

* corrections

* update name lookup API

* manual renaming

* implement core + cli

* implement UI

* fix

* build iOS app blindly

* shrink diff

* fix review comments

* review iterations

* opuse 5.5 /goal iterations

* fixes EP review

* fixes

* fixes

* short goal iterations

* refactor core-UI API to NameWarning with logic in core

* diff cosmetics

* simplify and normalize diagram

* add excalidraw normalizer script and test git hook

* cosmetics

* commit wireframe collab doc to repo as README

* review round on UX coverage

* iterate on /goal

* iterate on /goal

* add tests

* iterate on /goal

* fix freshness bug with local chats: When core marks a chat verified for a name, it now first un-verifies the user's other chats of the same kind:
  - a contact name covers contacts and business chats;
  - a channel name covers channels.

* align app divergences

* docs fixes

* iterations on /goal

* align excalidraw

* align and simplify core/UI/UXcanvas

* 27 rounds of /goal

* review

* core: simplify connect plan with name lookup

* simplify

* cache names in a separate table

* cache names resolution

* wip

* all

* fix

* remove cache

* update plans

* simplify

* bot types

* change types

* simplify

* update bot types

* remove review

* update view, tests

* simplify "unverify"

* update query plan

* changes

* simplify

* test helpers

* simpler

* update query plans

* fix

* query plans

* query plans

* fixes

* query plans

---------

Co-authored-by: Alain Brenzikofer <alain@encointer.org>
Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 17:05:15 +01:00
1 parent 2f4a443dda
commit 7306f3aeef
39 files changed
+2751 -385

No files matched your search

+38 -7
View File
@@ -93,7 +93,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer)
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), USDCents, XFTPServer)
import Simplex.Messaging.Session (SessionVar)
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, TransportPeer (..), simplexMQVersion)
@@ -712,8 +712,8 @@ data ChatCommand
deriving (Show)
data PlanResolveMode
= PRMAllGroups -- resolve all known groups and all unknown chats
| PRMUnknown -- only resolve if chat is unknown (default)
= PRMAllGroups -- resolve names, links of known groups and of unknown chats
| PRMUnknown -- resolve names, and links of unknown chats (default)
| PRMNever -- do not resolve links and names, only do local search
deriving (Eq, Show)
@@ -987,6 +987,7 @@ data ChatEvent
| CEvtGroupLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember}
| CEvtBusinessLinkConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, fromContact :: Contact}
| CEvtContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact}
| CEvtNameMoved {user :: User, contactIds :: [ContactId], groupIds :: [GroupId]}
| CEvtGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
| CEvtContactDeletedByContact {user :: User, contact :: Contact}
| CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest, chat_ :: Maybe AChat}
@@ -1156,11 +1157,26 @@ data ChatDeleteMode
data ConnectionPlan
= CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan}
| CPContactAddress {contactAddressPlan :: ContactAddressPlan}
| CPGroupLink {groupLinkPlan :: GroupLinkPlan}
| CPContactAddress {contactAddressPlan :: ContactAddressPlan, nameChange :: Maybe NameChange}
| CPGroupLink {groupLinkPlan :: GroupLinkPlan, nameChange :: Maybe NameChange}
| CPError {chatError :: ChatError}
deriving (Show)
data NameChange
= NCLapsed {nameWarning :: NameWarning}
| NCMoved {knownChat :: AChatInfo}
deriving (Show)
data NameWarning
= NWExpired {expiredAt :: UTCTime, graceUntil :: Maybe UTCTime}
| NWAvailable {price :: NamePrice}
| NWReservedForCommunity
| NWNotRegistered
deriving (Eq, Show)
data NamePrice = NamePrice {amount :: USDCents, years :: Int}
deriving (Eq, Show)
data InvitationLinkPlan
= ILPOk {contactSLinkData_ :: Maybe ContactShortLinkData, ownerVerification :: Maybe OwnerVerification}
| ILPOwnLink
@@ -1219,13 +1235,15 @@ connectionPlanProceed = \case
ILPOk {} -> True
ILPOwnLink -> True
_ -> False
CPContactAddress cap -> case cap of
CPContactAddress _ (Just NCLapsed {}) -> False
CPContactAddress cap _ -> case cap of
CAPOk {} -> True
CAPOwnLink -> True
CAPConnectingConfirmReconnect -> True
CAPContactViaAddress _ -> True
_ -> False
CPGroupLink glp -> case glp of
CPGroupLink _ (Just NCLapsed {}) -> False
CPGroupLink glp _ -> case glp of
GLPOk {} -> True
GLPOwnLink _ -> True
GLPConnectingConfirmReconnect -> True
@@ -1234,6 +1252,12 @@ connectionPlanProceed = \case
_ -> False
CPError _ -> True
connectionPlanOwnLink :: ConnectionPlan -> Bool
connectionPlanOwnLink = \case
CPContactAddress CAPOwnLink _ -> True
CPGroupLink GLPOwnLink {} _ -> True
_ -> False
data ForwardConfirmation
= FCFilesNotAccepted {fileIds :: [FileTransferId]}
| FCFilesInProgress {filesCount :: Int}
@@ -1482,6 +1506,7 @@ data ChatError
data SimplexDomainError
= SDENoValidLink -- the name's record has no usable contact/channel link
| SDEUnknownDomain -- the resolved link's profile has no name, or a different name
| SDENameWarning {nameWarning :: NameWarning}
deriving (Eq, Show)
data BadgeRedeemError
@@ -1849,6 +1874,10 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation)
$(JQ.deriveJSON defaultJSON ''NamePrice)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "NW") ''NameWarning)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SDE") ''SimplexDomainError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "BRE") ''BadgeRedeemError)
@@ -1865,6 +1894,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "NC") ''NameChange)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan)
$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig)
+171 -65
View File
@@ -120,9 +120,10 @@ import Simplex.Messaging.Crypto.Ratchet (E2ERatchetParamsUri (..), InitialKeys (
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (base64P)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (NAME), MsgFlags (..), NameRecord (..), NameRegistration (..), NameResponse (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (NAME), MsgFlags (..), NamePricing (..), NameRecord (..), NameRegistration (..), NameReservedReason (..), NameResponse (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), USDCents (..), UserProtocol, userProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.SystemTime (SystemSeconds, getSystemSeconds, roundedToUTCTime)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
import Simplex.Messaging.Util
@@ -1963,7 +1964,7 @@ processChatCommand cxt nm = \case
(_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks}), _) <- getShortLinkConnReq' nm user sLnk
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
gInfo' <- case groupSLinkData_ of
Just sLinkData -> fst <$> updateGroupFromLinkData user gInfo sLinkData Nothing
Just sLinkData -> (\(g, _, _) -> g) <$> updateGroupFromLinkData user gInfo sLinkData Nothing
_ -> pure gInfo
when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $
withGroupLock "syncSubscriberRelays" groupId $
@@ -2186,7 +2187,8 @@ processChatCommand cxt nm = \case
pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan
APIConnectPlan _ Nothing _ _ -> throwChatError CEInvalidConnReq
APIPrepareContact userId accLink verifiedDomain contactSLinkData -> withUserId userId $ \user -> do
let ContactShortLinkData {profile, message, business} = contactSLinkData
let ContactShortLinkData {profile = profile@Profile {contactDomain}, message, business} = contactSLinkData
domain_ = if verifiedDomain == (claimDomain <$> contactDomain) then verifiedDomain else Nothing
welcomeSharedMsgId <- forM message $ \_ -> getSharedMsgId
case accLink of
ACCL SCMContact ccLink
@@ -2195,7 +2197,10 @@ processChatCommand cxt nm = \case
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
groupProfile = businessGroupProfile profile groupPreferences
gVar <- asks random
(gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing verifiedDomain
(gInfo, hostMember_, moved) <- withStore $ \db -> do
(g, hm_) <- createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing domain_
(g, hm_,) <$> if isJust domain_ then liftIO (setOtherNameChatsMoved db user (GroupChat g Nothing)) else pure ([], [])
nameMovedToView user moved
hostMember <- maybe (throwCmdError "no host member") pure hostMember_
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDGroupRcv gInfo Nothing hostMember
@@ -2208,7 +2213,10 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTGroup chat
ACCL _ (CCLink cReq sLnk) -> do
ct <- withStore $ \db -> createPreparedContact db cxt user (linkPresHeader <$> sLnk) profile accLink welcomeSharedMsgId (True <$ verifiedDomain)
(ct, moved) <- withStore $ \db -> do
ct <- createPreparedContact db cxt user (linkPresHeader <$> sLnk) profile accLink welcomeSharedMsgId (True <$ domain_)
(ct,) <$> if isJust domain_ then liftIO (setOtherNameChatsMoved db user (DirectChat ct)) else pure ([], [])
nameMovedToView user moved
void $ createChatItem user (CDDirectSnd ct) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = CDDirectRcv ct
createItem sharedMsgId content = createChatItem user cd False content sharedMsgId Nothing Nothing
@@ -2221,9 +2229,10 @@ processChatCommand cxt nm = \case
_ -> Chat cInfo [] emptyChatStats
pure $ CRNewPreparedChat user $ AChat SCTDirect chat
APIPrepareGroup userId ccLink direct verifiedDomain groupSLinkData -> withUserId userId $ \user -> do
let GroupShortLinkData {groupProfile = GroupProfile {description}} = groupSLinkData
let GroupShortLinkData {groupProfile = gp@GroupProfile {description}} = groupSLinkData
domain_ = if verifiedDomain == groupClaim gp then verifiedDomain else Nothing
welcomeSharedMsgId <- forM description $ \_ -> getSharedMsgId
(gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId verifiedDomain
(gInfo, hostMember_) <- preparedGroupFromLink user ccLink direct groupSLinkData welcomeSharedMsgId domain_
void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing Nothing (Just epochStart)
let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_
cInfo = GroupChat gInfo Nothing
@@ -2238,7 +2247,10 @@ processChatCommand cxt nm = \case
when (isNothing preparedContact) $ throwCmdError "contact doesn't have link to connect"
when (isJust $ contactConn ct) $ throwCmdError "contact already has connection"
newUser <- privateGetUser newUserId
ct' <- withFastStore $ \db -> updatePreparedContactUser db cxt user ct newUser
(ct', moved) <- withFastStore $ \db -> do
ct'@Contact {profile = LocalProfile {contactDomainVerified}} <- updatePreparedContactUser db cxt user ct newUser
(ct',) <$> if contactDomainVerified == Just DVVerified then liftIO (setOtherNameChatsMoved db newUser (DirectChat ct')) else pure ([], [])
nameMovedToView newUser moved
-- create changed feature items (new user may have different preferences)
lift $ createContactChangedFeatureItems user ct ct'
pure $ CRContactUserChanged user ct newUser ct'
@@ -2253,7 +2265,10 @@ processChatCommand cxt nm = \case
when (isJust $ memberConn hostMember) $ throwCmdError "host member already has connection"
pure $ Just hostMember
newUser <- privateGetUser newUserId
gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db cxt user gInfo hostMember_ newUser
(gInfo', moved) <- withFastStore $ \db -> do
gInfo'@GroupInfo {groupDomainVerified} <- updatePreparedGroupUser db cxt user gInfo hostMember_ newUser
(gInfo',) <$> if groupDomainVerified == Just DVVerified then liftIO (setOtherNameChatsMoved db newUser (GroupChat gInfo' Nothing)) else pure ([], [])
nameMovedToView newUser moved
pure $ CRGroupUserChanged user gInfo newUser gInfo'
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do
ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId
@@ -2417,7 +2432,12 @@ processChatCommand cxt nm = \case
let connLink_ = preparedContact >>= \PreparedContact {connLinkToConnect = ACCL m (CCLink _ sLnk_)} -> ACSL m <$> sLnk_
domain <- maybe (throwCmdError "contact has no name to verify") pure contactDomain
(verified, reason) <- verifyEntityDomain user nm NTContact domain connLink_
ct' <- maybe (pure ct) (\v -> withFastStore' $ \db -> setContactDomainVerified db user ct v) verified
(ct', moved) <- case verified of
Just v -> withFastStore' $ \db -> do
ct' <- setContactDomainVerified db user ct v
(ct',) <$> if v then setOtherNameChatsMoved db user (DirectChat ct') else pure ([], [])
Nothing -> pure (ct, ([], []))
nameMovedToView user moved
pure $ CRContactDomainVerified user ct' reason
APIVerifyGroupDomain groupId -> withUser $ \user -> do
g@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
@@ -2431,7 +2451,10 @@ processChatCommand cxt nm = \case
| otherwise -> pure (False, Just "the name does not resolve to the link in the group profile")
Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered")
Left e -> throwError e
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified
(g', moved) <- withFastStore' $ \db -> do
g' <- setGroupDomainVerified db user g verified
(g',) <$> if verified then setOtherNameChatsMoved db user (GroupChat g' Nothing) else pure ([], [])
nameMovedToView user moved
pure $ CRGroupDomainVerified user g' reason
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}, groupDirectInv} <- withFastStore $ \db -> getContact db cxt user contactId
@@ -2449,7 +2472,7 @@ processChatCommand cxt nm = \case
toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct')
throwError e
ConnectSimplex incognito -> withUser $ \user -> do
plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing))
plan <- contactRequestPlan user adminContactReq Nothing Nothing `catchAllErrors` const (pure $ CPContactAddress (CAPOk Nothing Nothing) Nothing)
connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) Nothing Nothing plan
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm
ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing
@@ -4169,9 +4192,14 @@ processChatCommand cxt nm = \case
checkGroupProfileSize p'
when (useRelays' gInfo && isJust (ma' >>= review)) $ throwCmdError "Admission review is not supported in channels"
-- updateGroupProfile clears domain verification; re-set it when the caller already re-resolved the name
gInfo' <- withStore $ \db -> do
(gInfo', moved) <- withStore $ \db -> do
g <- updateGroupProfile db user gInfo p'
if domainVerified then liftIO $ setGroupDomainVerified db user g True else pure g
if domainVerified
then liftIO $ do
g' <- setGroupDomainVerified db user g True
(g',) <$> setOtherNameChatsMoved db user (GroupChat g' Nothing)
else pure (g, ([], []))
nameMovedToView user moved
msg <- case businessChat of
Just BusinessChatInfo {businessId} -> do
ms <- withStore' $ \db -> getGroupMembers db cxt user gInfo'
@@ -4502,7 +4530,7 @@ processChatCommand cxt nm = \case
-- local search only: look up #d then @d in the store, without online name resolution
| resolveMode == PRMNever -> connectPlanNoName $ ChatError CENotResolvedLocally
| otherwise ->
tryAllErrors (resolveNameRecord user nm d) >>= \case
tryAllErrors (resolveConnectableName user nm d) >>= \case
Right nr
| isJust (firstNameLink CCTChannel (nrSimplexChannel nr)) ->
(addOther nr <$> connectPlanName NTPublicGroup (Right nr)) `catchAllErrors` \e ->
@@ -4533,10 +4561,12 @@ processChatCommand cxt nm = \case
(\(l, p) -> (l, simplexName_, Nothing, p)) <$> case ctType of
CCTContact ->
knownLinkPlans >>= \case
Just r -> pure r
Just r -> knownNamePlan r contactLinkPlan
Nothing -> do
when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally
l' <- resolveSLink
resolveSLink >>= contactLinkPlan Nothing
where
contactLinkPlan knownChat_ l' = do
(FixedLinkData {rootKey}, cData, cReq) <- getShortLinkConnReq nm user l'
let presHeader = linkPresHeader l'
contactSLinkData_ <- mapM (linkDataBadge presHeader) =<< liftIO (decodeLinkUserData cData)
@@ -4545,33 +4575,39 @@ processChatCommand cxt nm = \case
planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
refreshContact ct' = case (planDomain, linkProfile_) of
(Just _, Just p) -> updateContactFromLinkData user ct' presHeader p
_ -> pure ct'
_ -> pure (ct', ([], []))
knownMoved moved = NCMoved . movedChat moved <$> knownChat_
forM_ planDomain $ \nameDomain ->
unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case
Just ct' | not (contactDeleted ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
(ct'', moved) <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'') (knownMoved moved))
_ -> do
let ContactLinkData _ UserContactData {owners} = cData
ov = verifyLinkOwner rootKey owners l' sig_
plan <- contactRequestPlan user cReq contactSLinkData_ ov
plan <- setNameChange (NCMoved <$> knownChat_) <$> contactRequestPlan user cReq contactSLinkData_ ov
case plan of
CPContactAddress (CAPKnown ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPKnown ct''))
CPContactAddress (CAPContactViaAddress ct') -> do
ct'' <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct''))
CPContactAddress (CAPKnown ct') _ -> do
(ct'', moved) <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPKnown ct'') (knownMoved moved))
CPContactAddress (CAPContactViaAddress ct') _ -> do
(ct'', moved) <- refreshContact ct'
pure (con l' cReq, CPContactAddress (CAPContactViaAddress ct'') (knownMoved moved))
CPGroupLink (GLPKnown g@GroupInfo {businessChat = Just _} u o os) _ | Just d <- planDomain -> do
(g', moved) <- withFastStore' $ \db -> do
g' <- setPreparedGroupDomain db user g d
(g',) <$> setOtherNameChatsMoved db user (GroupChat g' Nothing)
nameMovedToView user moved
pure (con l' cReq, CPGroupLink (GLPKnown g' u o os) (knownMoved moved))
_ -> pure (con l' cReq, plan)
where
knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan))
knownLinkPlans = withFastStore $ \db ->
liftIO (getUserContactLinkViaTarget db user nl') >>= \case
Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink)
Just UserContactLink {connLinkContact} -> pure $ Just (ACCL SCMContact connLinkContact, CPContactAddress CAPOwnLink Nothing)
Nothing ->
getContactToConnect db cxt user nl' >>= \case
Just (ccl, ct') -> pure $ if contactDeleted ct' then Nothing else Just (ACCL SCMContact ccl, CPContactAddress (CAPKnown ct'))
Just (ccl, ct') -> pure $ if contactDeleted ct' then Nothing else Just (ACCL SCMContact ccl, CPContactAddress (CAPKnown ct') Nothing)
Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl'
CCTGroup -> groupShortLinkPlan
CCTChannel -> groupShortLinkPlan
@@ -4588,21 +4624,26 @@ processChatCommand cxt nm = \case
CTLink l' -> pure l'
CTName n -> serverShortLink <$> resolveNameLink n
con l' cReq = ACCL SCMContact $ CCLink cReq (Just l')
gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g False Nothing (ListDef [])))
gPlan (ccl, g) = if memberRemoved (membership g) then Nothing else Just (ACCL SCMContact ccl, CPGroupLink (GLPKnown g False Nothing (ListDef [])) Nothing)
groupShortLinkPlan :: CM (ACreatedConnLink, ConnectionPlan)
groupShortLinkPlan =
knownLinkPlans >>= \case
Just (_, CPGroupLink (GLPKnown g _ _ _))
| resolveMode == PRMAllGroups -> resolveKnownGroup g
Just r -> pure r
Just (_, CPGroupLink (GLPKnown g _ _ _) _)
| resolveMode == PRMAllGroups, isNothing simplexName_ -> resolveKnownGroup g
Just r -> knownNamePlan r groupLinkPlan
Nothing -> do
when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally
l' <- resolveSLink
resolveSLink >>= groupLinkPlan Nothing
where
groupLinkPlan knownChat_ l' = do
(fd, cData@(ContactLinkData _ UserContactData {direct, owners, relays}), cReq) <- getShortLinkConnReq' nm user l'
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
if
| not direct && unsupportedGroupType groupSLinkData_ -> pure (con l' cReq, CPGroupLink (GLPUpdateRequired groupSLinkData_))
| not direct && null relays -> pure (con l' cReq, CPGroupLink (GLPNoRelays groupSLinkData_))
let planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
nc_ = NCMoved <$> knownChat_
knownMoved moved = NCMoved . movedChat moved <$> knownChat_
plan <- if
| not direct && unsupportedGroupType groupSLinkData_ -> pure $ CPGroupLink (GLPUpdateRequired groupSLinkData_) nc_
| not direct && null relays -> pure $ CPGroupLink (GLPNoRelays groupSLinkData_) nc_
| otherwise -> do
let FixedLinkData {linkEntityId, rootKey} = fd
linkInfo = GroupShortLinkInfo {direct, groupRelays = relays, publicGroupId = B64UrlByteString <$> linkEntityId}
@@ -4614,32 +4655,30 @@ processChatCommand cxt nm = \case
_ -> throwChatError CEInvalidConnReq
let ov = verifyLinkOwner rootKey owners l' sig_
glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners
planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
plan0 <- groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov glOwners
plan0 <- setNameChange nc_ <$> groupJoinRequestPlan user cReq (Just linkInfo) groupSLinkData_ ov glOwners
-- a joined channel is found by link but not by name (its domain is not verified locally,
-- e.g. an un-upgraded relay dropped the claim); refresh its profile from the fresh link
-- data and mark it verified, so the check below passes and future by-name lookups match
plan <- case (planDomain, plan0, groupSLinkData_) of
(Just nameDomain, CPGroupLink (GLPKnown g u o os), Just sLinkData) ->
(\(g', _) -> CPGroupLink (GLPKnown g' u o os)) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain)
case (planDomain, plan0, groupSLinkData_) of
(Just nameDomain, CPGroupLink (GLPKnown g u o os) _, Just sLinkData) ->
(\(g', _, moved) -> CPGroupLink (GLPKnown g' u o os) (knownMoved moved)) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain)
_ -> pure plan0
forM_ planDomain $ \nameDomain ->
let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of
CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _) -> Just groupProfile
CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) -> Just groupProfile
CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) -> Just groupProfile
CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) -> Just groupProfile
_ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_
in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
pure (con l' cReq, plan)
where
forM_ planDomain $ \nameDomain ->
let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of
CPGroupLink (GLPOk _ (Just GroupShortLinkData {groupProfile}) _) _ -> Just groupProfile
CPGroupLink (GLPKnown GroupInfo {groupProfile} _ _ _) _ -> Just groupProfile
CPGroupLink (GLPOwnLink GroupInfo {groupProfile}) _ -> Just groupProfile
CPGroupLink (GLPConnectingProhibit (Just GroupInfo {groupProfile})) _ -> Just groupProfile
_ -> (\GroupShortLinkData {groupProfile} -> groupProfile) <$> groupSLinkData_
in unless (domain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
pure (con l' cReq, plan)
unsupportedGroupType = \case
Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel
_ -> False
knownLinkPlans :: CM (Maybe (ACreatedConnLink, ConnectionPlan))
knownLinkPlans = withFastStore $ \db ->
liftIO (getGroupInfoViaUserTarget db cxt user nl') >>= \case
Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g))
Just (ccl, g) -> pure $ Just (ACCL SCMContact ccl, CPGroupLink (GLPOwnLink g) Nothing)
Nothing -> (gPlan =<<) <$> getGroupToConnect db cxt user nl'
resolveKnownGroup g = do
l' <- resolveSLink
@@ -4647,14 +4686,56 @@ processChatCommand cxt nm = \case
groupSLinkData_ <- liftIO $ decodeLinkUserData cData
let ov = verifyLinkOwner rk owners l' sig_
glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners
(g', updated) <- case groupSLinkData_ of
(g', updated, _) <- case groupSLinkData_ of
Just sLinkData -> updateGroupFromLinkData user g sLinkData Nothing
_ -> pure (g, False)
pure (con l' cReq, CPGroupLink (GLPKnown g' updated ov (ListDef glOwners)))
_ -> pure (g, False, ([], []))
pure (con l' cReq, CPGroupLink (GLPKnown g' updated ov (ListDef glOwners)) Nothing)
movedChat :: ([ContactId], [GroupId]) -> AChatInfo -> AChatInfo
movedChat (ctIds, gIds) (AChatInfo t c) = AChatInfo t $ case c of
DirectChat ct'@Contact {contactId, profile = p} | contactId `elem` ctIds -> DirectChat ct' {profile = p {contactDomainVerified = Just DVMoved}}
GroupChat g@GroupInfo {groupId} s | groupId `elem` gIds -> GroupChat g {groupDomainVerified = Just DVMoved} s
_ -> c
knownNamePlan :: (ACreatedConnLink, ConnectionPlan) -> (Maybe AChatInfo -> ShortLinkContact -> CM (ACreatedConnLink, ConnectionPlan)) -> CM (ACreatedConnLink, ConnectionPlan)
knownNamePlan r@(l, plan) linkPlan
| resolveMode == PRMNever || isNothing simplexName_ = pure r
| otherwise =
tryAllErrors resolveSLink >>= \case
Right l'
| ACCL SCMContact (CCLink _ (Just sl)) <- l, sameShortLinkContact sl l' -> pure r
| otherwise -> (knownChatLink <$> linkPlan knownChat_ l') `catchAllErrors` knownPlanOnError
Left (ChatError (CESimplexDomainNotReady _ (SDENameWarning w))) | w /= NWNotRegistered -> pure (l, setNameChange (Just $ NCLapsed w) plan)
Left e -> knownPlanOnError e
where
knownPlanOnError = \case
e@ChatErrorAgent {agentError} | temporaryOrHostError agentError -> throwError e
e@ChatErrorStore {} -> throwError e
e -> r <$ logWarn ("known chat plan used, name lookup error: " <> tshow e)
knownChat_ = planChat plan
knownChatLink r'@(_, p) = if isKnownChat p then (l, setNameChange Nothing p) else r'
isKnownChat p = isJust knownChat_ && refOf (planChat p) == refOf knownChat_
refOf = (>>= \(AChatInfo _ c) -> chatInfoToRef c)
setNameChange :: Maybe NameChange -> ConnectionPlan -> ConnectionPlan
setNameChange nc_ = \case
CPContactAddress cap _ -> CPContactAddress cap nc_
CPGroupLink glp _ -> CPGroupLink glp nc_
p -> p
planChat :: ConnectionPlan -> Maybe AChatInfo
planChat = \case
CPContactAddress cap _ -> AChatInfo SCTDirect . DirectChat <$> case cap of
CAPKnown ct' -> Just ct'
CAPContactViaAddress ct' -> Just ct'
CAPConnectingProhibit ct' -> Just ct'
_ -> Nothing
CPGroupLink glp _ -> (\g -> AChatInfo SCTGroup $ GroupChat g Nothing) <$> case glp of
GLPKnown g _ _ _ -> Just g
GLPOwnLink g -> Just g
GLPConnectingProhibit g_ -> g_
_ -> Nothing
_ -> Nothing
-- resolve a name to its first contact/channel short link
resolveNameLink :: SimplexNameInfo -> CM (ConnShortLink 'CMContact)
resolveNameLink SimplexNameInfo {nameType, nameDomain} = do
NameRecord {nrSimplexContact, nrSimplexChannel} <- maybe (resolveNameRecord user nm nameDomain) (ExceptT . pure) nameRec
NameRecord {nrSimplexContact, nrSimplexChannel} <- maybe (resolveConnectableName user nm nameDomain) (ExceptT . pure) nameRec
let (candidates, ctType') = case nameType of
NTContact -> (nrSimplexContact, CCTContact)
NTPublicGroup -> (nrSimplexChannel, CCTChannel)
@@ -4664,10 +4745,10 @@ processChatCommand cxt nm = \case
| connectionPlanProceed plan = do
case plan of CPError e -> eToView e; _ -> pure ()
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
CPContactAddress (CAPContactViaAddress Contact {contactId}) _ ->
processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId
CPContactAddress (CAPOk (Just sld) _) | isJust vName -> connectContactViaName sld
CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _)
CPContactAddress (CAPOk (Just sld) _) _ | isJust vName -> connectContactViaName sld
CPGroupLink (GLPOk (Just GroupShortLinkInfo {direct = False}) (Just gld) _) _
| ACCL SCMContact ccl <- ccLink -> joinChannelViaRelays ccl gld
_ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink
| otherwise = pure $ CRConnectionPlan user ccLink planSimplexName otherSimplexName plan
@@ -4726,7 +4807,7 @@ processChatCommand cxt nm = \case
contactRequestPlan user cReq cld ov = do
let cReqSchemas = contactCReqSchemas cReq
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
plan p = pure $ CPContactAddress p
plan p = pure $ CPContactAddress p Nothing
withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case
Just _ -> plan $ CAPOwnLink
Nothing ->
@@ -4749,7 +4830,7 @@ processChatCommand cxt nm = \case
groupJoinRequestPlan user cReq linkInfo gld ov glOwners = do
let cReqSchemas = contactCReqSchemas cReq
cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas
plan p = pure $ CPGroupLink p
plan p = pure $ CPGroupLink p Nothing
withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas) >>= \case
Just g -> plan $ GLPOwnLink g
Nothing -> do
@@ -4772,7 +4853,7 @@ processChatCommand cxt nm = \case
| memberActive membership = plan $ GLPKnown gInfo False ov (ListDef glOwners)
| otherwise = plan $ GLPOk linkInfo gld ov
where
plan p = pure $ CPGroupLink p
plan p = pure $ CPGroupLink p Nothing
contactCReqSchemas :: ConnReqContact -> (ConnReqContact, ConnReqContact)
contactCReqSchemas (CRContactUri crData e2e) =
( CRContactUri crData {crScheme = SSSimplex} e2e,
@@ -5154,7 +5235,11 @@ processChatCommand cxt nm = \case
useRelays = not direct
subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember
gVar <- asks random
withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ verifiedDomain
(gInfo, hostMember_, moved) <- withStore $ \db -> do
(g, hm_) <- createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ verifiedDomain
(g, hm_,) <$> if isJust verifiedDomain then liftIO (setOtherNameChatsMoved db user (GroupChat g Nothing)) else pure ([], [])
nameMovedToView user moved
pure (gInfo, hostMember_)
getSharedMsgId :: CM SharedMsgId
getSharedMsgId = do
@@ -5179,6 +5264,27 @@ resolveNameRecord user nm domain = do
NRRegistered {nameRecord} -> pure nameRecord
_ -> throwError $ chatErrorAgent $ SMP "" (NAME SMP.NOT_FOUND)
resolveConnectableName :: User -> NetworkRequestMode -> SimplexDomain -> CM NameRecord
resolveConnectableName user nm domain = do
NameResponse {registration} <- withAgent $ \a -> resolveSimplexName a nm (aUserId user) domain
now <- liftIO getSystemSeconds
either (throwChatError . CESimplexDomainNotReady domain . SDENameWarning) pure $ nameRecordOrWarning now domain registration
nameRecordOrWarning :: SystemSeconds -> SimplexDomain -> NameRegistration -> Either NameWarning NameRecord
nameRecordOrWarning now SimplexDomain {domain} = \case
NRRegistered {expires = Just expires, graceUntil}
| expires < now -> Left $ NWExpired (roundedToUTCTime expires) (roundedToUTCTime <$> mfilter (now <=) graceUntil)
NRRegistered {nameRecord} -> Right nameRecord
NRAvailable {pricing = NamePricing {registrationPrices, basePrice, minLabelLength}}
| labelLength >= minLabelLength ->
let USDCents perYear = M.findWithDefault basePrice labelLength registrationPrices
years = 2
in Left $ NWAvailable NamePrice {amount = USDCents (perYear * fromIntegral years), years}
NRReserved {reservedReason = NRRCommunity} -> Left NWReservedForCommunity
_ -> Left NWNotRegistered
where
labelLength = T.length domain
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text)
verifyEntityDomain user nm nameType SimplexDomainClaim {domain = StrJSON domain, proof = proof_} connLink_ = case (proof_, connLink_) of
(Nothing, _) -> pure (Nothing, Just "no name proof to verify")
+39 -11
View File
@@ -1631,40 +1631,68 @@ updatePublicGroupData user gInfo gks
| otherwise = pure gInfo
-- must not resolve names here: a background link-data refresh would leak channel membership to the resolver
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> Maybe SimplexDomain -> CM (GroupInfo, Bool)
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> Maybe SimplexDomain -> CM (GroupInfo, Bool, ([ContactId], [GroupId]))
updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} resolvedDomain_
| profileChanged || countChanged || verifyResolved = do
cxt <- chatStoreCxt
withStore $ \db -> do
(gInfo', moved) <- withStore $ \db -> do
g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo
g' <- case publicGroupData of
Just PublicGroupData {publicMemberCount} | countChanged ->
setPublicMemberCount db cxt user g publicMemberCount
_ -> pure g
g'' <- if verifyResolved then liftIO $ setGroupDomainVerified db user g' True else pure g'
pure (g'', profileChanged)
| otherwise = pure (gInfo, False)
if verifyResolved
then liftIO $ do
g'' <- setGroupDomainVerified db user g' True
(g'',) <$> setOtherNameChatsMoved db user (GroupChat g'' Nothing)
else pure (g', ([], []))
nameMovedToView user moved
pure (gInfo', profileChanged, moved)
| otherwise = pure (gInfo, False, ([], []))
where
profileChanged = p /= groupProfile
countChanged = case publicGroupData of
Just PublicGroupData {publicMemberCount} -> Just publicMemberCount /= localCount
_ -> False
groupClaim GroupProfile {publicGroup} = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)
newClaim = groupClaim groupProfile
verifyResolved = isJust resolvedDomain_ && resolvedDomain_ == newClaim
updateContactFromLinkData :: User -> Contact -> ProofPresHeader -> Profile -> CM Contact
groupClaim :: GroupProfile -> Maybe SimplexDomain
groupClaim GroupProfile {publicGroup} = claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)
updateContactFromLinkData :: User -> Contact -> ProofPresHeader -> Profile -> CM (Contact, ([ContactId], [GroupId]))
updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} presHeader linkProfile@Profile {contactDomain = newClaim}
| profileChanged || verifyChanged = do
cxt <- chatStoreCxt
withFastStore $ \db -> do
r@(_, moved) <- withFastStore $ \db -> do
ct' <- updateContactProfile db cxt user ct (Just presHeader) linkProfile
if verifyChanged then liftIO $ setContactDomainVerified db user ct' True else pure ct'
| otherwise = pure ct
if verifyChanged
then liftIO $ do
ct'' <- setContactDomainVerified db user ct' True
(ct'',) <$> setOtherNameChatsMoved db user (DirectChat ct'')
else pure (ct', ([], []))
nameMovedToView user moved
pure r
| otherwise = pure (ct, ([], []))
where
profileChanged = fromLocalProfile profile /= linkProfile
claimChanged = (claimDomain <$> prevClaim) /= (claimDomain <$> newClaim)
verifyChanged = contactDomainVerified /= Just True || claimChanged
verifyChanged = contactDomainVerified /= Just DVVerified || claimChanged
setOtherNameChatsMoved :: DB.Connection -> User -> ChatInfo c -> IO ([ContactId], [GroupId])
setOtherNameChatsMoved db user = \case
DirectChat Contact {contactId, profile = LocalProfile {contactDomain = Just d}} -> setMoved (SimplexNameInfo NTContact $ claimDomain d) (Just contactId) Nothing
GroupChat GroupInfo {groupId, businessChat = Just BusinessChatInfo {businessDomain = Just d}} _ -> setMoved (SimplexNameInfo NTContact $ claimDomain d) Nothing (Just groupId)
GroupChat GroupInfo {groupId, businessChat = Nothing, groupProfile} _ | Just d <- groupClaim groupProfile -> setMoved (SimplexNameInfo NTPublicGroup d) Nothing (Just groupId)
_ -> pure ([], [])
where
setMoved ni@SimplexNameInfo {nameType, nameDomain} ctId_ gId_ =
(,)
<$> (if nameType == NTContact then setNameContactsMoved db user nameDomain ctId_ else pure [])
<*> setNameGroupsMoved db user ni gId_
nameMovedToView :: User -> ([ContactId], [GroupId]) -> CM ()
nameMovedToView user (ctIds, gIds) = unless (null ctIds && null gIds) $ toView $ CEvtNameMoved user ctIds gIds
-- TODO [relays] owner: set owners on updating link data (multi-owner)
groupLinkData :: GroupInfoKeys -> GroupLink -> [GroupRelay] -> (UserConnLinkData 'CMContact, CRClientData)
+18 -2
View File
@@ -52,6 +52,7 @@ module Simplex.Chat.Store.Direct
getContactIdByName,
updateContactProfile,
setContactDomainVerified,
setNameContactsMoved,
updateContactUserPreferences,
updateContactAlias,
updateContactConnectionAlias,
@@ -115,7 +116,7 @@ import Simplex.Chat.Names (SimplexDomainClaim (..))
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), SConnectionMode (..), SimplexNameInfo (..), UserId)
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionModeI (..), ConnectionRequestUri, CreatedConnLink (..), SConnectionMode (..), SimplexDomain, SimplexNameInfo (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
@@ -599,7 +600,22 @@ setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} ve
WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?)
|]
(BI verified, userId, contactId)
pure (ct {profile = p {contactDomainVerified = Just verified}} :: Contact)
pure (ct {profile = p {contactDomainVerified = Just $ if verified then DVVerified else DVFailed}} :: Contact)
setNameContactsMoved :: DB.Connection -> User -> SimplexDomain -> Maybe ContactId -> IO [ContactId]
setNameContactsMoved db User {userId} domain exceptContactId_ = do
cts <-
DB.query
db
[sql|
SELECT ct.contact_id, ct.contact_profile_id FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
WHERE ct.user_id = ? AND ct.contact_id IS DISTINCT FROM ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1
|]
(userId, exceptContactId_, domain)
forM_ cts $ \(_ :: ContactId, profileId :: ProfileId) ->
DB.execute db "UPDATE contact_profiles SET contact_domain_verified = 2 WHERE contact_profile_id = ?" (Only profileId)
pure $ map fst cts
updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact
updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do
+25 -5
View File
@@ -47,6 +47,8 @@ module Simplex.Chat.Store.Groups
getGroupInfoByGroupLinkHash,
updateGroupProfile,
setGroupDomainVerified,
setNameGroupsMoved,
setPreparedGroupDomain,
updateGroupPreferences,
updateGroupProfileFromMember,
getGroupIdByName,
@@ -234,7 +236,7 @@ import qualified Data.Text as T
import Data.Time.Clock (NominalDiffTime, UTCTime (..), addUTCTime, getCurrentTime)
import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Badges (BadgeProof, BadgeRow, ProofPresHeader, acceptedBadge, badgeProofToRow, badgeToRow, sameBadgeProof, verifyBadge)
import Simplex.Chat.Names (SimplexDomainClaim (..))
import Simplex.Chat.Names (SimplexDomainClaim (..), mkDomainClaim)
import Simplex.Chat.Messages
import Simplex.Chat.Operators
import Simplex.Chat.Protocol hiding (Binary)
@@ -2787,12 +2789,30 @@ setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified = do
db
"UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?"
(BI verified, userId, groupId)
pure g {groupDomainVerified = Just verified}
pure g {groupDomainVerified = Just $ if verified then DVVerified else DVFailed}
setNameGroupsMoved :: DB.Connection -> User -> SimplexNameInfo -> Maybe GroupId -> IO [GroupId]
setNameGroupsMoved db User {userId} SimplexNameInfo {nameType, nameDomain} exceptGroupId_ =
map fromOnly <$> DB.query
db
( [sql|
UPDATE groups SET group_domain_verified = 2
WHERE user_id = ? AND group_domain_verified = 1 AND group_id IS DISTINCT FROM ?
AND group_profile_id IN (SELECT group_profile_id FROM group_profiles WHERE user_id = ? AND group_domain = ?)
|]
<> businessCond
<> " RETURNING group_id"
)
(userId, exceptGroupId_, userId, nameDomain)
where
businessCond = case nameType of
NTContact -> " AND business_chat IS NOT NULL"
NTPublicGroup -> " AND business_chat IS NULL"
-- A business group has no publicGroup claim, so the domain it was connected by (from its address) is written
-- directly to group_domain and marked verified, so it is found by the local name search (getGroupToConnect).
setPreparedGroupDomain :: DB.Connection -> User -> GroupInfo -> SimplexDomain -> IO GroupInfo
setPreparedGroupDomain db user@User {userId} g@GroupInfo {groupId} domain = do
setPreparedGroupDomain db user@User {userId} g@GroupInfo {groupId, businessChat} domain = do
DB.execute
db
[sql|
@@ -2800,7 +2820,7 @@ setPreparedGroupDomain db user@User {userId} g@GroupInfo {groupId} domain = do
WHERE group_profile_id IN (SELECT group_profile_id FROM groups WHERE user_id = ? AND group_id = ?)
|]
(domain, userId, groupId)
setGroupDomainVerified db user g True
setGroupDomainVerified db user g {businessChat = (\bc -> bc {businessDomain = Just $ mkDomainClaim domain}) <$> businessChat} True
updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo
updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do
@@ -2887,7 +2907,7 @@ getGroupInfoViaUserTarget db cxt user@User {userId} target = fmap eitherToMaybe
FROM user_contact_links ucl
JOIN groups g ON g.group_id = ucl.group_id
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE ucl.user_id = ? AND gp.group_domain = ?
WHERE ucl.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified IS DISTINCT FROM 2
|]
(userId, nameDomain ni)
toConnReqGroupId = \case
@@ -484,7 +484,7 @@ Query:
FROM user_contact_links ucl
JOIN groups g ON g.group_id = ucl.group_id
JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id
WHERE ucl.user_id = ? AND gp.group_domain = ?
WHERE ucl.user_id = ? AND gp.group_domain = ? AND g.group_domain_verified IS DISTINCT FROM 2
Plan:
SEARCH ucl USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
@@ -3849,6 +3849,15 @@ Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT ct.contact_id, ct.contact_profile_id FROM contacts ct
JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id
WHERE ct.user_id = ? AND ct.contact_id IS DISTINCT FROM ? AND cp.contact_domain = ? AND cp.contact_domain_verified = 1
Plan:
SEARCH ct USING COVERING INDEX sqlite_autoindex_contacts_2 (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT d.file_descr_id, d.file_descr_text, d.file_descr_part_no, d.file_descr_complete
FROM xftp_file_descriptions d
@@ -4434,6 +4443,26 @@ Query:
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE groups SET group_domain_verified = 2
WHERE user_id = ? AND group_domain_verified = 1 AND group_id IS DISTINCT FROM ?
AND group_profile_id IN (SELECT group_profile_id FROM group_profiles WHERE user_id = ? AND group_domain = ?)
AND business_chat IS NOT NULL RETURNING group_id
Plan:
SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=? AND group_profile_id=?)
LIST SUBQUERY 1
SEARCH group_profiles USING INDEX idx_group_profiles_user_id (user_id=?)
Query:
UPDATE groups SET group_domain_verified = 2
WHERE user_id = ? AND group_domain_verified = 1 AND group_id IS DISTINCT FROM ?
AND group_profile_id IN (SELECT group_profile_id FROM group_profiles WHERE user_id = ? AND group_domain = ?)
AND business_chat IS NULL RETURNING group_id
Plan:
SEARCH groups USING INDEX sqlite_autoindex_groups_2 (user_id=? AND group_profile_id=?)
LIST SUBQUERY 1
SEARCH group_profiles USING INDEX idx_group_profiles_user_id (user_id=?)
Query:
WITH user_groups AS MATERIALIZED (
SELECT g.group_id
@@ -6637,6 +6666,13 @@ Query:
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND group_id IS NULL AND short_link_contact IS NOT NULL
Plan:
SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?)
Query:
SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, business_address, auto_accept, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
@@ -8042,6 +8078,10 @@ Query: UPDATE contact_profiles SET contact_domain = ?, updated_at = ? WHERE user
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE contact_profiles SET contact_domain_verified = 2 WHERE contact_profile_id = ?
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE contact_profiles SET contact_domain_verified = NULL WHERE user_id = ? AND contact_profile_id = ?
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
+5 -5
View File
@@ -494,7 +494,7 @@ type ContactRow' = (ProfileId, ContactName, ContactName, Text, Maybe Text, Maybe
type ContactRow = Only ContactId :. ContactRow'
type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe BoolInt)
type ContactDomainRow = (Maybe SimplexDomain, Maybe SimplexDomainProof, Maybe DomainVerification)
toContact :: UTCTime -> StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, description, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, encodedPrefs, receivedPrefs, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, rejectionSupported_, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL) :. badgeRow :. domainRow) :. connRow) =
@@ -512,8 +512,8 @@ toContact now cxt user chatTags ((Only contactId :. (profileId, localDisplayName
rowToContactDomain :: ContactDomainRow -> Maybe SimplexDomainClaim
rowToContactDomain (domain_, domainProof_, _) = (`SimplexDomainClaim` domainProof_) . StrJSON <$> domain_
rowToDomainVerified :: ContactDomainRow -> Maybe Bool
rowToDomainVerified (_, _, domainVerification_) = unBI <$> domainVerification_
rowToDomainVerified :: ContactDomainRow -> Maybe DomainVerification
rowToDomainVerified (_, _, domainVerification_) = domainVerification_
contactDomainToRow :: Maybe SimplexDomainClaim -> (Maybe SimplexDomain, Maybe SimplexDomainProof)
contactDomainToRow d = (claimDomain <$> d, proof =<< d)
@@ -696,7 +696,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member
type GroupKeysRow = (Maybe C.PrivateKeyEd25519, Maybe C.PublicKeyEd25519, Maybe C.PrivateKeyEd25519)
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe BoolInt) :. GroupKeysRow :. GroupMemberRow
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Text, Maybe Text, Maybe ImageData, Maybe GroupType, Maybe ShortLinkContact, Maybe B64UrlByteString) :. PublicGroupAccessRow :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Text, Maybe Text, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. PreparedGroupRow :. BusinessChatInfoRow :. (BoolInt, Maybe RelayStatus, Maybe UIThemeEntityOverrides, Int64, Maybe Int64, Maybe VersionRoster, Maybe CustomData, Maybe Int64, Int, Maybe ConnReqContact, Maybe DomainVerification) :. GroupKeysRow :. GroupMemberRow
type PublicGroupAccessRow = (Maybe Text, Maybe SimplexDomain, Maybe BoolInt, Maybe BoolInt, Maybe SimplexDomainProof)
@@ -715,7 +715,7 @@ toGroupInfo now cxt userContactId chatTags ((groupId, localDisplayName, displayN
businessChat = toBusinessChatInfo (toPublicGroupAccess accessRow >>= groupDomainClaim) businessRow
preparedGroup = toPreparedGroup preparedGroupRow
groupSummary = GroupSummary {currentMembers, publicMemberCount}
gInfo = GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupDomainVerified = unBI <$> groupDomainVerified}
gInfo = GroupInfo {groupId, useRelays = BoolDef useRelays, relayOwnStatus, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, preparedGroup, chatTags, chatItemTTL, uiThemes, groupSummary, rosterVersion, customData, membersRequireAttention, viaGroupLinkUri, groupDomainVerified}
in (gInfo, groupKeysRow)
toGroupInfo_ :: UTCTime -> StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
+15 -3
View File
@@ -537,7 +537,7 @@ data GroupInfo = GroupInfo
rosterVersion :: Maybe VersionRoster,
membersRequireAttention :: Int,
viaGroupLinkUri :: Maybe ConnReqContact,
groupDomainVerified :: Maybe Bool
groupDomainVerified :: Maybe DomainVerification
}
deriving (Eq, Show)
@@ -831,14 +831,17 @@ data LocalProfile = LocalProfile
localBadge :: Maybe LocalBadge,
localAlias :: LocalAlias,
contactDomain :: Maybe SimplexDomainClaim,
contactDomainVerified :: Maybe Bool
contactDomainVerified :: Maybe DomainVerification
}
deriving (Eq, Show)
data DomainVerification = DVFailed | DVVerified | DVMoved
deriving (Eq, Show)
localProfileId :: LocalProfile -> ProfileId
localProfileId LocalProfile {profileId} = profileId
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe Bool -> LocalProfile
toLocalProfile :: ProfileId -> Profile -> LocalAlias -> UTCTime -> Maybe Bool -> Maybe DomainVerification -> LocalProfile
toLocalProfile profileId Profile {displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, badge, contactDomain} localAlias now badgeVerified contactDomainVerified =
LocalProfile {profileId, displayName, fullName, shortDescr, description, image, contactLink, preferences, peerType, localBadge, localAlias, contactDomain, contactDomainVerified}
where
@@ -2319,6 +2322,8 @@ $(JQ.deriveJSON defaultJSON ''UserContact)
$(JQ.deriveJSON defaultJSON ''Profile)
$(JQ.deriveJSON (enumJSON $ dropPrefix "DV") ''DomainVerification)
$(JQ.deriveJSON defaultJSON ''LocalProfile)
$(JQ.deriveJSON defaultJSON ''UserContactRequest)
@@ -2388,6 +2393,13 @@ instance FromField MsgFilter where fromField = fromIntField_ msgFilterIntP
instance ToField MsgFilter where toField = toField . msgFilterInt
instance FromField DomainVerification where
fromField = fromIntField_ $ \case
0 -> Just DVFailed
1 -> Just DVVerified
2 -> Just DVMoved
_ -> Nothing
$(JQ.deriveJSON defaultJSON ''CReqClientData)
$(JQ.deriveJSON defaultJSON ''MemberIdRole)
+47 -10
View File
@@ -73,7 +73,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), NetworkError (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), UserProtocol)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), NetworkError (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), USDCents (..), UserProtocol)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..))
import Simplex.Messaging.Util (safeDecodeUtf8, tshow)
@@ -218,7 +218,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation showFullLinks ccLink
CRConnectionIncognitoUpdated u c customUserProfile -> ttyUser u $ viewConnectionIncognitoUpdated c customUserProfile testView
CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged showFullLinks u c nu c'
CRConnectionPlan u connLink _ otherSimplexName connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan <> otherSimplexNameNote otherSimplexName
CRConnectionPlan u connLink planSimplexName otherSimplexName connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink planSimplexName connectionPlan <> otherSimplexNameNote otherSimplexName
CRNewPreparedChat u (AChat _ (Chat cInfo _ _)) -> ttyUser u $ case cInfo of
DirectChat ct -> [ttyContact' ct <> ": contact is prepared"]
GroupChat g _ -> [ttyGroup' g <> ": group is prepared"]
@@ -474,6 +474,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
CEvtRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
CEvtRcvFileProgressXFTP {} -> []
CEvtContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
CEvtNameMoved {} -> []
CEvtGroupMemberUpdated {} -> []
CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} _chat -> ttyUser u $ viewReceivedContactRequest c (fromLocalProfile profile)
CEvtServiceRequest u reqId sigKey_ req ->
@@ -1171,13 +1172,14 @@ viewDomainVerified nameType domain_ result =
Just reason -> [plain nameStr <> " not verified: " <> plain reason]
-- §4.7: show a peer's claimed name only with its verification context — "verified" / "verification
-- failed" when a status is recorded, "unverified" when there is a proof but no status yet, and nothing
-- failed" / "moved" when a status is recorded, "unverified" when there is a proof but no status yet, and nothing
-- at all when there is neither (an unproven, unverifiable claim is not shown).
simplexDomainLine :: SimplexNameType -> Maybe SimplexDomainClaim -> Maybe Bool -> [StyledString]
simplexDomainLine :: SimplexNameType -> Maybe SimplexDomainClaim -> Maybe DomainVerification -> [StyledString]
simplexDomainLine _ Nothing _ = []
simplexDomainLine nameType (Just SimplexDomainClaim {domain, proof}) status = case status of
Just True -> [line "verified"]
Just False -> [line "verification failed"]
Just DVVerified -> [line "verified"]
Just DVFailed -> [line "verification failed"]
Just DVMoved -> [line "moved"]
Nothing
| isJust proof -> [line "unverified"]
| otherwise -> []
@@ -2235,8 +2237,8 @@ otherSimplexNameNote = \case
Just ni@(SimplexNameInfo NTContact _) -> [plain $ "You can also connect to " <> shortNameInfoStr ni <> " in direct chat"]
Nothing -> []
viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString]
viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> Maybe SimplexNameInfo -> ConnectionPlan -> [StyledString]
viewConnectionPlan ChatConfig {logLevel, testView} _connLink planSimplexName = \case
CPInvitationLink ilp -> case ilp of
ILPOk contactSLinkData ov -> [invOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView]
ILPOwnLink -> [invLink "own link"]
@@ -2255,7 +2257,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
Just ContactShortLinkData {business}
| business -> ("business address: " <>)
_ -> ("invitation link: " <>)
CPContactAddress cap -> case cap of
p@(CPContactAddress cap nc_) -> (<> viewNameChange (connectionPlanOwnLink p) nc_) $ case cap of
CAPOk contactSLinkData ov -> [addrOrBiz contactSLinkData "ok to connect"] <> viewSigVerification ov <> [viewJSON contactSLinkData | testView]
CAPOwnLink -> [ctAddr "own address"]
CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"]
@@ -2273,7 +2275,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
Just ContactShortLinkData {business}
| business -> ("business address: " <>)
_ -> ("contact address: " <>)
CPGroupLink glp -> case glp of
p@(CPGroupLink glp nc_) -> (<> viewNameChange (connectionPlanOwnLink p) nc_) $ case glp of
GLPOk groupSLinkInfo_ groupSLinkData ov ->
let direct = maybe True (\(GroupShortLinkInfo {direct = d}) -> d) groupSLinkInfo_
in [grpLink $ if direct then "ok to connect directly" else "ok to connect via relays"]
@@ -2312,6 +2314,17 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
Nothing -> "group"
CPError e -> viewChatError False logLevel testView e
where
viewNameChange :: Bool -> Maybe NameChange -> [StyledString]
viewNameChange own = \case
Just (NCLapsed w) -> maybe [] (\SimplexNameInfo {nameDomain} -> [viewNameWarning own nameDomain w]) planSimplexName
Just (NCMoved (AChatInfo _ c)) -> case c of
DirectChat ct -> ["known contact @" <> ttyContact' ct]
GroupChat g@GroupInfo {businessChat} _
| isJust businessChat -> ["known business " <> ttyGroup' g]
| useRelays' g -> ["known channel " <> ttyGroup' g]
| otherwise -> ["known group " <> ttyGroup' g]
_ -> []
Nothing -> []
nextConnectPrepared Contact {preparedContact, activeConn} = case preparedContact of
Just _ -> maybe True (\c -> connStatus c == ConnPrepared) activeConn
_ -> False
@@ -2327,6 +2340,29 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
Just (OVFailed r) -> ["owner signature: FAILED (" <> plain r <> ")"]
Nothing -> []
viewNameWarning :: Bool -> SimplexDomain -> NameWarning -> StyledString
viewNameWarning own d = \case
NWExpired e g -> yours <> simplexNameStr d <> " expired on " <> plain (day e) <> maybe "" ((renewal <>) . plain . day) g
NWAvailable p -> yours <> simplexNameStr d <> " is no longer registered, available: " <> namePriceStr p
NWReservedForCommunity -> simplexNameStr d <> " is reserved for community"
NWNotRegistered -> simplexNameStr d <> " is not registered"
where
yours = if own then "your " else ""
renewal = if own then ", renew it before " else ", its owner can renew it until "
viewNameNotConnectable :: SimplexDomain -> NameWarning -> StyledString
viewNameNotConnectable d = \case
NWAvailable p -> simplexNameStr d <> " is available: " <> namePriceStr p
w -> viewNameWarning False d w
simplexNameStr :: SimplexDomain -> StyledString
simplexNameStr d = "SimpleX name " <> plain (fullDomainName d)
namePriceStr :: NamePrice -> StyledString
namePriceStr NamePrice {amount = USDCents c, years} =
let (dollars, cents) = c `divMod` 100
in plain $ "$" <> tshow dollars <> (if cents == 0 then "" else "." <> T.justifyRight 2 '0' (tshow cents)) <> " for " <> tshow years <> " years"
viewContactUpdated :: Contact -> Contact -> [StyledString]
viewContactUpdated
Contact {localDisplayName = n, profile = LocalProfile {fullName, shortDescr, description, contactLink}}
@@ -2795,6 +2831,7 @@ viewChatError isCmd logLevel testView = \case
CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
CEInvalidConnReq -> viewInvalidConnReq
CESimplexDomainNotReady domain (SDENameWarning w) -> [viewNameNotConnectable domain w]
CESimplexDomainNotReady domain domainErr ->
let reason = case domainErr of
SDENoValidLink -> "has no valid connection link"