From cc643e5aebc3b52eacebe285d129d12a2a81041b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:05:23 +0000 Subject: [PATCH] core: rework contact requests so that they are always created with entity (#6011) * core: rework contact requests so that they are always created with entity * remove group requests (revert) * fix schema * remove accepted, set xcontactId * enable tests * fix deduplication, fix address deletion * fix business ldn * disable, add tests * comments, schema * cleanup * fix * plans --- simplex-chat.cabal | 1 + src/Simplex/Chat/Library/Commands.hs | 5 +- src/Simplex/Chat/Library/Internal.hs | 71 ++--- src/Simplex/Chat/Library/Subscriber.hs | 162 +++++----- src/Simplex/Chat/Store/ContactRequest.hs | 299 ++++++++++++++++++ src/Simplex/Chat/Store/Direct.hs | 195 +----------- src/Simplex/Chat/Store/Groups.hs | 62 ++-- src/Simplex/Chat/Store/Messages.hs | 7 +- src/Simplex/Chat/Store/Profiles.hs | 26 -- .../Migrations/M20250526_short_links.hs | 14 + .../SQLite/Migrations/chat_query_plans.txt | 210 ++++++------ .../Store/SQLite/Migrations/chat_schema.sql | 7 + src/Simplex/Chat/Store/Shared.hs | 8 +- src/Simplex/Chat/Types.hs | 42 ++- tests/ChatTests/Profiles.hs | 30 +- 15 files changed, 632 insertions(+), 507 deletions(-) create mode 100644 src/Simplex/Chat/Store/ContactRequest.hs diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 2a9eebb1a4..1b4159061f 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -71,6 +71,7 @@ library Simplex.Chat.Store Simplex.Chat.Store.AppSettings Simplex.Chat.Store.Connections + Simplex.Chat.Store.ContactRequest Simplex.Chat.Store.Direct Simplex.Chat.Store.Files Simplex.Chat.Store.Groups diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 4f09da675f..fb85dec0a7 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1157,10 +1157,10 @@ processChatCommand' vr = \case when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" withUserContactLock "acceptContact" uclId $ do cReq <- withFastStore $ \db -> getContactRequest db user connReqId + -- TODO [short links] accept async, move to continuation on JOIN? (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito let contactUsed = isNothing gLinkInfo_ ct' <- withStore' $ \db -> do - deleteContactRequestRec db user cReq updateContactAccepted db user ct contactUsed conn' <- if sqSecured @@ -1808,6 +1808,7 @@ processChatCommand' vr = \case pure $ CRStartedConnectionToContact user ct' customUserProfile cr -> pure cr Just PreparedContact {connLinkToConnect = ACCL SCMContact ccLink, welcomeSharedMsgId} -> do + -- TODO [short links] reuse welcomeSharedMsgId msg_ <- forM msgContent_ $ \mc -> (,mc) <$> getSharedMsgId connectViaContact user incognito ccLink welcomeSharedMsgId msg_ (Just $ ACCGContact contactId) >>= \case CRSentInvitation {customUserProfile} -> do @@ -1823,7 +1824,7 @@ processChatCommand' vr = \case case preparedGroup gInfo of Nothing -> throwCmdError "group doesn't have link to connect" Just PreparedGroup {connLinkToConnect} -> - -- TODO [short links] store request message with shared message ID + -- TODO [short links] store request message with shared message ID (for business chat) connectViaContact user incognito connLinkToConnect Nothing Nothing (Just $ ACCGGroup gInfo (groupMemberId' hostMember)) >>= \case CRSentInvitation {customUserProfile} -> do -- get updated group info (connLinkStartedConnection and incognito membership) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 1df08c6171..e92a896a65 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -62,6 +62,7 @@ import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups @@ -879,7 +880,8 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' - (ct, conn) <- withStore' $ \db -> createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False + (ct, conn) <- withStore' $ \db -> + createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False pure (ct, conn, incognitoProfile) Just contactId -> do ct <- withFastStore $ \db -> getContact db vr user contactId @@ -888,7 +890,9 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' currentTs <- liftIO getCurrentTime - conn <- withStore' $ \db -> createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs + conn <- withStore' $ \db -> do + forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId + createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs pure (ct {activeConn = Just conn} :: Contact, conn, incognitoProfile) Just conn@Connection {customUserProfileId} -> do incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId @@ -898,27 +902,26 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = -- TODO [certs rcv] (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) -acceptContactRequestAsync :: User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> Maybe IncognitoProfile -> CM Contact +acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact acceptContactRequestAsync user uclId - cReqInvId - cReqChatVRange - cReqProfile - cReqXContactId_ - cReqPQSup + ct@Contact {contactId} + UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId, pqSupport = cReqPQSup} incognitoProfile = do subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile False vr <- chatVersionRange let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV + currentTs <- liftIO getCurrentTime withStore $ \db -> do - (ct, Connection {connId}) <- createAcceptedContact db vr user uclId acId chatV cReqChatVRange cReqProfile cReqXContactId_ cReqPQSup incognitoProfile subMode + forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId + Connection {connId} <- liftIO $ createAcceptedContactConn db user uclId contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId - pure ct + getContact db vr user contactId -acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember +acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember acceptGroupJoinRequestAsync user uclId @@ -926,13 +929,14 @@ acceptGroupJoinRequestAsync cReqInvId cReqChatVRange cReqProfile + cReqXContactId_ gAccepted gLinkMemRole incognitoProfile = do gVar <- asks random let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile gLinkMemRole initialStatus + createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ gLinkMemRole initialStatus currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo let Profile {displayName} = profileToSendOnAccept user incognitoProfile True GroupMember {memberRole = userRole, memberId = userMemberId} = membership @@ -955,7 +959,7 @@ acceptGroupJoinRequestAsync liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode getGroupMemberById db vr user groupMemberId -acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> GroupRejectionReason -> CM GroupMember +acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember acceptGroupJoinSendRejectAsync user uclId @@ -963,10 +967,11 @@ acceptGroupJoinSendRejectAsync cReqInvId cReqChatVRange cReqProfile + cReqXContactId_ rejectionReason = do gVar <- asks random (groupMemberId, memberId) <- withStore $ \db -> - createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile GRObserver GSMemRejected + createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ GRObserver GSMemRejected let GroupMember {memberRole = userRole, memberId = userMemberId} = membership msg = XGrpLinkReject $ @@ -984,23 +989,17 @@ acceptGroupJoinSendRejectAsync liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode getGroupMemberById db vr user groupMemberId -acceptBusinessJoinRequestAsync :: User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> CM (GroupInfo, GroupMember) +acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember) acceptBusinessJoinRequestAsync user uclId - cReqInvId - cReqChatVRange - cReqProfile - cReqXContactId_ = do + gInfo@GroupInfo {membership = GroupMember {memberRole = userRole, memberId = userMemberId}} + clientMember@GroupMember {groupMemberId, memberId} + UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId} = do vr <- chatVersionRange - gVar <- asks random let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True + -- TODO [short links] take groupPreferences from group info groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences - (gInfo, clientMember) <- withStore $ \db -> - createBusinessRequestGroup db vr gVar user cReqChatVRange cReqProfile cReqXContactId_ groupPreferences - let GroupInfo {membership} = gInfo - GroupMember {memberRole = userRole, memberId = userMemberId} = membership - GroupMember {groupMemberId, memberId} = clientMember msg = XGrpLinkInv $ GroupLinkInvitation @@ -1018,23 +1017,20 @@ acceptBusinessJoinRequestAsync subMode <- chatReadVar subscriptionMode let chatV = vr `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV - withStore' $ \db -> createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode + withStore' $ \db -> do + forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId + createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode let cd = CDGroupSnd gInfo Nothing + -- TODO [short links] move to profileContactRequest? createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = Just PQEncOff}) Nothing createGroupFeatureItems user cd CISndGroupFeature gInfo + -- TODO [short links] get updated business chat group and member? (currently not used) pure (gInfo, clientMember) businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, image} groupPreferences = GroupProfile {displayName, fullName, description = Nothing, image, groupPreferences = Just groupPreferences, memberAdmission = Nothing} -profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile -profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing - where - getIncognitoProfile = \case - NewIncognito p -> p - ExistingIncognito lp -> fromLocalProfile lp - introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do forM_ (memberConn m) $ \mConn -> do @@ -2253,15 +2249,6 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr) -userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile -userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do - let p' = fromMaybe (fromLocalProfile p) incognitoProfile - if inGroup - then redactedMemberProfile p' - else - let userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile - in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs} - connRequestPQEncryption :: ConnectionRequestUri c -> Maybe PQEncryption connRequestPQEncryption = \case CRContactUri _ -> Nothing diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 8b399c81ab..810af2fe0e 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -50,6 +50,7 @@ import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections +import Simplex.Chat.Store.ContactRequest import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups @@ -572,6 +573,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = lift $ setContactNetworkStatus ct' NSConnected toView $ CEvtContactConnected user ct' (fmap fromLocalProfile incognitoProfile) let createE2EItem = createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo $ Just pqEnc) Nothing + -- TODO [short links] get contact request by contactRequestId, check encryption (UserContactRequest.pqSupport)? when (directOrUsed ct') $ case (preparedContact ct', contactRequestId' ct') of (Nothing, Nothing) -> do createE2EItem @@ -1236,22 +1238,35 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> PQSupport -> CM () profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ welcomeMsgId_ requestMsg_ reqPQSup = do - uclGLinkInfo <- withStore $ \db -> getUserContactLinkById db userId uclId - let (UserContactLink {connLinkContact = CCLink connReq _, shortLinkDataSet, addressSettings}, gLinkInfo_) = uclGLinkInfo - AddressSettings {businessAddress, autoAccept} = addressSettings - isSimplexTeam = sameConnReqContact connReq adminContactReq - v = maxVersion chatVRange - case autoAccept of - Nothing -> - withStore (\db -> createOrUpdateContactRequest db vr user uclId invId chatVRange p xContactId_ reqPQSup) >>= \case - CORContact ct -> toView $ CEvtContactRequestAlreadyAccepted user ct - CORRequest cReq ct_ newRequest -> do - chat_ <- forM ct_ $ \ct -> do + (ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId + let v = maxVersion chatVRange + case gLinkInfo_ of + -- ##### Contact requests (regular and business contacts) ##### + Nothing -> do + let UserContactLink {connLinkContact = CCLink connReq _, shortLinkDataSet, addressSettings} = ucl + AddressSettings {autoAccept} = addressSettings + isSimplexTeam = sameConnReqContact connReq adminContactReq + gVar <- asks random + withStore (\db -> createOrUpdateContactRequest db gVar vr user uclId ucl isSimplexTeam invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup) >>= \case + RSAcceptedRequest _ucr re -> case re of + REContact ct -> + -- TODO [short links] update request msg + toView $ CEvtContactRequestAlreadyAccepted user ct + REBusinessChat gInfo _clientMember -> + -- TODO [short links] update request msg + toView $ CEvtBusinessRequestAlreadyAccepted user gInfo + RSCurrentRequest ucr re_ repeatRequest -> case re_ of + Nothing -> toView $ CEvtReceivedContactRequest user ucr Nothing + Just (REContact ct) -> do -- TODO [short links] prevent duplicate items -- update welcome message if changed (send update event to UI) and add updated feature items. -- Do not created e2e item on repeat request - if newRequest + if repeatRequest then do + -- TODO [short links] update request msg + -- .... + acceptOrShow Nothing -- pass item? + else do -- TODO [short links] save sharedMsgId instead of the last Nothing let createItem content = createChatItem user (CDDirectRcv ct) False content Nothing Nothing void $ createItem $ CIRcvDirectE2EEInfo $ E2EInfo $ Just $ CR.pqSupportToEnc $ reqPQSup @@ -1261,79 +1276,56 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = aci <- createItem $ CIRcvMsgContent mc unlessM (asks $ coreApi . config) $ toView $ CEvtNewChatItems user [aci] pure aci - let cInfo = DirectChat ct - pure $ AChat SCTDirect $ case aci of - Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} - _ -> Chat cInfo [] emptyChatStats - else pure $ AChat SCTDirect $ Chat (DirectChat ct) [] emptyChatStats - toView $ CEvtReceivedContactRequest user cReq chat_ - Just AutoAccept {acceptIncognito} - | businessAddress -> - if isSimplexTeam && v < businessChatsVersion - then - maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedContactByXContactId db vr user xContactId)) xContactId_ >>= \case - Just ct -> toView $ CEvtContactRequestAlreadyAccepted user ct - Nothing -> do - ct <- acceptContactRequestAsync user uclId invId chatVRange p xContactId_ reqPQSup Nothing - -- TODO [short links] add welcome message if welcomeMsgId is present - -- forM_ autoReply $ \arMC -> - -- when (shortLinkDataSet && v >= shortLinkDataVersion) $ - -- createInternalChatItem user (CDDirectSnd ct) (CISndMsgContent arMC) Nothing - -- TODO [short links] save sharedMsgId - forM_ requestMsg_ $ \(sharedMsgId, mc) -> - createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing - toView $ CEvtAcceptingContactRequest user ct - else - maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedBusinessChatByXContactId db vr user xContactId)) xContactId_ >>= \case - Just gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo - Nothing -> do - (gInfo, clientMember) <- acceptBusinessJoinRequestAsync user uclId invId chatVRange p xContactId_ - -- TODO [short links] add welcome message if welcomeMsgId is present - -- forM_ autoReply $ \arMC -> - -- when (shortLinkDataSet && v >= shortLinkDataVersion) $ - -- createInternalChatItem user (CDGroupSnd gInfo Nothing) (CISndMsgContent arMC) Nothing - -- TODO [short links] save sharedMsgId - forM_ requestMsg_ $ \(sharedMsgId, mc) -> - createInternalChatItem user (CDGroupRcv gInfo Nothing clientMember) (CIRcvMsgContent mc) Nothing - toView $ CEvtAcceptingBusinessRequest user gInfo - | otherwise -> case gLinkInfo_ of - Nothing -> - maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedContactByXContactId db vr user xContactId)) xContactId_ >>= \case - Just ct -> toView $ CEvtContactRequestAlreadyAccepted user ct - Nothing -> do - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- - if not shortLinkDataSet && acceptIncognito - then Just . NewIncognito <$> liftIO generateRandomProfile - else pure Nothing - ct <- acceptContactRequestAsync user uclId invId chatVRange p xContactId_ reqPQSup incognitoProfile - -- TODO [short links] add welcome message if welcomeMsgId is present - -- forM_ autoReply $ \arMC -> - -- when (shortLinkDataSet && v >= shortLinkDataVersion) $ - -- createInternalChatItem user (CDDirectSnd ct) (CISndMsgContent arMC) Nothing - -- TODO [short links] save sharedMsgId - forM_ requestMsg_ $ \(sharedMsgId, mc) -> - createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing - toView $ CEvtAcceptingContactRequest user ct - Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - acceptMember_ <- asks $ acceptMember . chatHooks . config - maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case - Right (acceptance, useRole) - | v < groupFastLinkJoinVersion -> - messageError "processUserContactRequest: chat version range incompatible for accepting group join request" - | otherwise -> do - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo - mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p acceptance useRole profileMode - (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem - createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing - toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' - Left rjctReason - | v < groupJoinRejectVersion -> - messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" - | otherwise -> do - mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p rjctReason - toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason + acceptOrShow aci + where + acceptOrShow aci_ = + case autoAccept of + Nothing -> do + let cInfo = DirectChat ct + chat = AChat SCTDirect $ case aci_ of + Just (AChatItem SCTDirect dir _ ci) -> Chat cInfo [CChatItem dir ci] emptyChatStats {unreadCount = 1, minUnreadItemId = chatItemId' ci} + _ -> Chat cInfo [] emptyChatStats + toView $ CEvtReceivedContactRequest user ucr (Just chat) + Just AutoAccept {acceptIncognito} -> do + incognitoProfile <- + if not shortLinkDataSet && acceptIncognito + then Just . NewIncognito <$> liftIO generateRandomProfile + else pure Nothing + ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile + -- chat in event? + toView $ CEvtAcceptingContactRequest user ct' + Just (REBusinessChat gInfo clientMember) -> do + -- TODO [short links] prevent duplicate items (use repeatRequest like for REContact) + (_gInfo', _clientMember') <- acceptBusinessJoinRequestAsync user uclId gInfo clientMember ucr + -- TODO [short links] add welcome message if welcomeMsgId is present + -- forM_ autoReply $ \arMC -> + -- when (shortLinkDataSet && v >= shortLinkDataVersion) $ + -- createInternalChatItem user (CDGroupSnd gInfo Nothing) (CISndMsgContent arMC) Nothing + -- TODO [short links] save sharedMsgId + forM_ requestMsg_ $ \(sharedMsgId, mc) -> + createInternalChatItem user (CDGroupRcv gInfo Nothing clientMember) (CIRcvMsgContent mc) Nothing + toView $ CEvtAcceptingBusinessRequest user gInfo + -- ##### Group link join requests (don't create contact requests) ##### + Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do + -- TODO [short links] deduplicate request by xContactId? + gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + acceptMember_ <- asks $ acceptMember . chatHooks . config + maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case + Right (acceptance, useRole) + | v < groupFastLinkJoinVersion -> + messageError "processUserContactRequest: chat version range incompatible for accepting group join request" + | otherwise -> do + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo + mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p xContactId_ acceptance useRole profileMode + (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem + createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing + toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem' + Left rjctReason + | v < groupJoinRejectVersion -> + messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked" + | otherwise -> do + mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p xContactId_ rjctReason + toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason memberCanSend :: GroupMember -> Maybe MsgScope -> CM () -> CM () memberCanSend m@GroupMember {memberRole} msgScope a = case msgScope of diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs new file mode 100644 index 0000000000..9fae87eaa1 --- /dev/null +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -0,0 +1,299 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Store.ContactRequest + ( createOrUpdateContactRequest, + setContactAcceptedXContactId, + setBusinessChatAcceptedXContactId + ) + where + +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Class +import Crypto.Random (ChaChaDRG) +import Data.Int (Int64) +import Data.Time.Clock (getCurrentTime) +import Simplex.Chat.Protocol (businessChatsVersion, MsgContent) +import Simplex.Chat.Store.Direct +import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Shared +import Simplex.Chat.Store.Profiles +import Simplex.Chat.Types +import Simplex.Chat.Types.Preferences +import Simplex.Messaging.Agent.Protocol (InvitationId) +import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow) +import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) +import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Crypto.Ratchet (PQSupport) +import Simplex.Messaging.Version +import UnliftIO.STM +#if defined(dbPostgres) +import Database.PostgreSQL.Simple ((:.) (..)) +import Database.PostgreSQL.Simple.SqlQQ (sql) +#else +import Database.SQLite.Simple ((:.) (..)) +import Database.SQLite.Simple.QQ (sql) +#endif + +createOrUpdateContactRequest :: + DB.Connection + -> TVar ChaChaDRG + -> VersionRangeChat + -> User + -> Int64 + -> UserContactLink + -> Bool + -> InvitationId + -> VersionRangeChat + -> Profile + -> Maybe XContactId + -> Maybe SharedMsgId + -> Maybe (SharedMsgId, MsgContent) + -> PQSupport + -> ExceptT StoreError IO RequestStage +createOrUpdateContactRequest + db + gVar + vr + user@User {userId, userContactId} + uclId + UserContactLink {addressSettings = AddressSettings {businessAddress}} + isSimplexTeam + invId + cReqChatVRange@(VersionRange minV maxV) + profile@Profile {displayName, fullName, image, contactLink, preferences} + xContactId_ + welcomeMsgId_ + requestMsg_ + pqSup = + case xContactId_ of + -- 0) this is very old legacy, when we didn't have xContactId at all (this should be deprecated) + Nothing -> createContactRequest + Just xContactId -> + -- 1) first we try to find accepted contact or business chat by xContactId + liftIO (getAcceptedContact xContactId) >>= \case + Just ct -> pure $ RSAcceptedRequest Nothing (REContact ct) + Nothing -> liftIO (getAcceptedBusinessChat xContactId) >>= \case + Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do + clientMember <- getGroupMemberByMemberId db vr user gInfo customerId + pure $ RSAcceptedRequest Nothing (REBusinessChat gInfo clientMember) + Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest + -- 2) if no legacy accepted contact or business chat was found, next we try to find an existing request + Nothing -> + liftIO (getContactRequestByXContactId xContactId) >>= \case + -- 3a) if request was found, we update it + Just cr -> updateContactRequest cr + -- 3b) if no request was found, we create a new contact request + Nothing -> createContactRequest + where + getAcceptedContact :: XContactId -> IO (Maybe Contact) + getAcceptedContact xContactId = do + ct_ <- + maybeFirstRow (toContact vr user []) $ + DB.query + db + [sql| + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + |] + (userId, xContactId) + mapM (addDirectChatTags db) ct_ + getAcceptedBusinessChat :: XContactId -> IO (Maybe GroupInfo) + getAcceptedBusinessChat xContactId = do + g_ <- + maybeFirstRow (toGroupInfo vr userContactId []) $ + DB.query + db + (groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?") + (xContactId, userId, userContactId) + mapM (addGroupChatTags db) g_ + getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest) + getContactRequestByXContactId xContactId = + maybeFirstRow toContactRequest $ + DB.query + db + [sql| + SELECT + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version + FROM contact_requests cr + JOIN connections c USING (user_contact_link_id) + JOIN contact_profiles p USING (contact_profile_id) + WHERE cr.user_id = ? + AND cr.xcontact_id = ? + LIMIT 1 + |] + (userId, xContactId) + createContactRequest :: ExceptT StoreError IO RequestStage + createContactRequest = do + currentTs <- liftIO $ getCurrentTime + ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do + liftIO $ + DB.execute + db + "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" + (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) + profileId <- liftIO $ insertedRowId db + liftIO $ + DB.execute + db + [sql| + INSERT INTO contact_requests + (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, + created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (uclId, Binary invId, minV, maxV, profileId, ldn, userId) + :. (currentTs, currentTs, xContactId_, welcomeMsgId_, fst <$> requestMsg_, pqSup) + ) + contactRequestId <- liftIO $ insertedRowId db + createRequestEntity ldn profileId contactRequestId currentTs + where + createRequestEntity ldn profileId contactRequestId currentTs + | businessAddress = + if isSimplexTeam && maxV < businessChatsVersion + then createContact' + else createBusinessChat + | otherwise = createContact' + where + createContact' = do + liftIO $ + DB.execute + db + "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?)" + (profileId, ldn, userId, currentTs, currentTs, currentTs, BI True, contactRequestId) + contactId <- liftIO $ insertedRowId db + liftIO $ + DB.execute + db + "UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?" + (contactId, contactRequestId) + ucr <- getContactRequest db user contactRequestId + ct <- getContact db vr user contactId + pure $ RSCurrentRequest ucr (Just $ REContact ct) False + createBusinessChat = do + let Profile {preferences = userPreferences} = profileToSendOnAccept user Nothing True + groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs userPreferences + (gInfo@GroupInfo {groupId}, clientMember) <- + createBusinessRequestGroup db vr gVar user cReqChatVRange profile profileId ldn groupPreferences + liftIO $ + DB.execute + db + "UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ?" + (groupId, contactRequestId) + ucr <- getContactRequest db user contactRequestId + pure $ RSCurrentRequest ucr (Just $ REBusinessChat gInfo clientMember) False + updateContactRequest :: UserContactRequest -> ExceptT StoreError IO RequestStage + updateContactRequest UserContactRequest {contactRequestId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do + currentTs <- liftIO getCurrentTime + liftIO $ updateProfile currentTs + updateRequest currentTs + ucr' <- getContactRequest db user contactRequestId + re_ <- getRequestEntity ucr' + pure $ RSCurrentRequest ucr' re_ True + where + updateProfile currentTs = + DB.execute + db + [sql| + UPDATE contact_profiles + SET display_name = ?, + full_name = ?, + image = ?, + contact_link = ?, + updated_at = ? + WHERE contact_profile_id IN ( + SELECT contact_profile_id + FROM contact_requests + WHERE user_id = ? + AND contact_request_id = ? + ) + |] + (displayName, fullName, image, contactLink, currentTs, userId, contactRequestId) + updateRequest currentTs = + if displayName == oldDisplayName + then + liftIO $ + DB.execute + db + [sql| + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + |] + (Binary invId, pqSup, minV, maxV, currentTs, userId, contactRequestId) + else + ExceptT $ withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do + liftIO $ do + DB.execute + db + [sql| + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + |] + (Binary invId, pqSup, minV, maxV, ldn, currentTs, userId, contactRequestId) + -- Here we could also update business chat, but is always synchronously auto-accepted so it's less of an issue + forM_ contactId_ $ \contactId -> + DB.execute + db + [sql| + UPDATE contacts + SET local_display_name = ?, updated_at = ? + WHERE contact_id = ? + |] + (ldn, currentTs, contactId) + safeDeleteLDN db user oldLdn + getRequestEntity :: UserContactRequest -> ExceptT StoreError IO (Maybe RequestEntity) + getRequestEntity UserContactRequest {contactRequestId, contactId_, businessGroupId_} = + case (contactId_, businessGroupId_) of + (Just contactId, Nothing) -> do + ct <- getContact db vr user contactId + pure $ Just (REContact ct) + (Nothing, Just businessGroupId) -> do + gInfo <- getGroupInfo db vr user businessGroupId + case gInfo of + GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do + clientMember <- getGroupMemberByMemberId db vr user gInfo customerId + pure $ Just (REBusinessChat gInfo clientMember) + _ -> throwError SEInvalidBusinessChatContactRequest + (Nothing, Nothing) -> pure Nothing + _ -> throwError $ SEInvalidContactRequestEntity contactRequestId + +setContactAcceptedXContactId :: DB.Connection -> Contact -> XContactId -> IO () +setContactAcceptedXContactId db Contact {contactId} xContactId = + DB.execute + db "UPDATE contacts SET xcontact_id = ? WHERE contact_id = ?" + (xContactId, contactId) + +setBusinessChatAcceptedXContactId :: DB.Connection -> GroupInfo -> XContactId -> IO () +setBusinessChatAcceptedXContactId db GroupInfo {groupId} xContactId = + DB.execute + db "UPDATE groups SET business_xcontact_id = ? WHERE group_id = ?" + (xContactId, groupId) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 355f56695e..3d7c15b6d6 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -57,9 +57,6 @@ module Simplex.Chat.Store.Direct incQuotaErrCounter, setQuotaErrCounter, getUserContacts, - createOrUpdateContactRequest, - getAcceptedContactByXContactId, - getAcceptedBusinessChatByXContactId, getUserContactLinkIdByCReq, getContactRequest, getContactRequestIdByName, @@ -67,7 +64,6 @@ module Simplex.Chat.Store.Direct createContactFromRequest, createAcceptedContactConn, createAcceptedContact, - deleteContactRequestRec, updateContactAccepted, getUserByContactRequestId, getPendingContactConnections, @@ -83,6 +79,7 @@ module Simplex.Chat.Store.Direct setContactUIThemes, setContactChatDeleted, getDirectChatTags, + addDirectChatTags, updateDirectChatTags, setDirectChatTTL, getDirectChatTTL, @@ -104,14 +101,12 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ACreatedConnLink (..), ConnId, CreatedConnLink (..), InvitationId, UserId, connMode) +import Simplex.Messaging.Agent.Protocol (ACreatedConnLink (..), ConnId, CreatedConnLink (..), UserId, connMode) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) -import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) +import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Protocol (SubscriptionMode (..)) -import Simplex.Messaging.Util ((<$$>)) -import Simplex.Messaging.Version #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..), (:.) (..)) import Database.PostgreSQL.Simple.SqlQQ (sql) @@ -679,179 +674,6 @@ getUserContacts db vr user@User {userId} = do contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ChatOrRequest -createOrUpdateContactRequest - db - vr - user@User {userId} - uclId - invId - (VersionRange minV maxV) - Profile {displayName, fullName, image, contactLink, preferences} - xContactId_ - pqSup = - -- this doesn't return a newly created contact request with contact, - -- because we only set xContactId after contact is accepted - -- (this allows older clients to update contact request by reusing xContactId) - liftIO (maybeM (getAcceptedContactByXContactId db vr user) xContactId_) >>= \case - Just ct -> pure $ CORContact ct - Nothing -> do - (ucr, ct_, newRequest) <- createOrUpdateRequest - pure $ CORRequest ucr ct_ newRequest - where - maybeM = maybe (pure Nothing) - createOrUpdateRequest :: ExceptT StoreError IO (UserContactRequest, Maybe Contact, Bool) - createOrUpdateRequest = do - (cReqId, newRequest) <- - ExceptT $ - maybeM getContactRequestByXContactId xContactId_ >>= \case - Nothing -> (,True) <$$> createContactRequest - Just cr@UserContactRequest {contactRequestId} -> updateContactRequest cr $> Right (contactRequestId, False) - ucr@UserContactRequest {contactId_} <- getContactRequest db user cReqId - ct_ <- forM contactId_ $ \contactId -> getContact db vr user contactId - pure (ucr, ct_, newRequest) - createContactRequest :: IO (Either StoreError Int64) - createContactRequest = do - currentTs <- getCurrentTime - withLocalDisplayName db userId displayName (fmap Right . createContactRequest_ currentTs) - where - createContactRequest_ currentTs ldn = do - DB.execute - db - "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) - profileId <- insertedRowId db - DB.execute - db - [sql| - INSERT INTO contact_requests - (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, - created_at, updated_at, xcontact_id, pq_support) - VALUES (?,?,?,?,?,?,?,?,?,?,?) - |] - ( (uclId, Binary invId, minV, maxV, profileId, ldn, userId) - :. (currentTs, currentTs, xContactId_, pqSup) - ) - contactRequestId <- insertedRowId db - DB.execute - db - "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?)" - (profileId, ldn, userId, currentTs, currentTs, currentTs, BI True, contactRequestId) - contactId <- insertedRowId db - DB.execute - db - "UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?" - (contactId, contactRequestId) - pure contactRequestId - getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest) - getContactRequestByXContactId xContactId = - maybeFirstRow toContactRequest $ - DB.query - db - [sql| - SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, - cr.peer_chat_min_version, cr.peer_chat_max_version - FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) - JOIN contact_profiles p USING (contact_profile_id) - WHERE cr.user_id = ? - AND cr.xcontact_id = ? - LIMIT 1 - |] - (userId, xContactId) - updateContactRequest :: UserContactRequest -> IO (Either StoreError ()) - updateContactRequest UserContactRequest {contactRequestId = cReqId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do - currentTs <- liftIO getCurrentTime - updateProfile currentTs - if displayName == oldDisplayName - then - Right - <$> DB.execute - db - [sql| - UPDATE contact_requests - SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? - WHERE user_id = ? AND contact_request_id = ? - |] - (Binary invId, pqSup, minV, maxV, currentTs, userId, cReqId) - else withLocalDisplayName db userId displayName $ \ldn -> - Right <$> do - DB.execute - db - [sql| - UPDATE contact_requests - SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? - WHERE user_id = ? AND contact_request_id = ? - |] - (Binary invId, pqSup, minV, maxV, ldn, currentTs, userId, cReqId) - forM_ contactId_ $ \contactId -> - DB.execute - db - [sql| - UPDATE contacts - SET local_display_name = ?, updated_at = ? - WHERE contact_id = ? - |] - (ldn, currentTs, contactId) - safeDeleteLDN db user oldLdn - where - updateProfile currentTs = - DB.execute - db - [sql| - UPDATE contact_profiles - SET display_name = ?, - full_name = ?, - image = ?, - contact_link = ?, - updated_at = ? - WHERE contact_profile_id IN ( - SELECT contact_profile_id - FROM contact_requests - WHERE user_id = ? - AND contact_request_id = ? - ) - |] - (displayName, fullName, image, contactLink, currentTs, userId, cReqId) - -getAcceptedContactByXContactId :: DB.Connection -> VersionRangeChat -> User -> XContactId -> IO (Maybe Contact) -getAcceptedContactByXContactId db vr user@User {userId} xContactId = do - ct_ <- - maybeFirstRow (toContact vr user []) $ - DB.query - db - [sql| - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - LEFT JOIN connections c ON c.contact_id = ct.contact_id - WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - |] - (userId, xContactId) - mapM (addDirectChatTags db) ct_ - -getAcceptedBusinessChatByXContactId :: DB.Connection -> VersionRangeChat -> User -> XContactId -> IO (Maybe GroupInfo) -getAcceptedBusinessChatByXContactId db vr User {userId, userContactId} xContactId = do - g_ <- - maybeFirstRow (toGroupInfo vr userContactId []) $ - DB.query - db - (groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?") - (xContactId, userId, userContactId) - mapM (addGroupChatTags db) g_ - getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64 getUserContactLinkIdByCReq db contactRequestId = ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $ @@ -864,8 +686,11 @@ getContactRequest db User {userId} contactRequestId = db [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) @@ -983,10 +808,6 @@ createAcceptedContact ct <- getContact db vr user contactId pure (ct, conn) -deleteContactRequestRec :: DB.Connection -> User -> UserContactRequest -> IO () -deleteContactRequestRec db User {userId} UserContactRequest {contactRequestId} = - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) - updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO () updateContactAccepted db User {userId} Contact {contactId} contactUsed = DB.execute diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 7ee3420896..e9462988df 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -1230,7 +1230,7 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo :. (minV, maxV) ) -createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) +createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId) createJoiningMember db gVar @@ -1238,6 +1238,7 @@ createJoiningMember GroupInfo {groupId, membership} cReqChatVRange Profile {displayName, fullName, image, contactLink, preferences} + cReqXContactId_ memberRole memberStatus = do currentTs <- liftIO getCurrentTime @@ -1260,12 +1261,12 @@ createJoiningMember [sql| INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (groupId, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership) - :. (userId, ldn, Nothing :: (Maybe Int64), profileId, currentTs, currentTs) + :. (userId, ldn, Nothing :: (Maybe Int64), profileId, cReqXContactId_, currentTs, currentTs) :. (minV, maxV) ) @@ -1283,7 +1284,7 @@ createJoiningMemberConnection Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just uclId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId -createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) +createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Int64 -> Text -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) createBusinessRequestGroup db vr @@ -1291,7 +1292,8 @@ createBusinessRequestGroup user@User {userId, userContactId} cReqChatVRange Profile {displayName, fullName, image, contactLink, preferences} - xContactId + profileId -- contact request profile id, to be used for member profile + ldn -- contact request local display name, to be used for group local display name groupPreferences = do currentTs <- liftIO getCurrentTime (groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs @@ -1301,36 +1303,30 @@ createBusinessRequestGroup clientMember <- getGroupMemberById db vr user groupMemberId pure (groupInfo, clientMember) where - insertGroup_ currentTs = ExceptT $ - withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do - groupId <- liftIO $ do - DB.execute - db - "INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (displayName, fullName, image, userId, groupPreferences, currentTs, currentTs) - profileId <- insertedRowId db - DB.execute - db - [sql| - INSERT INTO groups - (group_profile_id, local_display_name, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_xcontact_id) - VALUES (?,?,?,?,?,?,?,?,?,?) - |] - (profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs, BCCustomer, xContactId) - insertedRowId db - memberId <- liftIO $ encodedRandomBytes gVar 12 - membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr - pure (groupId, membership) + insertGroup_ currentTs = do + liftIO $ + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, image, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (displayName, fullName, image, userId, groupPreferences, currentTs, currentTs) + groupProfileId <- liftIO $ insertedRowId db + liftIO $ + DB.execute + db + [sql| + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat) + VALUES (?,?,?,?,?,?,?,?,?) + |] + (groupProfileId, ldn, userId, BI True, currentTs, currentTs, currentTs, currentTs, BCCustomer) + groupId <- liftIO $ insertedRowId db + memberId <- liftIO $ encodedRandomBytes gVar 12 + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing currentTs vr + pure (groupId, membership) VersionRange minV maxV = cReqChatVRange insertClientMember_ currentTs groupId membership = ExceptT $ do withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do - liftIO $ - DB.execute - db - "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) - profileId <- liftIO $ insertedRowId db createWithRandomId gVar $ \memId -> do DB.execute db diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index df97e0ea91..7a28bc371e 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1049,8 +1049,10 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of query = [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr @@ -1062,6 +1064,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of AND uc.local_display_name = '' AND uc.group_id IS NULL AND cr.contact_id IS NULL + AND cr.business_group_id IS NULL AND ( LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 93876b0706..bc18f5e7f3 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -413,32 +413,6 @@ deleteUserAddress db user@User {userId} = do ) |] (Only userId) - DB.execute - db - [sql| - DELETE FROM display_names - WHERE user_id = ? - AND local_display_name in ( - SELECT cr.local_display_name - FROM contact_requests cr - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - ) - AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) - |] - (userId, userId, userId) - DB.execute - db - [sql| - DELETE FROM contact_profiles - WHERE contact_profile_id in ( - SELECT cr.contact_profile_id - FROM contact_requests cr - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - ) - |] - (Only userId) void $ setUserProfileContactLink db user Nothing DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL" (Only userId) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs index ac6bc20880..a2fa9c3d11 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs @@ -15,6 +15,13 @@ ALTER TABLE contacts ADD COLUMN welcome_shared_msg_id BLOB; ALTER TABLE contacts ADD COLUMN contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL; CREATE INDEX idx_contacts_contact_request_id ON contacts(contact_request_id); +ALTER TABLE contact_requests ADD COLUMN business_group_id INTEGER REFERENCES groups(group_id) ON DELETE CASCADE; +CREATE INDEX idx_contact_requests_business_group_id ON contact_requests(business_group_id); +ALTER TABLE contact_requests ADD COLUMN welcome_shared_msg_id BLOB; +ALTER TABLE contact_requests ADD COLUMN request_shared_msg_id BLOB; + +ALTER TABLE group_members ADD COLUMN member_xcontact_id BLOB; + ALTER TABLE user_contact_links ADD COLUMN short_link_data_set INTEGER NOT NULL DEFAULT 0; ALTER TABLE user_contact_links ADD COLUMN address_welcome_message TEXT; @@ -36,6 +43,13 @@ ALTER TABLE contacts DROP COLUMN welcome_shared_msg_id; DROP INDEX idx_contacts_contact_request_id; ALTER TABLE contacts DROP COLUMN contact_request_id; +DROP INDEX idx_contact_requests_business_group_id; +ALTER TABLE contact_requests DROP COLUMN business_group_id; +ALTER TABLE contact_requests DROP COLUMN welcome_shared_msg_id; +ALTER TABLE contact_requests DROP COLUMN request_shared_msg_id; + +ALTER TABLE group_members DROP COLUMN member_xcontact_id; + ALTER TABLE user_contact_links DROP COLUMN short_link_data_set; ALTER TABLE user_contact_links DROP COLUMN address_welcome_message; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 2d08304f3f..a258eac2ab 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1,11 +1,27 @@ Query: - UPDATE contacts - SET local_display_name = ?, updated_at = ? - WHERE contact_id = ? - + UPDATE contacts + SET local_display_name = ?, updated_at = ? + WHERE contact_id = ? + Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + +Query: + UPDATE contact_requests + SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET chat_ts = ?, @@ -41,14 +57,6 @@ Query: Plan: -Query: - INSERT INTO groups - (group_profile_id, local_display_name, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_xcontact_id) - VALUES (?,?,?,?,?,?,?,?,?,?) - -Plan: - Query: SELECT -- GroupInfo @@ -94,22 +102,6 @@ Query: Plan: SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) -Query: - UPDATE contact_requests - SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? - WHERE user_id = ? AND contact_request_id = ? - -Plan: -SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) - -Query: - UPDATE contact_requests - SET agent_invitation_id = ?, pq_support = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? - WHERE user_id = ? AND contact_request_id = ? - -Plan: -SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE group_members SET support_chat_ts = ?, @@ -155,8 +147,8 @@ SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) Query: INSERT INTO contact_requests (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, - created_at, updated_at, xcontact_id, pq_support) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + created_at, updated_at, xcontact_id, welcome_shared_msg_id, request_shared_msg_id, pq_support) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -193,6 +185,37 @@ Query: Plan: +Query: + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat) + VALUES (?,?,?,?,?,?,?,?,?) + +Plan: + +Query: + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + LEFT JOIN connections c ON c.contact_id = ct.contact_id + WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + +Plan: +SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN +USE TEMP B-TREE FOR ORDER BY + Query: SELECT COUNT(1) FROM chat_items i @@ -335,16 +358,16 @@ Plan: Query: INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: Query: INSERT INTO group_members ( group_id, member_id, member_role, member_category, member_status, invited_by, invited_by_group_member_id, - user_id, local_display_name, contact_id, contact_profile_id, member_profile_id, created_at, updated_at, + user_id, local_display_name, contact_id, contact_profile_id, member_xcontact_id, created_at, updated_at, peer_chat_min_version, peer_chat_max_version) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) @@ -373,8 +396,11 @@ SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) @@ -384,7 +410,7 @@ Query: LIMIT 1 Plan: -SEARCH cr USING INDEX idx_contact_requests_xcontact_id (xcontact_id=?) +SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) @@ -913,29 +939,6 @@ SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) USE TEMP B-TREE FOR ORDER BY -Query: - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - LEFT JOIN connections c ON c.contact_id = ct.contact_id - WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - -Plan: -SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?) -SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN -USE TEMP B-TREE FOR ORDER BY - Query: SELECT -- GroupInfo @@ -1601,8 +1604,10 @@ USE TEMP B-TREE FOR ORDER BY Query: SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr @@ -1614,6 +1619,7 @@ Query: AND uc.local_display_name = '' AND uc.group_id IS NULL AND cr.contact_id IS NULL + AND cr.business_group_id IS NULL AND ( LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' @@ -1628,8 +1634,10 @@ SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id= Query: SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr @@ -1641,6 +1649,7 @@ Query: AND uc.local_display_name = '' AND uc.group_id IS NULL AND cr.contact_id IS NULL + AND cr.business_group_id IS NULL AND ( LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' @@ -1655,8 +1664,10 @@ SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id= Query: SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr @@ -1668,6 +1679,7 @@ Query: AND uc.local_display_name = '' AND uc.group_id IS NULL AND cr.contact_id IS NULL + AND cr.business_group_id IS NULL AND ( LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%' OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%' @@ -1682,8 +1694,11 @@ SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id= Query: SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, p.preferences, cr.created_at, cr.updated_at, + cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, + cr.contact_id, cr.business_group_id, cr.user_contact_link_id, + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, + cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) @@ -3803,26 +3818,6 @@ SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (m SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) -Query: - DELETE FROM contact_profiles - WHERE contact_profile_id in ( - SELECT cr.contact_profile_id - FROM contact_requests cr - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - ) - -Plan: -SEARCH contact_profiles USING INTEGER PRIMARY KEY (rowid=?) -LIST SUBQUERY 1 -SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) -SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) -SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) -SEARCH connections USING COVERING INDEX idx_connections_custom_user_profile_id (custom_user_profile_id=?) -SEARCH group_members USING COVERING INDEX idx_group_members_member_profile_id (member_profile_id=?) -SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) -SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_profile_id=?) - Query: DELETE FROM contact_profiles WHERE user_id = ? AND contact_profile_id = ? @@ -3954,30 +3949,6 @@ SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND loca SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) SEARCH users USING INTEGER PRIMARY KEY (rowid=?) -Query: - DELETE FROM display_names - WHERE user_id = ? - AND local_display_name in ( - SELECT cr.local_display_name - FROM contact_requests cr - JOIN user_contact_links uc USING (user_contact_link_id) - WHERE uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL - ) - AND local_display_name NOT IN (SELECT local_display_name FROM users WHERE user_id = ?) - -Plan: -SEARCH display_names USING PRIMARY KEY (user_id=? AND local_display_name=?) -LIST SUBQUERY 1 -SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) -SEARCH cr USING INDEX idx_contact_requests_user_contact_link_id (user_contact_link_id=?) -LIST SUBQUERY 2 -SEARCH users USING INTEGER PRIMARY KEY (rowid=?) -SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) -SEARCH group_members USING COVERING INDEX idx_group_members_user_id_local_display_name (user_id=? AND local_display_name=?) -SEARCH groups USING COVERING INDEX sqlite_autoindex_groups_1 (user_id=? AND local_display_name=?) -SEARCH contacts USING COVERING INDEX sqlite_autoindex_contacts_1 (user_id=? AND local_display_name=?) -SEARCH users USING INTEGER PRIMARY KEY (rowid=?) - Query: DELETE FROM display_names WHERE user_id = ? AND local_display_name = ( @@ -5491,6 +5462,7 @@ SEARCH chat_item_reactions USING COVERING INDEX idx_chat_item_reactions_group_id SEARCH chat_items USING COVERING INDEX idx_chat_items_fwd_from_group_id (fwd_from_group_id=?) SEARCH chat_items USING COVERING INDEX idx_chat_items_group_id (group_id=?) SEARCH messages USING COVERING INDEX idx_messages_group_id (group_id=?) +SEARCH contact_requests USING COVERING INDEX idx_contact_requests_business_group_id (business_group_id=?) SEARCH user_contact_links USING COVERING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH files USING COVERING INDEX idx_files_group_id (group_id=?) SEARCH group_members USING COVERING INDEX sqlite_autoindex_group_members_1 (group_id=?) @@ -5659,10 +5631,6 @@ Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, vi Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) -Query: INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?) -Plan: -SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) - Query: INSERT INTO display_names (local_display_name, ldn_base, user_id, created_at, updated_at) VALUES (?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?) @@ -6034,6 +6002,10 @@ Query: UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1 Plan: SEARCH connections USING INDEX idx_connections_to_subscribe (to_subscribe=?) +Query: UPDATE contact_requests SET business_group_id = ? WHERE contact_request_id = ? +Plan: +SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ? Plan: SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) @@ -6086,6 +6058,10 @@ Query: UPDATE contacts SET user_preferences = ?, updated_at = ? WHERE user_id = Plan: SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE contacts SET xcontact_id = ? WHERE contact_id = ? +Plan: +SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ? Plan: SEARCH files USING INTEGER PRIMARY KEY (rowid=?) @@ -6162,6 +6138,10 @@ Query: UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE gr Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET business_xcontact_id = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET chat_item_id = ?, updated_at = ? WHERE user_id = ? AND group_id = ? Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 3968b13ee8..78207bc64f 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -181,6 +181,7 @@ CREATE TABLE group_members( support_chat_items_member_attention INTEGER NOT NULL DEFAULT 0, support_chat_items_mentions INTEGER NOT NULL DEFAULT 0, support_chat_last_msg_from_member_ts TEXT, + member_xcontact_id BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -355,6 +356,9 @@ CREATE TABLE contact_requests( peer_chat_max_version INTEGER NOT NULL DEFAULT 1, pq_support INTEGER NOT NULL DEFAULT 0, contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + business_group_id INTEGER REFERENCES groups(group_id) ON DELETE CASCADE, + welcome_shared_msg_id BLOB, + request_shared_msg_id BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON UPDATE CASCADE @@ -1060,3 +1064,6 @@ CREATE INDEX idx_chat_items_group_scope_item_status ON chat_items( item_ts ); CREATE INDEX idx_contacts_contact_request_id ON contacts(contact_request_id); +CREATE INDEX idx_contact_requests_business_group_id ON contact_requests( + business_group_id +); diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index bcef2c6ef7..afd7915256 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -84,6 +84,8 @@ data StoreError | SEUserContactLinkNotFound | SEContactRequestNotFound {contactRequestId :: Int64} | SEContactRequestNotFoundByName {contactName :: ContactName} + | SEInvalidContactRequestEntity {contactRequestId :: Int64} + | SEInvalidBusinessChatContactRequest | SEGroupNotFound {groupId :: GroupId} | SEGroupNotFoundByName {groupName :: GroupName} | SEGroupMemberNameNotFound {groupId :: GroupId, groupMemberName :: ContactName} @@ -470,13 +472,13 @@ getProfileById db userId profileId = toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Int64) :. (AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId) :. (agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do let profile = Profile {displayName, fullName, image, contactLink, preferences} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in UserContactRequest {contactRequestId, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt} + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} userQuery :: Query userQuery = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index b806def1bd..c9059dace7 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -21,6 +21,7 @@ {-# LANGUAGE TypeFamilyDependencies #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} {-# HLINT ignore "Use newtype instead of data" #-} @@ -38,7 +39,7 @@ import Data.ByteString.Char8 (ByteString, pack, unpack) import qualified Data.ByteString.Lazy as LB import Data.Functor (($>)) import Data.Int (Int64) -import Data.Maybe (isJust) +import Data.Maybe (fromMaybe, isJust) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) @@ -344,6 +345,7 @@ data UserContactRequest = UserContactRequest { contactRequestId :: Int64, agentInvitationId :: AgentInvId, contactId_ :: Maybe ContactId, + businessGroupId_ :: Maybe GroupId, userContactLinkId :: Int64, agentContactConnId :: AgentConnId, -- connection id of user contact cReqChatVRange :: VersionRangeChat, @@ -353,7 +355,9 @@ data UserContactRequest = UserContactRequest createdAt :: UTCTime, updatedAt :: UTCTime, xContactId :: Maybe XContactId, - pqSupport :: PQSupport + pqSupport :: PQSupport, + welcomeSharedMsgId :: Maybe SharedMsgId, + requestSharedMsgId :: Maybe SharedMsgId } deriving (Eq, Show) @@ -393,15 +397,15 @@ instance ToJSON ConnReqUriHash where toJSON = strToJSON toEncoding = strToJEncoding --- TODO [short links] this type is most likely incorrect, as it does not communicate when contact exists as opposed to when it is --- just created, as was the original intention. --- It also has no information when group exists on repeat requests. --- Most likely, whatever information from request is needed should have been added to CORContact (or inside Contact), --- instead of passing Maybe contact in request. -data ChatOrRequest - = CORContact Contact - -- Contact is Maybe for backward compatibility with legacy requests, all new requests are created with contact - | CORRequest UserContactRequest (Maybe Contact) Bool +data RequestEntity + = REContact Contact + | REBusinessChat GroupInfo GroupMember + +type RepeatRequest = Bool + +data RequestStage + = RSAcceptedRequest (Maybe UserContactRequest) RequestEntity -- Optional request is for legacy deleted requests + | RSCurrentRequest UserContactRequest (Maybe RequestEntity) RepeatRequest -- Optional entity is for legacy requests without entity type UserName = Text @@ -634,6 +638,22 @@ redactedMemberProfile Profile {displayName, fullName, image} = data IncognitoProfile = NewIncognito Profile | ExistingIncognito LocalProfile +profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile +profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing + where + getIncognitoProfile = \case + NewIncognito p -> p + ExistingIncognito lp -> fromLocalProfile lp + +userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile +userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do + let p' = fromMaybe (fromLocalProfile p) incognitoProfile + if inGroup + then redactedMemberProfile p' + else + let userPrefs = maybe (preferences' user) (const Nothing) incognitoProfile + in (p' :: Profile) {preferences = Just . toChatPrefs $ mergePreferences (userPreferences <$> ct) userPrefs} + type LocalAlias = Text data LocalProfile = LocalProfile diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 43f926db0d..1e65707b82 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -47,7 +47,11 @@ chatProfileTests = do it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "reject contact and delete contact link" testRejectContactAndDeleteUserContact - it "delete connection requests when contact link deleted" testDeleteConnectionRequests + -- TODO [short links] fix address deletion: + -- TODO - either alert user that N chats will be deleted and delete contact request contacts and business chats + -- TODO - or allow to accept contact requests for deleted address (remove cascade deletes, rework agent) + xit "delete connection requests when contact link deleted" testDeleteConnectionRequests + it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks -- TODO [short links] test auto-reply with current version, with connecting client not preparing contact it "auto-reply message" testAutoReplyMessage it "auto-reply message in incognito" testAutoReplyMessageInIncognito @@ -675,6 +679,30 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ cath ##> ("/c " <> cLink') alice <#? cath +testContactLinkDeletedConnectedContactWorks :: HasCallStack => TestParams -> IO () +testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob ##> ("/c " <> cLink) + alice <#? bob + + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice @@@ [("@bob", "Audio/video calls: enabled")] + bob @@@ [("@alice", "Audio/video calls: enabled")] + + alice ##> "/da" + alice <## "Your chat address is deleted - accepted contacts will remain connected." + alice <## "To create a new chat address use /ad" + + alice <##> bob + alice @@@ [("@bob", "hey")] + bob @@@ [("@alice", "hey")] + testAutoReplyMessage :: HasCallStack => TestParams -> IO () testAutoReplyMessage = testChatCfg2 testCfgNoShortLinks aliceProfile bobProfile $ \alice bob -> do