fixes EP review

This commit is contained in:
Alain Brenzikofer
2026-09-24 11:28:38 +02:00
parent 0a245c67cb
commit a8235b9440
36 changed files with 292 additions and 21459 deletions
+59 -31
View File
@@ -123,7 +123,7 @@ 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 qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.SystemTime (getSystemSeconds)
import Simplex.Messaging.SystemTime (getSystemSeconds, roundedToUTCTime)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
import Simplex.Messaging.Util
@@ -1605,7 +1605,7 @@ processChatCommand cxt nm = \case
UserContactLink {shortLinkDataSet, connLinkContact = CCLink _ sl_} <- withFastStore (`getUserAddress` user)
case sl_ of
Just sl | shortLinkDataSet -> do
NameRecord {nrSimplexContact} <- resolveNameRecord user nm domain
(NameRecord {nrSimplexContact}, _) <- resolveNameRecord user nm domain
unless (nameResolvesTo sl nrSimplexContact) $ throwChatError $ CESimplexDomainNotReady domain SDENoValidLink
pure $ Just (CLShort sl)
_ -> throwCmdError "create the address short link and add it to name"
@@ -1958,7 +1958,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 -> fst <$> updateGroupFromLinkData user gInfo sLinkData Nothing Nothing
_ -> pure gInfo
when (memberRole' (membership gInfo) /= GROwner && memberCurrent (membership gInfo)) $
withGroupLock "syncSubscriberRelays" groupId $
@@ -2410,21 +2410,21 @@ 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' <- maybe (pure ct) (\(v, expiresAt) -> withFastStore' $ \db -> setContactDomainVerified db user ct v expiresAt) verified
pure $ CRContactDomainVerified user ct' reason
APIVerifyGroupDomain groupId -> withUser $ \user -> do
g@GroupInfo {groupProfile = GroupProfile {publicGroup}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
PublicGroupProfile {groupLink, publicGroupAccess} <- maybe (throwCmdError "not a public group") pure publicGroup
claim <- maybe (throwCmdError "group has no name to verify") pure $ publicGroupAccess >>= groupDomainClaim
-- checks the profile link, not the link we joined through (which may have rotated)
(verified, reason) <-
(verified, reason, expiresAt) <-
tryAllErrors (resolveNameRecord user nm (claimDomain claim)) >>= \case
Right NameRecord {nrSimplexChannel}
| nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing)
| 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")
Right (NameRecord {nrSimplexChannel}, expiresAt)
| nameResolvesTo groupLink nrSimplexChannel -> pure (True, Nothing, expiresAt)
| otherwise -> pure (False, Just "the name does not resolve to the link in the group profile", expiresAt)
Left (ChatErrorAgent {agentError = SMP _ (NAME SMP.NOT_FOUND)}) -> pure (False, Just "the name is not registered", Nothing)
Left e -> throwError e
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified
g' <- withFastStore' $ \db -> setGroupDomainVerified db user g verified expiresAt
pure $ CRGroupDomainVerified user g' reason
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId
@@ -3278,7 +3278,7 @@ processChatCommand cxt nm = \case
processChatCommand cxt nm $ APIListGroups userId (contactId' <$> ct_) search_
APIUpdateGroupProfile groupId p' -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfoKeys db cxt user groupId
runUpdateGroupProfile user gInfo p' False
runUpdateGroupProfile user gInfo p' Nothing
UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} ->
updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr}
ShowGroupProfile gName -> withUser $ \user ->
@@ -3292,11 +3292,11 @@ processChatCommand cxt nm = \case
case publicGroup of
Just pg@PublicGroupProfile {groupLink, publicGroupAccess = existingAccess} -> do
let domainChanged = (claimDomain <$> newClaim) /= (claimDomain <$> (existingAccess >>= groupDomainClaim))
forM_ (claimDomain <$> newClaim) $ \newDomain ->
when domainChanged $ do
NameRecord {nrSimplexChannel} <- resolveNameRecord user nm newDomain
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} (isJust newClaim && domainChanged)
verifiedExpiry_ <- forM (if domainChanged then claimDomain <$> newClaim else Nothing) $ \newDomain -> do
(NameRecord {nrSimplexChannel}, expiresAt) <- resolveNameRecord user nm newDomain
unless (nameResolvesTo groupLink nrSimplexChannel) $ throwChatError $ CESimplexDomainNotReady newDomain SDENoValidLink
pure expiresAt
runUpdateGroupProfile user gInfo p {publicGroup = Just pg {publicGroupAccess = Just access}} verifiedExpiry_
Nothing -> throwChatError $ CECommandError "not a public group"
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId
@@ -4097,8 +4097,8 @@ processChatCommand cxt nm = \case
void (sendDirectContactMessage user ct' $ XInfo p Nothing) `catchAllErrors` eToView
lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct'
pure $ CRContactPrefsUpdated user ct ct'
runUpdateGroupProfile :: User -> GroupInfoKeys -> GroupProfile -> Bool -> CM ChatResponse
runUpdateGroupProfile user (GIK gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} gks) p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} domainVerified = do
runUpdateGroupProfile :: User -> GroupInfoKeys -> GroupProfile -> Maybe (Maybe UTCTime) -> CM ChatResponse
runUpdateGroupProfile user (GIK gInfo@GroupInfo {businessChat, groupProfile = p@GroupProfile {displayName = n}} gks) p'@GroupProfile {displayName = n', image = img', memberAdmission = ma'} verifiedExpiry_ = do
assertUserGroupRole gInfo GROwner
when (n /= n') $ checkValidName n'
checkProfileImageSize img'
@@ -4107,7 +4107,7 @@ processChatCommand cxt nm = \case
-- updateGroupProfile clears domain verification; re-set it when the caller already re-resolved the name
gInfo' <- withStore $ \db -> do
g <- updateGroupProfile db user gInfo p'
if domainVerified then liftIO $ setGroupDomainVerified db user g True else pure g
maybe (pure g) (liftIO . setGroupDomainVerified db user g True) verifiedExpiry_
msg <- case businessChat of
Just BusinessChatInfo {businessId} -> do
ms <- withStore' $ \db -> getGroupMembers db cxt user gInfo'
@@ -4200,7 +4200,7 @@ processChatCommand cxt nm = \case
applicable = if channel then groupFeatureInChannel feature else groupFeatureInRegularGroup feature
unless applicable $
throwCmdError $ T.unpack (groupFeatureNameText feature) <> " is not available in " <> (if channel then "channels" else "groups")
runUpdateGroupProfile user gInfo (update p) False
runUpdateGroupProfile user gInfo (update p) Nothing
withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse
withCurrentCall ctId action = do
(user, ct) <- withStore $ \db -> do
@@ -4411,6 +4411,24 @@ processChatCommand cxt nm = \case
invitationReqAndPlan cReq sLnk_ cld ov = do
plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError)
pure (Just (ACCL SCMInvitation (CCLink cReq sLnk_)), Nothing, Nothing, plan)
connectPlan user t@(ACTarget SCMContact ct) PRMUnknown sig_ Nothing
| isName =
tryAllErrors (connectPlan user t PRMNever sig_ Nothing) >>= \case
Right r@(_, _, _, p) -> ifM (resolvedRecently p) (pure r) resolvePlan
Left _ -> resolvePlan
where
isName = case ct of
CTDomain _ -> True
CTShortContact (CTName _) -> True
_ -> False
resolvePlan = connectPlan user t PRMAll sig_ Nothing
resolvedRecently p = do
resolution_ <- withFastStore' $ \db -> case p of
CPContactAddress (CAPKnown ct') _ -> getContactDomainResolution db user ct'
CPGroupLink (GLPKnown g _ _ _) _ -> getGroupDomainResolution db user g
_ -> pure Nothing
now <- liftIO getCurrentTime
pure $ maybe False (\(resolvedAt, expiresAt_) -> diffUTCTime now resolvedAt < nominalDay && maybe True (now <) expiresAt_) resolution_
connectPlan user (ACTarget SCMContact ct) resolveMode sig_ nameRec = case ct of
CTDomain d
-- local search only: look up #d then @d in the store, without online name resolution
@@ -4466,7 +4484,7 @@ processChatCommand cxt nm = \case
when (resolveMode == PRMNever) $ throwChatError CENotResolvedLocally
l' <- resolveSLink
case known_ of
Just r | knownLinkOf r == Just l' -> pure r
Just r@(_, p) | knownLinkOf r == Just l' -> r <$ when (isJust simplexName_) (setKnownVerified p)
_ -> (if isJust known_ then second setAddressChanged else id) <$> resolvedPlan l'
where
resolvedPlan l' = do
@@ -4476,7 +4494,7 @@ processChatCommand cxt nm = \case
linkDomain_ = linkProfile_ >>= \Profile {contactDomain} -> claimDomain <$> contactDomain
planDomain = case nl of CTName ni -> Just (nameDomain ni); _ -> Nothing
refreshContact ct' = case (planDomain, linkProfile_) of
(Just _, Just p) -> updateContactFromLinkData user ct' p
(Just _, Just p) -> updateContactFromLinkData user ct' p expiresAt
_ -> pure ct'
forM_ planDomain $ \nameDomain ->
unless (linkDomain_ == Just nameDomain) $ throwChatError $ CESimplexDomainNotReady nameDomain SDEUnknownDomain
@@ -4519,6 +4537,11 @@ processChatCommand cxt nm = \case
CTLink l' -> pure l'
CTName n -> serverShortLink <$> resolveNameLink n
con l' cReq = ACCL SCMContact $ CCLink cReq (Just l')
expiresAt = nameExpiresAt =<< eitherToMaybe =<< nameRec
setKnownVerified = \case
CPContactAddress (CAPKnown ct') _ -> void $ withFastStore' $ \db -> setContactDomainVerified db user ct' True expiresAt
CPGroupLink (GLPKnown g _ _ _) _ -> void $ withFastStore' $ \db -> setGroupDomainVerified db user g True expiresAt
_ -> pure ()
reResolveKnown (_, p) = resolveMode == PRMAll && case p of
CPContactAddress (CAPKnown _) _ -> True
CPGroupLink GLPKnown {} _ -> True
@@ -4563,7 +4586,7 @@ processChatCommand cxt nm = \case
-- 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) Nothing) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain)
(\(g', _) -> CPGroupLink (GLPKnown g' u o os) Nothing) <$> updateGroupFromLinkData user g sLinkData (Just nameDomain) expiresAt
_ -> pure plan0
forM_ planDomain $ \nameDomain ->
let domain_ = (\GroupProfile {publicGroup} -> claimDomain <$> (publicGroup >>= publicGroupAccess >>= groupDomainClaim)) =<< case plan of
@@ -4588,7 +4611,7 @@ processChatCommand cxt nm = \case
let ov = verifyLinkOwner rk owners l' sig_
glOwners = map (\OwnerAuth {ownerId, ownerKey} -> GroupLinkOwner {memberId = MemberId ownerId, memberKey = ownerKey}) owners
(g', updated) <- case groupSLinkData_ of
Just sLinkData -> updateGroupFromLinkData user g sLinkData Nothing
Just sLinkData -> updateGroupFromLinkData user g sLinkData (nameDomain <$> simplexName_) expiresAt
_ -> pure (g, False)
pure (con l' cReq, CPGroupLink (GLPKnown g' updated ov (ListDef glOwners)) Nothing)
-- resolve a name to its first contact/channel short link
@@ -5111,10 +5134,10 @@ resolveNameRegistration user nm domain =
registration <$> withAgent (\a -> resolveSimplexName a nm (aUserId user) domain)
-- the resolver now also reports names that are not registered, which stay the agent's NAME NOT_FOUND
resolveNameRecord :: User -> NetworkRequestMode -> SimplexDomain -> CM NameRecord
resolveNameRecord :: User -> NetworkRequestMode -> SimplexDomain -> CM (NameRecord, Maybe UTCTime)
resolveNameRecord user nm domain =
resolveNameRegistration user nm domain >>= \case
NRRegistered {nameRecord} -> pure nameRecord
reg@NRRegistered {nameRecord} -> pure (nameRecord, nameExpiresAt reg)
_ -> throwError $ chatErrorAgent $ SMP "" (NAME SMP.NOT_FOUND)
nameExpired :: NameRegistration -> CM Bool
@@ -5122,6 +5145,11 @@ nameExpired = \case
NRRegistered {expires = Just expires} -> (expires <) <$> liftIO getSystemSeconds
_ -> pure False
nameExpiresAt :: NameRegistration -> Maybe UTCTime
nameExpiresAt = \case
NRRegistered {expires} -> roundedToUTCTime <$> expires
_ -> Nothing
nameHasLink :: SimplexNameType -> NameRegistration -> Bool
nameHasLink nameType = \case
NRRegistered {nameRecord = NameRecord {nrSimplexContact, nrSimplexChannel}} -> case nameType of
@@ -5143,20 +5171,20 @@ setAddressChanged = \case
CPGroupLink (GLPOk li gld ov _) nr -> CPGroupLink (GLPOk li gld ov True) nr
p -> p
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe Bool, Maybe Text)
verifyEntityDomain :: User -> NetworkRequestMode -> SimplexNameType -> SimplexDomainClaim -> Maybe AConnShortLink -> CM (Maybe (Bool, Maybe UTCTime), 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")
(_, Nothing) -> pure (Nothing, Just "no connection link to check the name against")
(Just proof, Just (ACSL SCMContact profileSLnk)) -> do
NameRecord {nrSimplexContact, nrSimplexChannel} <- resolveNameRecord user nm domain
(NameRecord {nrSimplexContact, nrSimplexChannel}, expiresAt) <- resolveNameRecord user nm domain
let resolvedLinks = case nameType of
NTContact -> nrSimplexContact
NTPublicGroup -> nrSimplexChannel
if not (nameResolvesTo profileSLnk resolvedLinks)
then pure (Just False, Just "the name does not resolve to this address")
then pure (Just (False, expiresAt), Just "the name does not resolve to this address")
else do
ok <- verifyDomainProof proof profileSLnk
pure (Just ok, if ok then Nothing else Just "the name proof was not signed by this address's owner")
pure (Just (ok, expiresAt), if ok then Nothing else Just "the name proof was not signed by this address's owner")
(Just _, Just _) -> pure (Nothing, Just "unexpected connection link type for name verification")
where
verifyDomainProof :: SimplexDomainProof -> ShortLinkContact -> CM Bool
@@ -5777,7 +5805,7 @@ sendServiceRequestBytes nm user sendTarget requestTimeout signKey request = do
_ -> throwCmdError "service request target must be a contact"
CTDomain d -> resolveDomain d
resolveDomain d = do
nr <- resolveNameRecord user nm d
(nr, _) <- resolveNameRecord user nm d
case firstNameLink CCTContact (nrSimplexContact nr) of
Just sLnk -> resolveShortLink sLnk
Nothing -> throwChatError $ CESimplexDomainNotReady d SDENoValidLink
+6 -6
View File
@@ -1611,8 +1611,8 @@ 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 gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} resolvedDomain_
updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> Maybe SimplexDomain -> Maybe UTCTime -> CM (GroupInfo, Bool)
updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} resolvedDomain_ expiresAt
| profileChanged || countChanged || verifyResolved = do
cxt <- chatStoreCxt
withStore $ \db -> do
@@ -1621,7 +1621,7 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G
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'
g'' <- if verifyResolved then liftIO $ setGroupDomainVerified db user g' True expiresAt else pure g'
pure (g'', profileChanged)
| otherwise = pure (gInfo, False)
where
@@ -1633,13 +1633,13 @@ updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = G
newClaim = groupClaim groupProfile
verifyResolved = isJust resolvedDomain_ && resolvedDomain_ == newClaim
updateContactFromLinkData :: User -> Contact -> Profile -> CM Contact
updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim}
updateContactFromLinkData :: User -> Contact -> Profile -> Maybe UTCTime -> CM Contact
updateContactFromLinkData user ct@Contact {profile = profile@LocalProfile {contactDomain = prevClaim, contactDomainVerified}} linkProfile@Profile {contactDomain = newClaim} expiresAt
| profileChanged || verifyChanged = do
cxt <- chatStoreCxt
withFastStore $ \db -> do
ct' <- updateContactProfile db cxt user ct linkProfile
if verifyChanged then liftIO $ setContactDomainVerified db user ct' True else pure ct'
if verifyChanged then liftIO $ setContactDomainVerified db user ct' True expiresAt else pure ct'
| otherwise = pure ct
where
profileChanged = fromLocalProfile profile /= linkProfile
+15 -5
View File
@@ -52,6 +52,7 @@ module Simplex.Chat.Store.Direct
getContactIdByName,
updateContactProfile,
setContactDomainVerified,
getContactDomainResolution,
updateContactUserPreferences,
updateContactAlias,
updateContactConnectionAlias,
@@ -410,7 +411,7 @@ createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId verifie
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
ctUserPreferences = newContactUserPrefs user p
ct <- getContact db cxt user =<< createContact_ db cxt user p ctUserPreferences prepared "" currentTs
liftIO $ maybe (pure ct) (setContactDomainVerified db user ct) verified_
liftIO $ maybe (pure ct) (\v -> setContactDomainVerified db user ct v Nothing) verified_
updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact
updatePreparedContactUser
@@ -590,17 +591,26 @@ updateContactProfile db cxt user@User {userId} c p' = do
clearVerificationIfClaimChanged
pure $ Right c {localDisplayName = ldn, profile, mergedPreferences}
setContactDomainVerified :: DB.Connection -> User -> Contact -> Bool -> IO Contact
setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} verified = do
setContactDomainVerified :: DB.Connection -> User -> Contact -> Bool -> Maybe UTCTime -> IO Contact
setContactDomainVerified db User {userId} ct@Contact {contactId, profile = p} verified expiresAt = do
currentTs <- getCurrentTime
DB.execute
db
[sql|
UPDATE contact_profiles SET contact_domain_verified = ?
UPDATE contact_profiles SET contact_domain_verified = ?, contact_domain_resolved_at = ?, contact_domain_expires_at = ?
WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?)
|]
(BI verified, userId, contactId)
(BI verified, currentTs, expiresAt, userId, contactId)
pure (ct {profile = p {contactDomainVerified = Just verified}} :: Contact)
getContactDomainResolution :: DB.Connection -> User -> Contact -> IO (Maybe (UTCTime, Maybe UTCTime))
getContactDomainResolution db User {userId} Contact {profile = LocalProfile {profileId}} =
maybeFirstRow id $
DB.query
db
"SELECT contact_domain_resolved_at, contact_domain_expires_at FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? AND contact_domain_resolved_at IS NOT NULL"
(userId, profileId)
updateContactUserPreferences :: DB.Connection -> User -> Contact -> Preferences -> IO Contact
updateContactUserPreferences db user@User {userId} c@Contact {contactId} userPreferences = do
updatedAt <- getCurrentTime
+16 -6
View File
@@ -47,6 +47,7 @@ module Simplex.Chat.Store.Groups
getGroupInfoByGroupLinkHash,
updateGroupProfile,
setGroupDomainVerified,
getGroupDomainResolution,
updateGroupPreferences,
updateGroupProfileFromMember,
getGroupIdByName,
@@ -669,7 +670,7 @@ createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile b
-- a business has no domain in its profile, so set it out-of-band; a channel already has it (createGroup_), just verify
g' <- liftIO $ case verifiedDomain of
Just d | business -> setPreparedGroupDomain db user g d
Just _ -> setGroupDomainVerified db user g True
Just _ -> setGroupDomainVerified db user g True Nothing
Nothing -> pure g
pure (g', hostMember_)
where
@@ -2740,14 +2741,23 @@ updateGroupProfile db user@User {userId} g@GroupInfo {groupId, localDisplayName,
(ldn, currentTs, userId, groupId)
safeDeleteLDN db user localDisplayName
setGroupDomainVerified :: DB.Connection -> User -> GroupInfo -> Bool -> IO GroupInfo
setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified = do
setGroupDomainVerified :: DB.Connection -> User -> GroupInfo -> Bool -> Maybe UTCTime -> IO GroupInfo
setGroupDomainVerified db User {userId} g@GroupInfo {groupId} verified expiresAt = do
currentTs <- getCurrentTime
DB.execute
db
"UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?"
(BI verified, userId, groupId)
"UPDATE groups SET group_domain_verified = ?, group_domain_resolved_at = ?, group_domain_expires_at = ? WHERE user_id = ? AND group_id = ?"
(BI verified, currentTs, expiresAt, userId, groupId)
pure g {groupDomainVerified = Just verified}
getGroupDomainResolution :: DB.Connection -> User -> GroupInfo -> IO (Maybe (UTCTime, Maybe UTCTime))
getGroupDomainResolution db User {userId} GroupInfo {groupId} =
maybeFirstRow id $
DB.query
db
"SELECT group_domain_resolved_at, group_domain_expires_at FROM groups WHERE user_id = ? AND group_id = ? AND group_domain_resolved_at IS NOT NULL"
(userId, groupId)
-- 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
@@ -2759,7 +2769,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 True Nothing
updateGroupPreferences :: DB.Connection -> User -> GroupInfo -> GroupPreferences -> IO GroupInfo
updateGroupPreferences db User {userId} g@GroupInfo {groupId, groupProfile = p} ps = do
@@ -52,6 +52,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry
import Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges
import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges
import Simplex.Chat.Store.Postgres.Migrations.M20260918_badge_issue_errors
import Simplex.Chat.Store.Postgres.Migrations.M20260924_simplex_name_resolved
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -103,7 +104,8 @@ schemaMigrations =
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges),
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors)
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
("20260924_simplex_name_resolved", m20260924_simplex_name_resolved, Just down_m20260924_simplex_name_resolved)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,27 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260924_simplex_name_resolved where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260924_simplex_name_resolved :: Text
m20260924_simplex_name_resolved =
[r|
ALTER TABLE contact_profiles ADD COLUMN contact_domain_resolved_at TIMESTAMPTZ;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_expires_at TIMESTAMPTZ;
ALTER TABLE groups ADD COLUMN group_domain_resolved_at TIMESTAMPTZ;
ALTER TABLE groups ADD COLUMN group_domain_expires_at TIMESTAMPTZ;
|]
down_m20260924_simplex_name_resolved :: Text
down_m20260924_simplex_name_resolved =
[r|
ALTER TABLE contact_profiles DROP COLUMN contact_domain_resolved_at;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_expires_at;
ALTER TABLE groups DROP COLUMN group_domain_resolved_at;
ALTER TABLE groups DROP COLUMN group_domain_expires_at;
|]
@@ -680,7 +680,9 @@ CREATE TABLE test_chat_schema.contact_profiles (
contact_domain text,
contact_domain_proof text,
contact_domain_verified smallint,
description text
description text,
contact_domain_resolved_at timestamp with time zone,
contact_domain_expires_at timestamp with time zone
);
@@ -1168,7 +1170,9 @@ CREATE TABLE test_chat_schema.groups (
roster_blob bytea,
group_domain_verified smallint,
stored_roster_version bigint,
applied_complete_roster_version bigint
applied_complete_roster_version bigint,
group_domain_resolved_at timestamp with time zone,
group_domain_expires_at timestamp with time zone
);
+3 -1
View File
@@ -175,6 +175,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry
import Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges
import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges
import Simplex.Chat.Store.SQLite.Migrations.M20260918_badge_issue_errors
import Simplex.Chat.Store.SQLite.Migrations.M20260924_simplex_name_resolved
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -349,7 +350,8 @@ schemaMigrations =
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges),
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors)
("20260918_badge_issue_errors", m20260918_badge_issue_errors, Just down_m20260918_badge_issue_errors),
("20260924_simplex_name_resolved", m20260924_simplex_name_resolved, Just down_m20260924_simplex_name_resolved)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,26 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260924_simplex_name_resolved where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260924_simplex_name_resolved :: Query
m20260924_simplex_name_resolved =
[sql|
ALTER TABLE contact_profiles ADD COLUMN contact_domain_resolved_at TEXT;
ALTER TABLE contact_profiles ADD COLUMN contact_domain_expires_at TEXT;
ALTER TABLE groups ADD COLUMN group_domain_resolved_at TEXT;
ALTER TABLE groups ADD COLUMN group_domain_expires_at TEXT;
|]
down_m20260924_simplex_name_resolved :: Query
down_m20260924_simplex_name_resolved =
[sql|
ALTER TABLE contact_profiles DROP COLUMN contact_domain_resolved_at;
ALTER TABLE contact_profiles DROP COLUMN contact_domain_expires_at;
ALTER TABLE groups DROP COLUMN group_domain_resolved_at;
ALTER TABLE groups DROP COLUMN group_domain_expires_at;
|]
@@ -5392,7 +5392,7 @@ Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE contact_profiles SET contact_domain_verified = ?
UPDATE contact_profiles SET contact_domain_verified = ?, contact_domain_resolved_at = ?, contact_domain_expires_at = ?
WHERE contact_profile_id IN (SELECT contact_profile_id FROM contacts WHERE user_id = ? AND contact_id = ?)
Plan:
@@ -7451,6 +7451,10 @@ Query: SELECT connection_id FROM connections WHERE user_id = ? AND group_member_
Plan:
SEARCH connections USING INDEX idx_connections_group_member_id (group_member_id=?)
Query: SELECT contact_domain_resolved_at, contact_domain_expires_at FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? AND contact_domain_resolved_at IS NOT NULL
Plan:
SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT contact_id FROM contacts WHERE user_id = ? AND chat_item_ttl > 0 OR chat_item_ttl IS NULL
Plan:
SCAN contacts
@@ -7537,6 +7541,10 @@ Query: SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id
Plan:
SEARCH g USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT group_domain_resolved_at, group_domain_expires_at FROM groups WHERE user_id = ? AND group_id = ? AND group_domain_resolved_at IS NOT NULL
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT group_id FROM group_members WHERE group_member_id = ?
Plan:
SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?)
@@ -7861,6 +7869,14 @@ 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_expires_at = datetime('now', '-1 hours')
Plan:
SCAN contact_profiles
Query: UPDATE contact_profiles SET contact_domain_resolved_at = datetime('now', '-2 days')
Plan:
SCAN contact_profiles
Query: UPDATE contact_profiles SET image = ? WHERE display_name = ?
Plan:
SEARCH contact_profiles USING INDEX contact_profiles_index (display_name=?)
@@ -8093,7 +8109,11 @@ Query: UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE use
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE groups SET group_domain_verified = ? WHERE user_id = ? AND group_id = ?
Query: UPDATE groups SET group_domain_resolved_at = datetime('now', '-2 days')
Plan:
SCAN groups
Query: UPDATE groups SET group_domain_verified = ?, group_domain_resolved_at = ?, group_domain_expires_at = ? WHERE user_id = ? AND group_id = ?
Plan:
SEARCH groups USING INTEGER PRIMARY KEY (rowid=?)
@@ -32,7 +32,9 @@ CREATE TABLE contact_profiles(
contact_domain TEXT,
contact_domain_proof TEXT,
contact_domain_verified INTEGER,
description TEXT
description TEXT,
contact_domain_resolved_at TEXT,
contact_domain_expires_at TEXT
) STRICT;
CREATE TABLE users(
user_id INTEGER PRIMARY KEY,
@@ -209,7 +211,9 @@ CREATE TABLE groups(
roster_blob BLOB,
group_domain_verified INTEGER,
stored_roster_version INTEGER,
applied_complete_roster_version INTEGER, -- received
applied_complete_roster_version INTEGER,
group_domain_resolved_at TEXT,
group_domain_expires_at TEXT, -- received
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
-1
View File
@@ -74,7 +74,6 @@ import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, BlockingInfo (..), BlockingReason (..), NameRegistration (..), NamePricing (..), NetworkError (..), ProtocolServer (..), ProtocolTypeI, SProtocolType (..), USDCents (..), UserProtocol)
import Simplex.Messaging.SimplexName (fullDomainName)
import Simplex.Messaging.SystemTime (roundedSeconds)
import qualified Simplex.Messaging.Protocol as SMP
import Simplex.Messaging.Transport.Client (TransportHost (..))