diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 176da2481e..efed487739 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -996,7 +996,11 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? // case let .contactConnecting(contact): // TODO profile update case let .receivedContactRequest(user, contactRequest): - return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest)) + if let userContactLinkId = contactRequest.userContactLinkId_ { + return (UserContact(userContactLinkId: userContactLinkId).id, .contactRequest(user, contactRequest)) + } else { + return nil + } case let .newChatItems(user, chatItems): // Received items are created one at a time if let chatItem = chatItems.first { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 28780b21a9..3f7922e9b8 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1908,10 +1908,6 @@ public struct UserContact: Decodable, Hashable { self.userContactLinkId = userContactLinkId } - public init(contactRequest: UserContactRequest) { - self.userContactLinkId = contactRequest.userContactLinkId - } - public var id: String { "@>\(userContactLinkId)" } @@ -1919,7 +1915,7 @@ public struct UserContact: Decodable, Hashable { public struct UserContactRequest: Decodable, NamedChat, Hashable { var contactRequestId: Int64 - public var userContactLinkId: Int64 + public var userContactLinkId_: Int64? public var cReqChatVRange: VersionRange var localDisplayName: ContactName var profile: Profile @@ -1936,7 +1932,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { public static let sampleData = UserContactRequest( contactRequestId: 1, - userContactLinkId: 1, + userContactLinkId_: 1, cReqChatVRange: VersionRange(1, 1), localDisplayName: "alice", profile: Profile.sampleData, diff --git a/cabal.project b/cabal.project index cd29f46132..69768be71c 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a46edd60f0e2460ec4a7d6fb371412f32e988357 + tag: c5eb66038bb9268671a353638606f6d0bd1de761 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 79d0c22732..e497bee486 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a46edd60f0e2460ec4a7d6fb371412f32e988357" = "0vix4s9nfxrwiy56rc294s9ik7l9rar4zg2pvl1c4v914lsphybq"; + "https://github.com/simplex-chat/simplexmq.git"."c5eb66038bb9268671a353638606f6d0bd1de761" = "08b1lqwpmcn139c09lr71gmh5kgxlcdfhvwlnxxafr26m4nc003w"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e4810dad3b..51117f9b72 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -108,6 +108,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links + Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete else exposed-modules: Simplex.Chat.Archive @@ -241,6 +242,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links + Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 52fd48cfde..f64816b9dc 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1150,45 +1150,62 @@ processChatCommand' vr = \case pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) _ -> throwCmdError "not supported" APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do - (uclId, (ucl, gLinkInfo_)) <- withFastStore $ \db -> do - uclId <- getUserContactLinkIdByCReq db connReqId - uclGLinkInfo <- getUserContactLinkById db userId uclId - pure (uclId, uclGLinkInfo) - let UserContactLink {shortLinkDataSet, addressSettings} = ucl - when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" - withUserContactLock "acceptContact" uclId $ do - cReq@UserContactRequest {welcomeSharedMsgId} <- withFastStore $ \db -> getContactRequest db user connReqId - (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito - let contactUsed = isNothing gLinkInfo_ - ct' <- withStore' $ \db -> do - updateContactAccepted db user ct contactUsed - conn' <- - if sqSecured - then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady - else pure conn - pure ct {contactUsed, activeConn = Just conn'} - when sqSecured $ forM_ (autoReply addressSettings) $ \mc -> case welcomeSharedMsgId of - Just smId -> - void $ sendDirectContactMessage user ct' $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing - Nothing -> do - (msg, _) <- sendDirectContactMessage user ct' $ XMsgNew $ MCSimple $ extMsgContent mc Nothing - ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] - pure $ CRAcceptingContactRequest user ct' + uclData_ <- withFastStore $ \db -> do + uclId_ <- getUserContactLinkIdByCReq db connReqId + forM uclId_ $ \uclId -> do -- address may be deleted + uclGLinkInfo <- getUserContactLinkById db userId uclId + pure (uclId, uclGLinkInfo) + withContactRequestLock "acceptContact" connReqId $ case uclData_ of + Nothing -> do -- address was deleted + when incognito $ throwCmdError "incognito not allowed when address is not found" + cReq <- withFastStore $ \db -> getContactRequest db user connReqId + (ct, _sqSecured) <- acceptCReq user cReq True + pure $ CRAcceptingContactRequest user ct + Just (uclId, (ucl@UserContactLink {shortLinkDataSet}, gLinkInfo_)) -> do + when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" + withUserContactLock "acceptContact" uclId $ do + cReq <- withFastStore $ \db -> getContactRequest db user connReqId + let contactUsed = isNothing gLinkInfo_ -- for redundancy, as group link requests are auto-accepted + (ct, sqSecured) <- acceptCReq user cReq contactUsed + when sqSecured $ sendWelcomeMsg user ct ucl cReq + pure $ CRAcceptingContactRequest user ct + where + acceptCReq user cReq contactUsed = do + (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito + ct' <- withStore' $ \db -> do + updateContactAccepted db user ct contactUsed + conn' <- + if sqSecured + then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady + else pure conn + pure ct {contactUsed, activeConn = Just conn'} + pure (ct', sqSecured) + sendWelcomeMsg user ct ucl UserContactRequest {welcomeSharedMsgId} = + forM_ (autoReply $ addressSettings ucl) $ \mc -> case welcomeSharedMsgId of + Just smId -> + void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing + Nothing -> do + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] APIRejectContact connReqId -> withUser $ \user -> do - userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId - withUserContactLock "rejectContact" userContactLinkId $ do - (cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId}, ct_) <- - withFastStore $ \db -> do - cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId - ct_ <- forM contactId_ $ \contactId -> do - ct <- getContact db vr user contactId - deleteContact db user ct - pure ct - liftIO $ deleteContactRequest db user connReqId - pure (cReq, ct_) - withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected user cReq ct_ + uclId_ <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId + withContactRequestLock "rejectContact" connReqId $ case uclId_ of + Nothing -> rejectCReq user -- address was deleted + Just uclId -> withUserContactLock "rejectContact" uclId $ rejectCReq user + where + rejectCReq user = do + (cReq@UserContactRequest {agentInvitationId = AgentInvId invId}, ct_) <- + withFastStore $ \db -> do + cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId + ct_ <- forM contactId_ $ \contactId -> do + ct <- getContact db vr user contactId + deleteContact db user ct + pure ct + liftIO $ deleteContactRequest db user connReqId + pure (cReq, ct_) + withAgent (`rejectContact` invId) + pure $ CRContactRequestRejected user cReq ct_ APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withFastStore $ \db -> getContact db vr user contactId @@ -2785,6 +2802,7 @@ processChatCommand' vr = \case CLContact ctId -> "Contact " <> tshow ctId CLGroup gId -> "Group " <> tshow gId CLUserContact ucId -> "UserContact " <> tshow ucId + CLContactRequest crId -> "ContactRequest " <> tshow crId CLFile fId -> "File " <> tshow fId DebugEvent event -> toView event >> ok_ GetAgentSubsTotal userId -> withUserId userId $ \user -> do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index cef0f1bb8a..4055e583df 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -142,6 +142,10 @@ withUserContactLock :: Text -> Int64 -> CM a -> CM a withUserContactLock name = withEntityLock name . CLUserContact {-# INLINE withUserContactLock #-} +withContactRequestLock :: Text -> Int64 -> CM a -> CM a +withContactRequestLock name = withEntityLock name . CLContactRequest +{-# INLINE withContactRequestLock #-} + withFileLock :: Text -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} @@ -878,7 +882,7 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of -- It may be reasonable to set it when contact is first prepared, but then we can't use it to ignore requests after acceptance, -- and it may lead to race conditions with XInfo events. acceptContactRequest :: User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured) -acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognito = do +acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId_, xContactId, pqSupport} incognito = do subMode <- chatReadVar subscriptionMode let pqSup = PQSupportOn pqSup' = pqSup `CR.pqSupportAnd` pqSupport @@ -887,20 +891,20 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = (ct, conn, incognitoProfile) <- case contactId_ of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' (ct, conn) <- withStore' $ \db -> - createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False + 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 case contactConn ct of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' currentTs <- liftIO getCurrentTime conn <- withStore' $ \db -> do forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId - createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs + 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 @@ -908,7 +912,7 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = let profileToSend = profileToSendOnAccept user incognitoProfile False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend -- TODO [certs rcv] - (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) + (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aUserId user) (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact acceptContactRequestAsync @@ -925,7 +929,7 @@ acceptContactRequestAsync currentTs <- liftIO getCurrentTime withStore $ \db -> do forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId - Connection {connId} <- liftIO $ createAcceptedContactConn db user uclId contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs + Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId getContact db vr user contactId @@ -2194,7 +2198,7 @@ agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> Cha agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact dm <- encodeConnInfoPQ pqSup chatV msg - connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode + connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode pure (cmdId, connId) deleteAgentConnectionAsync :: ConnId -> CM () diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index a56c89102b..a248665a0c 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -143,12 +143,11 @@ createOrUpdateContactRequest 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.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 = ? diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index fe0b1f64c7..b72b597f94 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -708,7 +708,7 @@ getUserContacts db vr user@User {userId} = do contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64 +getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO (Maybe Int64) getUserContactLinkIdByCReq db contactRequestId = ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $ DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId) @@ -734,12 +734,11 @@ contactRequestQuery = 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.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) |] @@ -774,8 +773,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId, userId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createContactFromRequest :: DB.Connection -> User -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) -createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do +createContactFromRequest :: DB.Connection -> User -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) +createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId_ agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do currentTs <- getCurrentTime let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences DB.execute @@ -784,7 +783,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc (userId, localDisplayName, profileId, BI True, userPreferences, currentTs, currentTs, currentTs, xContactId, BI contactUsed) contactId <- insertedRowId db DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName) - conn <- createAcceptedContactConn db user uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs + conn <- createAcceptedContactConn db user uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn ct = Contact @@ -813,12 +812,12 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc } pure (ct, conn) -createAcceptedContactConn :: DB.Connection -> User -> Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection -createAcceptedContactConn db User {userId} uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do +createAcceptedContactConn :: DB.Connection -> User -> Maybe Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection +createAcceptedContactConn db User {userId} uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do customUserProfileId <- forM incognitoProfile $ \case NewIncognito p -> createIncognitoProfile_ db userId currentTs p ExistingIncognito LocalProfile {profileId = pId} -> pure pId - createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just uclId) customUserProfileId 0 currentTs subMode pqSup + createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing uclId_ customUserProfileId 0 currentTs subMode pqSup updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO () updateContactAccepted db User {userId} Contact {contactId} contactUsed = diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index b84e3fe4ec..8c7c02d709 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1051,12 +1051,11 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of 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.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 ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 7edaf796e7..4b6a1071ff 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -9,6 +9,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission import Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope import Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links +import Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -17,7 +18,8 @@ schemaMigrations = ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope), - ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links) + ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links), + ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs new file mode 100644 index 0000000000..b3868c82d7 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250702_contact_requests_remove_cascade_delete :: Text +m20250702_contact_requests_remove_cascade_delete = + T.pack + [r| +ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey; + +ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id DROP NOT NULL; + +ALTER TABLE contact_requests + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey + FOREIGN KEY (user_contact_link_id) + REFERENCES user_contact_links(user_contact_link_id) + ON UPDATE CASCADE + ON DELETE SET NULL; +|] + +down_m20250702_contact_requests_remove_cascade_delete :: Text +down_m20250702_contact_requests_remove_cascade_delete = + T.pack + [r| +ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey; + +ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id SET NOT NULL; + +ALTER TABLE contact_requests + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey + FOREIGN KEY (user_contact_link_id) + REFERENCES user_contact_links(user_contact_link_id) + ON UPDATE CASCADE + ON DELETE CASCADE; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 377d923256..40e4979c68 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -349,7 +349,7 @@ ALTER TABLE test_chat_schema.contact_profiles ALTER COLUMN contact_profile_id AD CREATE TABLE test_chat_schema.contact_requests ( contact_request_id bigint NOT NULL, - user_contact_link_id bigint NOT NULL, + user_contact_link_id bigint, agent_invitation_id bytea NOT NULL, contact_profile_id bigint, local_display_name text NOT NULL, @@ -2313,7 +2313,7 @@ ALTER TABLE ONLY test_chat_schema.contact_requests ALTER TABLE ONLY test_chat_schema.contact_requests - ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE SET NULL; @@ -2689,3 +2689,6 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links ALTER TABLE ONLY test_chat_schema.xftp_file_descriptions ADD CONSTRAINT xftp_file_descriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; + + + diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 14ee9a2567..02d8e4776e 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -132,6 +132,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission import Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope import Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links +import Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -263,7 +264,8 @@ schemaMigrations = ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope), - ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links) + ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links), + ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs new file mode 100644 index 0000000000..c758a962cd --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250702_contact_requests_remove_cascade_delete :: Query +m20250702_contact_requests_remove_cascade_delete = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace( + replace( + sql, + 'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links', + 'user_contact_link_id INTEGER REFERENCES user_contact_links' + ), + 'ON UPDATE CASCADE ON DELETE CASCADE,', + 'ON UPDATE CASCADE ON DELETE SET NULL,' + ) +WHERE name = 'contact_requests' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20250702_contact_requests_remove_cascade_delete :: Query +down_m20250702_contact_requests_remove_cascade_delete = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace( + replace( + sql, + 'ON UPDATE CASCADE ON DELETE SET NULL,', + 'ON UPDATE CASCADE ON DELETE CASCADE,' + ), + 'user_contact_link_id INTEGER REFERENCES user_contact_links', + 'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links' + ) +WHERE name = 'contact_requests' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index a85ba4a4cb..ee717e71f5 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -274,7 +274,7 @@ Plan: Query: INSERT INTO conn_invitations - (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); Plan: @@ -822,7 +822,7 @@ Query: DELETE FROM commands WHERE command_id = ? Plan: SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) -Query: DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ? +Query: DELETE FROM conn_invitations WHERE invitation_id = ? Plan: SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) 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 7a2d03117b..a01152dd18 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -398,12 +398,11 @@ Query: 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.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 = ? @@ -412,7 +411,6 @@ Query: Plan: 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=?) Query: SELECT COUNT(1) @@ -1631,12 +1629,11 @@ Query: 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.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 ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? @@ -1655,18 +1652,16 @@ Plan: SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -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.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.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 ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? @@ -1715,7 +1708,6 @@ Plan: SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) 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=?) Query: SELECT @@ -4742,35 +4734,31 @@ Query: 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.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.business_group_id = ? Plan: SEARCH cr USING INDEX idx_contact_requests_business_group_id (business_group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -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.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.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.contact_request_id = ? Plan: SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 6cf5a8f630..b41bc560ab 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -344,8 +344,8 @@ CREATE TABLE user_contact_links( ); CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, - user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links - ON UPDATE CASCADE ON DELETE CASCADE, + user_contact_link_id INTEGER REFERENCES user_contact_links + ON UPDATE CASCADE ON DELETE SET NULL, agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 4bcfa10361..7bd1ca87d4 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -64,6 +64,7 @@ data ChatLockEntity | CLContact ContactId | CLGroup GroupId | CLUserContact Int64 + | CLContactRequest Int64 | CLFile Int64 deriving (Eq, Ord) @@ -488,13 +489,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, Maybe GroupId, Int64) :. (AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (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_, businessGroupId_, userContactLinkId) :. (agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (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_, businessGroupId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, 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 eb61988f8a..0848247c37 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -347,8 +347,7 @@ data UserContactRequest = UserContactRequest agentInvitationId :: AgentInvId, contactId_ :: Maybe ContactId, businessGroupId_ :: Maybe GroupId, - userContactLinkId :: Int64, - agentContactConnId :: AgentConnId, -- connection id of user contact + userContactLinkId_ :: Maybe Int64, cReqChatVRange :: VersionRangeChat, localDisplayName :: ContactName, profileId :: Int64, diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a5f29b00a4..47fd16f642 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -48,10 +48,7 @@ chatProfileTests = do it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "reject contact and delete contact link" testRejectContactAndDeleteUserContact - -- 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 "keep connection requests when contact link deleted" testKeepConnectionRequests 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 @@ -673,8 +670,8 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr cath ##> ("/c " <> cLink) cath <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" -testDeleteConnectionRequests :: HasCallStack => TestParams -> IO () -testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ +testKeepConnectionRequests :: HasCallStack => TestParams -> IO () +testKeepConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" cLink <- getContactLink alice True @@ -687,14 +684,52 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ alice <## "Your chat address is deleted - accepted contacts will remain connected." alice <## "To create a new chat address use /ad" + -- can accept and reject requests after address deletion + 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 + + alice ##> "/rc cath" + alice <## "cath: contact request rejected" + + alice @@@ [("@bob", "hey")] + + -- bob's request to new address uses different name alice ##> "/ad" cLink' <- getContactLink alice True + bob ##> ("/c " <> cLink') - -- same names are used here, as they were released at /da - alice <#? bob + bob <## "connection request sent!" + alice <## "bob_1 (Bob) wants to connect to you!" + alice <## "to accept: /ac bob_1" + alice <## "to reject: /rc bob_1 (the sender will NOT be notified)" + + alice ##> "/ac bob_1" + alice <## "bob_1 (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice_1 (Alice): contact is connected") + (alice <## "bob_1 (Bob): contact is connected") + + alice #> "@bob_1 hi" + bob <# "alice_1> hi" + bob #> "@alice_1 hey" + alice <# "bob_1> hey" + cath ##> ("/c " <> cLink') alice <#? cath + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + alice <##> cath + + alice @@@ [("@cath", "hey"), ("@bob_1", "hey"), ("@bob", "hey")] + testContactLinkDeletedConnectedContactWorks :: HasCallStack => TestParams -> IO () testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile $ \alice bob -> do