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 0849f43377.

* 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 <evgeny@poberezkin.com>
Co-authored-by: Diogo <diogofncunha@gmail.com>
This commit is contained in:
spaced4ndy
2024-10-11 18:37:38 +04:00
committed by GitHub
parent 2127c7dcce
commit 9a87f344b5
21 changed files with 401 additions and 91 deletions
+1 -1
View File
@@ -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)
+20 -20
View File
@@ -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 = "<group>"; };
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
6405489E2CB56735005DE1E4 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
6405489F2CB56735005DE1E4 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
640548A02CB56735005DE1E4 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
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 = "<group>"; };
640548A22CB56735005DE1E4 /* libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-EtclnBf7vnkLnhnDA1lixv.a"; sourceTree = "<group>"; };
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
@@ -501,6 +496,11 @@
6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = "<group>"; };
64466DC729FC2B3B00E3D48D /* CreateSimpleXAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSimpleXAddress.swift; sourceTree = "<group>"; };
64466DCB29FFE3E800E3D48D /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
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 = "<group>"; };
644844632CB932A5004A1BC6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
644844642CB932A5004A1BC6 /* libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.7-4dlRUyozvfTCAHYlr9Ja1T.a"; sourceTree = "<group>"; };
644844652CB932A5004A1BC6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
644844662CB932A5004A1BC6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6448BBB528FA9D56000D2AB9 /* GroupLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupLinkView.swift; sourceTree = "<group>"; };
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeVoiceView.swift; sourceTree = "<group>"; };
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIVoiceView.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
+1 -1
View File
@@ -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)
+2
View File
@@ -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
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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";
+1
View File
@@ -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
+68 -29
View File
@@ -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
+4
View File
@@ -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 #-}
@@ -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;
|]
@@ -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);
+11
View File
@@ -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_)
+53 -28
View File
@@ -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)
+3 -2
View File
@@ -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
+3 -1
View File
@@ -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
+3 -3
View File
@@ -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 =
+5
View File
@@ -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"
+2 -2
View File
@@ -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
+131 -2
View File
@@ -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
+65
View File
@@ -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 $