From 1d0247b7ddd6c53f3fd51f3d94cec3cb9656ec30 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sun, 18 Oct 2026 08:51:33 +0100 Subject: [PATCH] core: preserve profile peer type when connecting incognito (#7751) * core: preserve profile peer type when connecting incognito * Apply batched suggestions from code review Co-authored-by: Evgeny * fix --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- src/Simplex/Chat/Library/Commands.hs | 16 ++++----- src/Simplex/Chat/Library/Internal.hs | 7 ++-- src/Simplex/Chat/Library/Subscriber.hs | 3 +- src/Simplex/Chat/ProfileGenerator.hs | 9 ++--- .../SQLite/Migrations/chat_query_plans.txt | 4 +-- src/Simplex/Chat/Store/Shared.hs | 8 ++--- tests/ChatTests/Profiles.hs | 34 ++++++++++++++++++- 7 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index fee1a8e4d9..07839dd032 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -462,7 +462,7 @@ processChatCommand cxt nm = \case checkValidName displayName checkProfileImageSize image checkProfileSize p - p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile + p@Profile {displayName} <- liftIO $ maybe (generateRandomProfile Nothing) pure profile u <- asks currentUser users <- withFastStore' getUsers forM_ users $ \User {profile = LocalProfile {displayName = n}, activeUser, viewPwdHash, userChatRelay = userChatRelay'} -> do @@ -2132,7 +2132,7 @@ processChatCommand cxt nm = \case Welcome -> withUser $ pure . CRWelcome APIAddContact userId incognito -> withUserId userId $ \user -> do -- [incognito] generate profile for connection - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just <$> liftIO (generateIncognitoProfile user) else pure Nothing subMode <- chatReadVar subscriptionMode (preparedReq, preparedParams@PreparedLinkParams {plpLinkKey = LinkKey linkKey}) <- withAgent $ \a -> prepareInvitationLink a (aUserId user) IKUsePQ Nothing linkProfile <- presentUserBadge user incognitoProfile (Just $ PHLink linkKey) $ userProfileDirect user incognitoProfile Nothing True @@ -2150,7 +2150,7 @@ processChatCommand cxt nm = \case let PendingContactConnection {pccConnStatus, customUserProfileId, connLinkInv} = conn case (pccConnStatus, customUserProfileId, incognito) of (ConnNew, Nothing, True) -> do - incognitoProfile <- liftIO generateRandomProfile + incognitoProfile <- liftIO $ generateIncognitoProfile user sLnk <- updatePCCShortLinkData conn $ userProfileDirect user (Just incognitoProfile) Nothing True conn' <- withFastStore' $ \db -> do pId <- createIncognitoProfile db user incognitoProfile @@ -2329,7 +2329,7 @@ processChatCommand cxt nm = \case publicMemberCount_ = (\PublicGroupData {publicMemberCount} -> publicMemberCount) <$> publicGroupData_ -- Prepare group record once before connecting to relays (updatePreparedRelayedGroup): -- set group link info and incognito profile, generate and store membership keys - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just <$> liftIO (generateIncognitoProfile user) else pure Nothing let cReqHash = contactCReqHash $ CRContactUri crData {crScheme = SSSimplex} e2e g'@(GIK gInfo' _) <- withFastStore $ \db -> do g'@(GIK gInfo' _) <- updatePreparedRelayedGroup db cxt user gInfo mainCReq cReqHash incognitoProfile rootKey publicMemberCount_ @@ -3886,7 +3886,7 @@ processChatCommand cxt nm = \case pqSup' = PQSupportOn joinNewConn chatV = do -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just <$> liftIO (generateIncognitoProfile user) else pure Nothing (connId, binding) <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup' @@ -3958,7 +3958,7 @@ processChatCommand cxt nm = \case -- [incognito] generate profile to send, or use membership profile for relay groups incognitoProfile_ <- case gInfo_ of Just (Just (GIK gInfo _)) | useRelays' gInfo -> pure $ ExistingIncognito <$> incognitoMembershipProfile gInfo - _ -> if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + _ -> if incognito then Just . NewIncognito <$> liftIO (generateIncognitoProfile user) else pure Nothing let incognitoProfile = fromIncognitoProfile <$> incognitoProfile_ subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk @@ -3974,7 +3974,7 @@ processChatCommand cxt nm = \case (connId, chatV, reqHeader) <- prepareContact user cReq pqSup newXContactId <- XContactId <$> drgRandomBytes 16 -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just <$> liftIO (generateIncognitoProfile user) else pure Nothing subMode <- chatReadVar subscriptionMode let cReqHash = contactCReqHash cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq reqHeader cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup @@ -4345,7 +4345,7 @@ processChatCommand cxt nm = \case when (isPublicGroup groupKeys && isJust (memberAdmission >>= review)) $ throwCmdError "Admission review is not supported in channels" when (not (isPublicGroup groupKeys) && isJust publicGroup) $ throwCmdError "publicGroup is not allowed in groups" -- [incognito] generate incognito profile for group membership - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just <$> liftIO (generateIncognitoProfile user) else pure Nothing withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile memberId groupKeys publicMemberCount_ createNewGroupItems :: User -> GroupInfo -> CM () createNewGroupItems user gInfo = do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 418d454ada..ced5c8c676 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -994,7 +994,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId let chatV = vr cxt `peerConnChatVersion` cReqChatVRange (ct, conn, incognitoProfile, getPresHeader) <- case contactId_ of Nothing -> do - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO (generateIncognitoProfile user) else pure Nothing (connId, binding) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' (ct, conn) <- withStore' $ \db -> createContactFromRequest db user userContactLinkId_ connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False @@ -1003,7 +1003,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConn ct of Nothing -> do - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO (generateIncognitoProfile user) else pure Nothing (connId, binding) <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' currentTs <- liftIO getCurrentTime conn <- withStore' $ \db -> do @@ -1336,6 +1336,9 @@ introduceInChannel cxt user g@(GIK gInfo _) subscriber@GroupMember {activeConn = sendIntros ms = forM_ (L.nonEmpty $ map (memberIntroEvt gInfo) ms) $ \evts -> sendGroupMemberMessages user g conn evts +generateIncognitoProfile :: User -> IO Profile +generateIncognitoProfile User {profile = LocalProfile {peerType}} = generateRandomProfile peerType + userProfileInGroup :: User -> GroupInfo -> Maybe Profile -> Profile userProfileInGroup user g = userProfileInGroup' user (Just g) {-# INLINE userProfileInGroup #-} diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 9047ca1532..ead232b3be 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -57,7 +57,6 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.Batch (batchDeliveryTasks1, batchProfiles, batchProfilesWithBody, encodeBinaryBatch, encodeFwdElement, maxBatchElementSize) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events -import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Store import Simplex.Chat.Store.Connections @@ -1535,7 +1534,7 @@ processAgentMessageConn cxt user@User {userId} entity gks_ corrId agentConnId ag Just AutoAccept {acceptIncognito} -> do incognitoProfile <- if not shortLinkDataSet && acceptIncognito - then Just . NewIncognito <$> liftIO generateRandomProfile + then Just . NewIncognito <$> liftIO (generateIncognitoProfile user) else pure Nothing ct' <- acceptContactRequestAsync user uclId ct ucr incognitoProfile toView $ CEvtAcceptingContactRequest user ct' diff --git a/src/Simplex/Chat/ProfileGenerator.hs b/src/Simplex/Chat/ProfileGenerator.hs index b460a73d3d..bc1d069540 100644 --- a/src/Simplex/Chat/ProfileGenerator.hs +++ b/src/Simplex/Chat/ProfileGenerator.hs @@ -1,16 +1,17 @@ +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.ProfileGenerator where import Data.Text (Text) -import Simplex.Chat.Types (Profile (..)) +import Simplex.Chat.Types (ChatPeerType, Profile (..)) import System.Random (randomRIO) -generateRandomProfile :: IO Profile -generateRandomProfile = do +generateRandomProfile :: Maybe ChatPeerType -> IO Profile +generateRandomProfile peerType = do adjective <- pick adjectives noun <- pickNoun adjective 2 - pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} + pure $ Profile {displayName = adjective <> noun, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType, preferences = Nothing, badge = Nothing, contactDomain = Nothing} where pick :: [a] -> IO a pick xs = (xs !!) <$> randomRIO (0, length xs - 1) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 7f08ffda73..fefef85c38 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -5108,8 +5108,8 @@ Query: Plan: Query: - INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, chat_peer_type, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?) Plan: SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 75a49090ed..bf41d2de6e 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -323,14 +323,14 @@ createConnection_ db userId connType entityId acId connStatus connChatVersion pe ent ct = if connType == ct then entityId else Nothing createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 -createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, description, image} = do +createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, shortDescr, description, image, peerType} = do DB.execute db [sql| - INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, user_id, incognito, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?) + INSERT INTO contact_profiles (display_name, full_name, short_descr, description, image, chat_peer_type, user_id, incognito, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (displayName, fullName, shortDescr, description, image, userId, Just (BI True), createdAt, createdAt) + (displayName, fullName, shortDescr, description, image, peerType, userId, Just (BI True), createdAt, createdAt) insertedRowId db updateConnSupportPQ :: DB.Connection -> Int64 -> PQSupport -> PQEncryption -> IO () diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 3779006d8d..91ec9a54da 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -32,7 +32,7 @@ import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) import Simplex.Chat.Protocol (ChatBinding (..), ChatMsgEvent (XInfo), ContactShortLinkData (ContactShortLinkData), LinkOwnerSig, MsgChatLink (..), MsgContent (..), encodeChatBinding) import Simplex.Chat.Store.Groups (getGroupMembers) import Simplex.Chat.Store.Shared (createContact, getGroupInfoKeys) -import Simplex.Chat.Types (AgentConnId (..), ConnStatus (..), GroupId, GroupInfo (..), GroupInfoKeys (..), GroupMember (..), Profile (..), GroupRejectionReason (..), fromLocalProfile, profileFromName, userProfileDirect, pattern VersionChat) +import Simplex.Chat.Types (AgentConnId (..), ChatPeerType (..), ConnStatus (..), GroupId, GroupInfo (..), GroupInfoKeys (..), GroupMember (..), Profile (..), GroupRejectionReason (..), fromLocalProfile, profileFromName, userProfileDirect, pattern VersionChat) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) @@ -133,6 +133,8 @@ chatProfileTests = do it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito + it "join group incognito as bot" $ testJoinGroupIncognitoPeerType (Just CPTBot) + it "join group incognito as human" $ testJoinGroupIncognitoPeerType Nothing it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito it "deleting contact first, group second deletes incognito profile" testDeleteContactThenGroupDeletesIncognitoProfile @@ -4905,6 +4907,36 @@ testShortLinkConnectPreparedGroupIncognito = testChat3 aliceProfile bobProfile c alice <# "#team cath> 3" bob ?<# "#team cath> 3" +testJoinGroupIncognitoPeerType :: HasCallStack => Maybe ChatPeerType -> TestParams -> IO () +testJoinGroupIncognitoPeerType peerType_ = testChat3 aliceProfile bobProfile {peerType = peerType_} cathProfile test + where + test alice bob cath = do + createGroup2 "team" alice cath + alice ##> "/create link #team" + (shortLink, fullLink) <- getGroupLinks alice "team" GRMember True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: ok to connect directly" + groupSLinkData <- getTermLine bob + bob ##> ("/_prepare group 1 " <> fullLink <> " " <> shortLink <> " " <> groupSLinkData) + bob <## "#team: group is prepared" + bob ##> "/_connect group #1 incognito=on" + bobIncognito <- getTermLine bob + bob <## "#team: connection started incognito" + alice <## (bobIncognito <> ": accepting request to join group #team...") + concurrentlyN_ + [ alice <## ("#team: " <> bobIncognito <> " joined the group"), + do + bob <## "#team: joining the group..." + bob <## ("#team: you joined the group incognito as " <> bobIncognito) + bob <## "#team: member cath (Catherine) is connected", + do + cath <## ("#team: alice added " <> bobIncognito <> " to the group (connecting...)") + cath <## ("#team: new member " <> bobIncognito <> " is connected") + ] + forM_ [alice, cath] $ \cc -> do + GroupMember {memberProfile} <- getTestMember cc "team" (T.pack bobIncognito) + peerType (fromLocalProfile memberProfile) `shouldBe` peerType_ + testShortLinkChangePreparedGroupUser :: HasCallStack => TestParams -> IO () testShortLinkChangePreparedGroupUser = testChat3 aliceProfile bobProfile cathProfile test where