From d5eb7b78111a2336c668736bfc397ed4aa052ede Mon Sep 17 00:00:00 2001 From: Diogo Date: Wed, 21 Aug 2024 10:27:58 +0100 Subject: [PATCH] core: api to change user of pending connections (#4681) * core: add api that enables change of owner user id for pending connections * old user sends request, incognito handling and coverage * call agent inside set connection api * only set user id if servers match * simplify * reduce test noise * return invitation when a newone is created * add test for profile on different server * refactor namings * update simplexmq * refactor * test improvements and simplify * remove fdescribes * simplify and reduce vars scope * put if back * refactor, change error * refactor view * refactor --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 35 +++++++++ src/Simplex/Chat/Controller.hs | 3 + src/Simplex/Chat/Store/Direct.hs | 14 ++++ src/Simplex/Chat/View.hs | 16 ++++ tests/ChatTests/Profiles.hs | 125 +++++++++++++++++++++++++++++++ 7 files changed, 195 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 05f5935554..08973f9be8 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: 571d148bdf9e29403b9bcec6f7daeff815a4db38 + tag: 1cbf8c0015a8014bcc9d1894055d734947176684 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 2802bfda6e..a77e8ee167 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."571d148bdf9e29403b9bcec6f7daeff815a4db38" = "1p7m7i4wdyvd2wypm04cx4w960vnyqa9wkfnlpv72cz83k0xirxf"; + "https://github.com/simplex-chat/simplexmq.git"."1cbf8c0015a8014bcc9d1894055d734947176684" = "01x8fvwzahz32fmxc2mjvxl1z6agwr3952hl4qjswym76r1dpc1s"; "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/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2d3ed3ca84..5899be6445 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1655,6 +1655,40 @@ processChatCommand' vr = \case case conn'_ of Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do + conn <- withFastStore $ \db -> getPendingContactConnection db userId connId + let PendingContactConnection {pccConnStatus, connReqInv} = conn + case (pccConnStatus, connReqInv) of + (ConnNew, Just cReqInv) -> do + newUser <- privateGetUser newUserId + conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) + pure $ CRConnectionUserChanged user conn conn' newUser + _ -> throwChatError CEConnectionUserChangeProhibited + where + canKeepLink :: ConnReqInvitation -> User -> CM Bool + canKeepLink (CRInvitationUri crData _) newUser = do + let ConnReqUriData {crSmpQueues = q :| _} = crData + SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q + cfg <- asks config + newUserServers <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + pure $ smpServer `elem` newUserServers + updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do + withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) + withFastStore' $ \db -> do + conn' <- updatePCCUser db userId conn newUserId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + pure conn' + recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do + subMode <- chatReadVar subscriptionMode + (agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode + conn' <- withFastStore' $ \db -> do + deleteConnectionRecord db user connId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + createDirectConnection db newUser agConnId cReq ConnNew Nothing subMode initialChatVersion PQSupportOn + deleteAgentConnectionAsync user (aConnId' conn) + 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 @@ -7790,6 +7824,7 @@ chatCommandP = "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), + "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), ("/connect" <|> "/c") *> (AddContact <$> incognitoP), ForwardMessage <$> chatNameP <* " <- @" <*> displayName <* A.space <*> msgTextP, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index dbfa361c70..c4f056c778 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -403,6 +403,7 @@ data ChatCommand | APIAddContact UserId IncognitoEnabled | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled + | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIConnectPlan UserId AConnectionRequestUri | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) | Connect IncognitoEnabled (Maybe AConnectionRequestUri) @@ -628,6 +629,7 @@ data ChatResponse | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} + | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} @@ -1191,6 +1193,7 @@ data ChatErrorType | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} | CEConnectionIncognitoChangeProhibited + | CEConnectionUserChangeProhibited | CEPeerChatVRangeIncompatible | CEInternalError {message :: String} | CEException {message :: String} diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 508a992543..c0f007b6ac 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -64,6 +64,7 @@ module Simplex.Chat.Store.Direct createAcceptedContact, getUserByContactRequestId, getPendingContactConnections, + updatePCCUser, getContactConnections, getConnectionById, getConnectionsContacts, @@ -424,6 +425,19 @@ updatePCCIncognito db User {userId} conn customUserProfileId = do (customUserProfileId, updatedAt, userId, pccConnId conn) pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} +updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> IO PendingContactConnection +updatePCCUser db userId conn newUserId = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (newUserId, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, updatedAt} + deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () deletePCCIncognitoProfile db User {userId} profileId = DB.execute diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 3d519c3b5c..e154b5b902 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -172,6 +172,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRVersionInfo info _ _ -> viewVersionInfo logLevel info CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c + CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView @@ -1498,6 +1499,20 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr | isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"] | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] +viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] +viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connReqInv} User {localDisplayName = n'} PendingContactConnection {connReqInv = connReqInv'} = + case (connReqInv, connReqInv') of + (Just cReqInv, Just cReqInv') + | cReqInv /= cReqInv' -> [userChangedStr <> ", new link:"] <> newLink cReqInv' + _ -> [userChangedStr] + where + userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' + newLink cReqInv = + [ "", + (plain . strEncode) (simplexChatInvitation cReqInv), + "" + ] + viewConnectionPlan :: ConnectionPlan -> [StyledString] viewConnectionPlan = \case CPInvitationLink ilp -> case ilp of @@ -2025,6 +2040,7 @@ viewChatError isCmd logLevel testView = \case CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"] + CEConnectionUserChangeProhibited -> ["incognito mode change prohibited for user"] CEPeerChatVRangeIncompatible -> ["peer chat protocol version range incompatible"] CEInternalError e -> ["internal chat error: " <> plain e] CEException e -> ["exception: " <> plain e] diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 23004677c9..43ad5ba841 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -63,6 +63,11 @@ chatProfileTests = do describe "contact aliases" $ do it "set contact alias" testSetAlias it "set connection alias" testSetConnectionAlias + describe "pending connection users" $ do + it "change user for pending connection" testChangePCCUser + it "change from incognito profile connects as new user" testChangePCCUserFromIncognito + it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito + it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv describe "preferences" $ do it "set contact preferences" testSetContactPrefs it "feature offers" testFeatureOffers @@ -1557,6 +1562,126 @@ testSetAlias = testChat2 aliceProfile bobProfile $ alice ##> "/contacts" alice <## "bob (Bob)" +testChangePCCUser :: HasCallStack => FilePath -> IO () +testChangePCCUser = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite + alice ##> "/connect" + inv <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/create user alisa2" + showActiveUser alice "alisa2" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to other user + alice ##> "/_set conn user :1 3" + alice <## "connection 1 changed from user alisa to user alisa2" + alice ##> "/user alisa2" + showActiveUser alice "alisa2" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa2: contact is connected") + +testChangePCCUserFromIncognito :: HasCallStack => FilePath -> IO () +testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to initial user + alice ##> "/_set conn user :1 1" + alice <## "connection 1 changed from user alisa to user alice" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + +testChangePCCUserAndThenIncognito :: HasCallStack => FilePath -> IO () +testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + inv <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection to incognito and make sure it's attached to the newly created user profile + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + alisaIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (alisaIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + +testChangePCCUserDiffSrv :: HasCallStack => FilePath -> IO () +testChangePCCUserDiffSrv = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite + alice ##> "/connect" + _ <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user with different servers + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user and use the newly created connection + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa: contact is connected") + testSetConnectionAlias :: HasCallStack => FilePath -> IO () testSetConnectionAlias = testChat2 aliceProfile bobProfile $ \alice bob -> do