From 9a87f344b56430edb3c565c101ca214e48ee2180 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 11 Oct 2024 18:37:38 +0400 Subject: [PATCH] core: do not regenerate key when accepting connection to avoid invalidating invitation link on bad networks (#5018) * core: prepare conn (plan) * update * group join * comment * comment * wip * Revert "wip" This reverts commit 0849f433772e2681a6dc2f0051b83d2b5d0a8f31. * accept * save contact_id, reuse contact * refactor * simplexmq * set contactUsed * support retrying join * exclude prepared connections from API responses * avoid race with events * avoid race better * fix UI * update library * tmp * update * display error details on ios cmd prohibited * underscore instead of empty * Update apps/ios/Shared/Model/SimpleXAPI.swift Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> * test * update simplexmq --------- Co-authored-by: Evgeny Poberezkin Co-authored-by: Diogo --- apps/ios/Shared/Model/SimpleXAPI.swift | 2 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++--- apps/ios/SimpleXChat/APITypes.swift | 2 +- apps/ios/SimpleXChat/ChatTypes.swift | 2 + .../chat/simplex/common/model/ChatModel.kt | 2 + cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 97 +++++++++---- src/Simplex/Chat/Controller.hs | 4 + .../M20241010_contact_requests_contact_id.hs | 22 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 2 + src/Simplex/Chat/Protocol.hs | 11 ++ src/Simplex/Chat/Store/Direct.hs | 81 +++++++---- src/Simplex/Chat/Store/Messages.hs | 5 +- src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Shared.hs | 6 +- src/Simplex/Chat/Types.hs | 5 + tests/ChatClient.hs | 4 +- tests/ChatTests/Direct.hs | 133 +++++++++++++++++- tests/ChatTests/Profiles.hs | 65 +++++++++ 21 files changed, 401 insertions(+), 91 deletions(-) create mode 100644 src/Simplex/Chat/Migrations/M20241010_contact_requests_contact_id.hs diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 17f5936b6b..a0a32156c4 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -462,7 +462,7 @@ func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode, String let r = chatSendCmdSync(.apiGetNtfToken) switch r { case let .ntfToken(token, status, ntfMode, ntfServer): return (token, status, ntfMode, ntfServer) - case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED))): return (nil, nil, .off, nil) + case .chatCmdError(_, .errorAgent(.CMD(.PROHIBITED, _))): return (nil, nil, .off, nil) default: logger.debug("apiGetNtfToken response: \(String(describing: r))") return (nil, nil, .off, nil) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 3e2e908d32..7b9bbb726d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -144,11 +144,6 @@ 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; }; 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; }; - 640548A32CB56735005DE1E4 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6405489E2CB56735005DE1E4 /* libgmpxx.a */; }; - 640548A42CB56735005DE1E4 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6405489F2CB56735005DE1E4 /* libgmp.a */; }; - 640548A52CB56735005DE1E4 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A02CB56735005DE1E4 /* libffi.a */; }; - 640548A62CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */; }; - 640548A72CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; @@ -159,6 +154,11 @@ 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */; }; 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64466DCB29FFE3E800E3D48D /* MailView.swift */; }; + 644844672CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644844622CB932A4004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a */; }; + 644844682CB932A5004A1BC6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644844632CB932A5004A1BC6 /* libffi.a */; }; + 644844692CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644844642CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a */; }; + 6448446A2CB932A5004A1BC6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644844652CB932A5004A1BC6 /* libgmpxx.a */; }; + 6448446B2CB932A5004A1BC6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644844662CB932A5004A1BC6 /* libgmp.a */; }; 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */; }; 644EFFDE292BCD9D00525D5B /* ComposeVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */; }; 644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */; }; @@ -486,11 +486,6 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = ""; }; 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = ""; }; - 6405489E2CB56735005DE1E4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 6405489F2CB56735005DE1E4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 640548A02CB56735005DE1E4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a"; sourceTree = ""; }; - 640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a"; sourceTree = ""; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; }; 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; @@ -501,6 +496,11 @@ 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; 64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = ""; }; 64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = ""; }; + 644844622CB932A4004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a"; sourceTree = ""; }; + 644844632CB932A5004A1BC6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 644844642CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a"; sourceTree = ""; }; + 644844652CB932A5004A1BC6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 644844662CB932A5004A1BC6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = ""; }; 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = ""; }; 644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = ""; }; @@ -655,14 +655,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 640548A32CB56735005DE1E4 /* libgmpxx.a in Frameworks */, - 640548A72CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a in Frameworks */, - 640548A52CB56735005DE1E4 /* libffi.a in Frameworks */, - 640548A42CB56735005DE1E4 /* libgmp.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 640548A62CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a in Frameworks */, + 644844692CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a in Frameworks */, + 6448446B2CB932A5004A1BC6 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, + 644844682CB932A5004A1BC6 /* libffi.a in Frameworks */, + 644844672CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a in Frameworks */, + 6448446A2CB932A5004A1BC6 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -739,11 +739,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 640548A02CB56735005DE1E4 /* libffi.a */, - 6405489F2CB56735005DE1E4 /* libgmp.a */, - 6405489E2CB56735005DE1E4 /* libgmpxx.a */, - 640548A12CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv-ghc9.6.3.a */, - 640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */, + 644844632CB932A5004A1BC6 /* libffi.a */, + 644844662CB932A5004A1BC6 /* libgmp.a */, + 644844652CB932A5004A1BC6 /* libgmpxx.a */, + 644844622CB932A4004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T-ghc9.6.3.a */, + 644844642CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index bff150f58f..fae6d2293f 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -2041,7 +2041,7 @@ public enum SQLiteError: Decodable, Hashable { } public enum AgentErrorType: Decodable, Hashable { - case CMD(cmdErr: CommandErrorType) + case CMD(cmdErr: CommandErrorType, errContext: String) case CONN(connErr: ConnectionErrorType) case SMP(serverAddress: String, smpErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7b81057e0b..45dab17cf2 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1852,6 +1852,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable { public enum ConnStatus: String, Decodable, Hashable { case new = "new" + case prepared = "prepared" case joined = "joined" case requested = "requested" case accepted = "accepted" @@ -1863,6 +1864,7 @@ public enum ConnStatus: String, Decodable, Hashable { get { switch self { case .new: return true + case .prepared: return false case .joined: return false case .requested: return true case .accepted: return true diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 682a472060..6bc565097f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1889,6 +1889,7 @@ class PendingContactConnection( @Serializable enum class ConnStatus { @SerialName("new") New, + @SerialName("prepared") Prepared, @SerialName("joined") Joined, @SerialName("requested") Requested, @SerialName("accepted") Accepted, @@ -1898,6 +1899,7 @@ enum class ConnStatus { val initiated: Boolean? get() = when (this) { New -> true + Prepared -> false Joined -> false Requested -> true Accepted -> true diff --git a/cabal.project b/cabal.project index da46056668..515ad0312b 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: b8971a31bcb82fffabcb792c9afd6bc4a96ec649 + tag: 887044283079b91f5d1ce84372a7e1a5c31c379b source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 314cb070f1..658f2fb648 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."b8971a31bcb82fffabcb792c9afd6bc4a96ec649" = "1p6m390ngcsp7i7vy0m0zxh167gkbciavva9a00l6pxwzaz9qmpi"; + "https://github.com/simplex-chat/simplexmq.git"."887044283079b91f5d1ce84372a7e1a5c31c379b" = "1lqlcyph9pn8ibwydi8m8bfcrswgs0dcsd867ldwr4xqnlkmd7qd"; "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 c637789ad2..5c3a313869 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -149,6 +149,7 @@ library Simplex.Chat.Migrations.M20240827_calls_uuid Simplex.Chat.Migrations.M20240920_user_order Simplex.Chat.Migrations.M20241008_indexes + Simplex.Chat.Migrations.M20241010_contact_requests_contact_id Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 55c70d6a2e..5f586e8dad 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1299,12 +1299,18 @@ processChatCommand' vr = \case APIAcceptContact incognito connReqId -> withUser $ \_ -> do (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withFastStore $ \db -> getContactRequest' db connReqId withUserContactLock "acceptContact" userContactLinkId $ do + (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequest user cReq incognitoProfile contactUsed - pure $ CRAcceptingContactRequest user ct + ct' <- withStore' $ \db -> do + deleteContactRequestRec db user cReq + 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 $ CRAcceptingContactRequest user ct' APIRejectContact connReqId -> withUser $ \user -> do cReq@UserContactRequest {userContactLinkId, agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- withFastStore $ \db -> @@ -1763,7 +1769,7 @@ processChatCommand' vr = \case pure conn' APIConnectPlan userId cReqUri -> withUserId userId $ \user -> CRConnectionPlan user <$> connectPlan user cReqUri - APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do + APIConnect userId incognito (Just (ACR SCMInvitation cReq@(CRInvitationUri crData e2e))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing @@ -1774,10 +1780,27 @@ processChatCommand' vr = \case Just (agentV, pqSup') -> do let chatV = agentToChatVersion agentV dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' - conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup' - joinPreparedAgentConnection user pccConnId connId cReq dm pqSup' subMode - pure $ CRSentConfirmation user conn + withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case + Nothing -> joinNewConn chatV dm + Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing) + | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link + | connStatus == ConnPrepared -> do -- retrying join after error + pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId + joinPreparedConn (aConnId conn) pcc dm + Just ent -> throwChatError $ CECommandError $ "connection exists: " <> show (connEntityInfo ent) + where + joinNewConn chatV dm = do + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' + pcc <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' + joinPreparedConn connId pcc dm + joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do + void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode + withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared ConnJoined + pure $ CRSentConfirmation user pcc {pccConnStatus = ConnJoined} + cReqs = + ( CRInvitationUri crData {crScheme = SSSimplex} e2e, + CRInvitationUri crData {crScheme = simplexChat} e2e + ) APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do @@ -2029,20 +2052,21 @@ processChatCommand' vr = \case Just Connection {peerChatVRange} -> do subMode <- chatReadVar subscriptionMode dm <- encodeConnInfo $ XGrpAcpt membershipMemId - agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff - let chatV = vr `peerConnChatVersion` peerChatVRange - cId <- withFastStore' $ \db -> do - Connection {connId = cId} <- createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode + agentConnId <- case memberConn fromMember of + Nothing -> do + agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff + let chatV = vr `peerConnChatVersion` peerChatVRange + void $ withFastStore' $ \db -> createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode + pure agentConnId + Just conn -> pure $ aConnId conn + withFastStore' $ \db -> do updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - pure cId - void (withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode) + void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId True connRequest dm PQSupportOff subMode) `catchChatError` \e -> do withFastStore' $ \db -> do - deleteConnectionRecord db user cId updateGroupMemberStatus db userId fromMember GSMemInvited updateGroupMemberStatus db userId membership GSMemInvited - withAgent $ \a -> deleteConnectionAsync a False agentConnId throwError e updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user)) pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing @@ -2615,7 +2639,7 @@ processChatCommand' vr = \case joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM () joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do - void (withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq connInfo pqSup subMode) + void (withAgent $ \a -> joinConnection a (aUserId user) connId True cReq connInfo pqSup subMode) `catchChatError` \e -> do withFastStore' $ \db -> deleteConnectionRecord db user pccConnId withAgent $ \a -> deleteConnectionAsync a False connId @@ -2857,6 +2881,8 @@ processChatCommand' vr = \case connectPlan user (ACR SCMInvitation (CRInvitationUri crData e2e)) = do withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case Nothing -> pure $ CPInvitationLink ILPOk + Just (RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing) -> + pure $ CPInvitationLink ILPOk Just (RcvDirectMsgConnection conn ct_) -> do let Connection {connStatus, contactConnInitiated} = conn if @@ -3664,29 +3690,41 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of liftIO $ B.hPut h "" >> hFlush h | otherwise = liftIO $ B.writeFile fPath "" -acceptContactRequest :: User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> CM Contact -acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId, pqSupport} incognitoProfile contactUsed = do +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 subMode <- chatReadVar subscriptionMode let pqSup = PQSupportOn - vr <- chatVersionRange - let profileToSend = profileToSendOnAccept user incognitoProfile False - chatV = vr `peerConnChatVersion` cReqChatVRange pqSup' = pqSup `CR.pqSupportAnd` pqSupport + vr <- chatVersionRange + let chatV = vr `peerConnChatVersion` cReqChatVRange + (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' + (ct, conn) <- withStore' $ \db -> createAcceptedContact db user connId chatV cReqChatVRange cName profileId cp userContactLinkId 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 -> throwChatError $ CECommandError "contact has no connection" + Just conn@Connection {customUserProfileId} -> do + incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId + pure (ct, conn, ExistingIncognito <$> incognitoProfile) + let profileToSend = profileToSendOnAccept user incognitoProfile False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - (acId, sqSecured) <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode - let connStatus = if sqSecured then ConnSndReady else ConnNew - withStore' $ \db -> createAcceptedContact db user acId connStatus chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed + (ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> PQSupport -> CM Contact -acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed pqSup = do +acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed pqSup = do subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile False vr <- chatVersionRange let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV withStore' $ \db -> do - ct@Contact {activeConn} <- createAcceptedContact db user acId ConnNew chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed - forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId + (ct, Connection {connId}) <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed + deleteContactRequestRec db user cReq + setCommandConnId db user cmdId connId pure ct acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember @@ -4551,6 +4589,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = lift $ setContactNetworkStatus ct' NSConnected toView $ CRContactConnected user ct' (fmap fromLocalProfile incognitoProfile) when (directOrUsed ct') $ do + unless (contactUsed ct') $ withFastStore' $ \db -> updateContactUsed db user ct' createInternalChatItem user (CDDirectRcv ct') (CIRcvDirectE2EEInfo $ E2EInfo pqEnc) Nothing createFeatureEnabledItems ct' when (contactConnInitiated conn') $ do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index e13566aa7b..700dec9d2e 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1416,6 +1416,10 @@ catchStoreError :: ExceptT StoreError IO a -> (StoreError -> ExceptT StoreError catchStoreError = catchAllErrors mkStoreError {-# INLINE catchStoreError #-} +tryStoreError' :: ExceptT StoreError IO a -> IO (Either StoreError a) +tryStoreError' = tryAllErrors' mkStoreError +{-# INLINE tryStoreError' #-} + mkStoreError :: SomeException -> StoreError mkStoreError = SEInternalError . show {-# INLINE mkStoreError #-} diff --git a/src/Simplex/Chat/Migrations/M20241010_contact_requests_contact_id.hs b/src/Simplex/Chat/Migrations/M20241010_contact_requests_contact_id.hs new file mode 100644 index 0000000000..24e7f3a98e --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241010_contact_requests_contact_id.hs @@ -0,0 +1,22 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241010_contact_requests_contact_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241010_contact_requests_contact_id :: Query +m20241010_contact_requests_contact_id = + [sql| +ALTER TABLE contact_requests ADD COLUMN contact_id INTEGER REFERENCES contacts ON DELETE CASCADE; + +CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +|] + +down_m20241010_contact_requests_contact_id :: Query +down_m20241010_contact_requests_contact_id = + [sql| +DROP INDEX idx_contact_requests_contact_id; + +ALTER TABLE contact_requests DROP COLUMN contact_id; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index ad13fb5db9..2619a5c4e5 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -327,6 +327,7 @@ CREATE TABLE contact_requests( peer_chat_min_version INTEGER NOT NULL DEFAULT 1, peer_chat_max_version INTEGER NOT NULL DEFAULT 1, pq_support INTEGER NOT NULL DEFAULT 0, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON UPDATE CASCADE @@ -888,3 +889,4 @@ CREATE INDEX idx_chat_items_fwd_from_chat_item_id ON chat_items( CREATE INDEX idx_received_probes_group_member_id on received_probes( group_member_id ); +CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index f827568fa7..ea39293b9f 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -125,6 +125,17 @@ data ConnectionEntity $(JQ.deriveJSON (sumTypeJSON fstToLower) ''ConnectionEntity) +connEntityInfo :: ConnectionEntity -> String +connEntityInfo = \case + RcvDirectMsgConnection c ct_ -> ctInfo ct_ <> ", status: " <> show (connStatus c) + RcvGroupMsgConnection c g m -> mInfo g m <> ", status: " <> show (connStatus c) + SndFileConnection c _ft -> "snd file, status: " <> show (connStatus c) + RcvFileConnection c _ft -> "rcv file, status: " <> show (connStatus c) + UserContactConnection c _uc -> "user address, status: " <> show (connStatus c) + where + ctInfo = maybe "connection" $ \Contact {contactId} -> "contact " <> show contactId + mInfo GroupInfo {groupId} GroupMember {groupMemberId} = "group " <> show groupId <> ", member " <> show groupMemberId + updateEntityConnStatus :: ConnectionEntity -> ConnStatus -> ConnectionEntity updateEntityConnStatus connEntity connStatus = case connEntity of RcvDirectMsgConnection c ct_ -> RcvDirectMsgConnection (st c) ((\ct -> (ct :: Contact) {activeConn = Just $ st c}) <$> ct_) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index c0f007b6ac..4d33fa113d 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -62,6 +62,8 @@ module Simplex.Chat.Store.Direct getContactRequestIdByName, deleteContactRequest, createAcceptedContact, + deleteContactRequestRec, + updateContactAccepted, getUserByContactRequestId, getPendingContactConnections, updatePCCUser, @@ -69,6 +71,7 @@ module Simplex.Chat.Store.Direct getConnectionById, getConnectionsContacts, updateConnectionStatus, + updateConnectionStatusFromTo, updateContactSettings, setConnConnReqInv, resetContactConnInitiated, @@ -655,7 +658,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V db [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, + 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 @@ -724,7 +727,7 @@ getContactRequest db User {userId} contactRequestId = db [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, + 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 @@ -766,9 +769,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) -createAcceptedContact :: DB.Connection -> User -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connStatus connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do - DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) +createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case NewIncognito p -> createIncognitoProfile_ db userId createdAt p @@ -779,29 +781,42 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} "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 (?,?,?,?,?,?,?,?,?,?)" (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed) contactId <- insertedRowId db - conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connStatus connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup + DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName) + conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn - pure $ - Contact - { contactId, - localDisplayName, - profile = toLocalProfile profileId profile "", - activeConn = Just conn, - viaGroup = Nothing, - contactUsed, - contactStatus = CSActive, - chatSettings = defaultChatSettings, - userPreferences, - mergedPreferences, - createdAt, - updatedAt = createdAt, - chatTs = Just createdAt, - contactGroupMemberId = Nothing, - contactGrpInvSent = False, - uiThemes = Nothing, - chatDeleted = False, - customData = Nothing - } + ct = + Contact + { contactId, + localDisplayName, + profile = toLocalProfile profileId profile "", + activeConn = Just conn, + viaGroup = Nothing, + contactUsed, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt, + updatedAt = createdAt, + chatTs = Just createdAt, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + uiThemes = Nothing, + chatDeleted = False, + customData = Nothing + } + 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 + db + "UPDATE contacts SET contact_used = ? WHERE user_id = ? AND contact_id = ?" + (contactUsed, userId, contactId) getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName db User {userId} cName = @@ -927,7 +942,17 @@ getConnectionsContacts db agentConnIds = do toContactRef (contactId, connId, acId, localDisplayName) = ContactRef {contactId, connId, agentConnId = AgentConnId acId, localDisplayName} updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO () -updateConnectionStatus db Connection {connId} connStatus = do +updateConnectionStatus db Connection {connId} = updateConnectionStatus_ db connId +{-# INLINE updateConnectionStatus #-} + +updateConnectionStatusFromTo :: DB.Connection -> Int64 -> ConnStatus -> ConnStatus -> IO () +updateConnectionStatusFromTo db connId fromStatus toStatus = do + maybeFirstRow fromOnly (DB.query db "SELECT conn_status FROM connections WHERE connection_id = ?" (Only connId)) >>= \case + Just status | status == fromStatus -> updateConnectionStatus_ db connId toStatus + _ -> pure () + +updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO () +updateConnectionStatus_ db connId connStatus = do currentTs <- getCurrentTime if connStatus == ConnReady then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 562a865276..ad77e6c3f1 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -882,7 +882,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of db ( [sql| SELECT - cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, + 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 as ts, cr.peer_chat_min_version, cr.peer_chat_max_version @@ -930,6 +930,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of FROM connections WHERE user_id = :user_id AND conn_type = :conn_contact + AND conn_status != :conn_status AND contact_id IS NULL AND conn_level = 0 AND via_contact IS NULL @@ -938,7 +939,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of |] <> pagQuery ) - ([":user_id" := userId, ":conn_contact" := ConnContact, ":search" := search] <> pagParams) + ([":user_id" := userId, ":conn_contact" := ConnContact, ":conn_status" := ConnPrepared, ":search" := search] <> pagParams) toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData toPreview connRow = let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index a546b1851a..e2d12e78d7 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -113,6 +113,7 @@ import Simplex.Chat.Migrations.M20240528_quota_err_counter import Simplex.Chat.Migrations.M20240827_calls_uuid import Simplex.Chat.Migrations.M20240920_user_order import Simplex.Chat.Migrations.M20241008_indexes +import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -225,7 +226,8 @@ schemaMigrations = ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter), ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid), ("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order), - ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes) + ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), + ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index ba41cc47be..f9a8685ec8 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -411,13 +411,13 @@ getProfileById db userId profileId = toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, 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, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt} + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, createdAt, updatedAt} userQuery :: Query userQuery = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 71fa1d98b9..36bf9edb52 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -297,6 +297,7 @@ userContactGroupId UserContact {groupId} = groupId data UserContactRequest = UserContactRequest { contactRequestId :: Int64, agentInvitationId :: AgentInvId, + contactId_ :: Maybe ContactId, userContactLinkId :: Int64, agentContactConnId :: AgentConnId, -- connection id of user contact cReqChatVRange :: VersionRangeChat, @@ -1371,6 +1372,8 @@ aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId data ConnStatus = -- | connection is created by initiating party with agent NEW command (createConnection) ConnNew + | -- | connection is prepared, to avoid changing keys on invitation links when retrying. + ConnPrepared | -- | connection is joined by joining party with agent JOIN command (joinConnection) ConnJoined | -- | initiating party received CONF notification (to be renamed to REQ) @@ -1399,6 +1402,7 @@ instance ToJSON ConnStatus where instance TextEncoding ConnStatus where textDecode = \case "new" -> Just ConnNew + "prepared" -> Just ConnPrepared "joined" -> Just ConnJoined "requested" -> Just ConnRequested "accepted" -> Just ConnAccepted @@ -1408,6 +1412,7 @@ instance TextEncoding ConnStatus where _ -> Nothing textEncode = \case ConnNew -> "new" + ConnPrepared -> "prepared" ConnJoined -> "joined" ConnRequested -> "requested" ConnAccepted -> "accepted" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 2ce04dbaca..36bdf92dbf 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -463,7 +463,7 @@ smpServerCfg = withSmpServer :: IO () -> IO () withSmpServer = withSmpServer' smpServerCfg -withSmpServer' :: ServerConfig -> IO () -> IO () +withSmpServer' :: ServerConfig -> IO a -> IO a withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing) xftpTestPort :: ServiceName @@ -515,7 +515,7 @@ withXFTPServer' cfg = runXFTPServerBlocking started cfg Nothing ) -serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO () +serverBracket :: (TMVar Bool -> IO ()) -> IO a -> IO a serverBracket server f = do started <- newEmptyTMVarIO bracket diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index c47cf975a1..8971e8d22d 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1,9 +1,12 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} module ChatTests.Direct where @@ -22,14 +25,18 @@ import Database.SQLite.Simple (Only (..)) import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), DefaultAgentServers (..)) import Simplex.Chat.Messages (ChatItemId) -import Simplex.Chat.Options (ChatOpts (..)) +import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificationCode, verificationCode, pattern VersionChat) +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport import Simplex.Messaging.Util (safeDecodeUtf8) import Simplex.Messaging.Version import System.Directory (copyFile, doesDirectoryExist, doesFileExist) @@ -40,6 +47,8 @@ chatDirectTests :: SpecWith FilePath chatDirectTests = do describe "direct messages" $ do describe "add contact and send/receive messages" testAddContact + it "retry connecting via the same link" testRetryConnecting + xit'' "retry connecting via the same link with client timeout" testRetryConnectingClientTimeout it "mark multiple messages as read" testMarkReadDirect it "clear chat with contact" testContactClear it "deleting contact deletes profile" testDeleteContactDeletesProfile @@ -215,6 +224,126 @@ testAddContact = versionTestMatrix2 runTestAddContact then chatFeatures else (0, e2eeInfoNoPQStr) : tail chatFeatures +testRetryConnecting :: HasCallStack => FilePath -> IO () +testRetryConnecting tmp = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test tmp + where + test alice bob = do + inv <- withSmpServer' serverCfg' $ do + alice ##> "/_connect 1" + getInvitation alice + alice <## "server disconnected localhost ()" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice #> "@bob message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + bob <## "server disconnected localhost (@alice)" + alice <## "server disconnected localhost (@bob)" + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + storeLogFile = Just $ tmp <> "/smp-server-store.log", + storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + +testRetryConnectingClientTimeout :: HasCallStack => FilePath -> IO () +testRetryConnectingClientTimeout tmp = do + inv <- withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts tmp cfg' opts' "alice" aliceProfile $ \alice -> do + alice ##> "/_connect 1" + inv <- getInvitation alice + + withNewTestChatCfgOpts tmp cfgZeroTimeout opts' "bob" bobProfile $ \bob -> do + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "smp agent error: BROKER {brokerAddress = \"smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7003\", brokerErr = TIMEOUT}" + + pure inv + + logFile <- readFile $ tmp <> "/smp-server-store.log" + logFile `shouldContain` "SECURE" + + withSmpServer' serverCfg' $ do + withTestChatCfgOpts tmp cfg' opts' "alice" $ \alice -> do + withTestChatCfgOpts tmp cfg' opts' "bob" $ \bob -> do + bob ##> ("/_connect plan 1 " <> inv) + bob <## "invitation link: ok to connect" + bob ##> ("/_connect 1 " <> inv) + bob <## "confirmation sent!" + + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice #> "@bob message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + storeLogFile = Just $ tmp <> "/smp-server-store.log", + storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + cfgZeroTimeout = + (testCfg :: ChatConfig) + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + }, + defaultServers = + let def@DefaultAgentServers {netCfg} = defaultServers testCfg + in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + testMarkReadDirect :: HasCallStack => FilePath -> IO () testMarkReadDirect = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 003fba7cfe..06ed9aa5bc 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -14,10 +14,14 @@ import Control.Monad.Except import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T +import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Options import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Types (ConnStatus (..), Profile (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport @@ -33,6 +37,7 @@ chatProfileTests = do it "use multiword profile names" testMultiWordProfileNames describe "user contact link" $ do it "create and connect via contact link" testUserContactLink + it "retry accepting connection via contact link" testRetryAcceptingViaContactLink it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests @@ -253,6 +258,66 @@ testUserContactLink = alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] alice <##> cath +testRetryAcceptingViaContactLink :: HasCallStack => FilePath -> IO () +testRetryAcceptingViaContactLink tmp = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile test tmp + where + test alice bob = do + cLink <- withSmpServer' serverCfg' $ do + alice ##> "/ad" + getContactLink alice True + alice <## "server disconnected localhost ()" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" + bob ##> ("/_connect 1 " <> cLink) + bob <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: ok to connect" + bob ##> ("/_connect 1 " <> cLink) + alice <#? bob + alice <## "server disconnected localhost ()" + bob <## "server disconnected localhost ()" + alice ##> "/ac bob" + alice <##. "smp agent error: BROKER" + withSmpServer' serverCfg' $ do + alice <## "server connected localhost ()" + bob <## "server connected localhost ()" + 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 message 1" + bob <# "alice> message 1" + bob #> "@alice message 2" + alice <# "bob> message 2" + alice <## "server disconnected localhost (@bob)" + bob <## "server disconnected localhost (@alice)" + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS, False)], + msgQueueQuota = 2, + storeLogFile = Just $ tmp <> "/smp-server-store.log", + storeMsgsFile = Just $ tmp <> "/smp-server-messages.log" + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } + testProfileLink :: HasCallStack => FilePath -> IO () testProfileLink = testChat3 aliceProfile bobProfile cathProfile $