diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 203e790f1e..9604a1675f 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -3068,6 +3068,7 @@ SubscribeError: - pastTimestamp: bool - userChatRelay: bool - clientService: bool +- keepActiveUser: bool --- diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 09c88069e2..00546839bf 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -3367,6 +3367,7 @@ export interface NewUser { pastTimestamp: boolean userChatRelay: boolean clientService: boolean + keepActiveUser: boolean } export interface NoteFolder { diff --git a/packages/simplex-chat-nodejs/src/api.ts b/packages/simplex-chat-nodejs/src/api.ts index 958304a8ea..069da85c12 100644 --- a/packages/simplex-chat-nodejs/src/api.ts +++ b/packages/simplex-chat-nodejs/src/api.ts @@ -867,7 +867,7 @@ export class ChatApi { * Network usage: no. */ async apiCreateActiveUser(profile?: T.Profile): Promise { - const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false, clientService: false}})) + const r = await this.sendChatCmd(CC.CreateActiveUser.cmdString({newUser: {profile, pastTimestamp: false, userChatRelay: false, clientService: false, keepActiveUser: false}})) if (r.type === "activeUser") return r.user throw new ChatCommandError("unexpected response", r) } diff --git a/packages/simplex-chat-python/src/simplex_chat/api.py b/packages/simplex-chat-python/src/simplex_chat/api.py index e3d36c45df..5180f2a0dc 100644 --- a/packages/simplex-chat-python/src/simplex_chat/api.py +++ b/packages/simplex-chat-python/src/simplex_chat/api.py @@ -660,7 +660,7 @@ class ChatApi: raise async def api_create_active_user(self, profile: T.Profile | None = None) -> T.User: - new_user: T.NewUser = {"pastTimestamp": False, "userChatRelay": False, "clientService": False} + new_user: T.NewUser = {"pastTimestamp": False, "userChatRelay": False, "clientService": False, "keepActiveUser": False} if profile is not None: new_user["profile"] = profile r = await self.send_chat_cmd(CC.CreateActiveUser_cmd_string({"newUser": new_user})) diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index 25206c8654..a91dc8be41 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -2356,6 +2356,7 @@ class NewUser(TypedDict): pastTimestamp: bool userChatRelay: bool clientService: bool + keepActiveUser: bool class NoteFolder(TypedDict): noteFolderId: int # int64 diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index f5f7f581df..9ea9bb8124 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -160,7 +160,7 @@ createActiveUser cc CoreChatOpts {chatRelay, headless} createBot_ userDisplayNam createUser loop False $ mkProfile displayName mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} createUser onError clientService p = - execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService}) 0 `runReaderT` cc >>= \case + execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, userChatRelay = BoolDef chatRelay, clientService = BoolDef clientService, keepActiveUser = BoolDef False}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user r -> printResponseEvent (Nothing, Nothing) (config cc) r >> onError diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 2f2a25d8fd..4e6ea583f0 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -420,7 +420,7 @@ parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace processChatCommand :: StoreCxt -> NetworkRequestMode -> ChatCommand -> CM ChatResponse processChatCommand cxt nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay, clientService} -> do + CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay, clientService, keepActiveUser} -> do forM_ profile $ \p@Profile {displayName, image} -> do checkValidName displayName checkProfileImageSize image @@ -432,17 +432,19 @@ processChatCommand cxt nm = \case when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} when (isTrue userChatRelay && isTrue userChatRelay') $ throwChatError CEChatRelayExists - (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u + curUser_ <- readTVarIO u + (uss, (smp', xftp')) <- chooseServers curUser_ let service = isTrue clientService + activateNewUser = isNothing curUser_ || not (isTrue keepActiveUser) auId <- withAgent $ \a -> createUser a service smp' xftp' ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> do - user <- createUserRecordAt db (AgentUserId auId) (isTrue userChatRelay) service p True ts + user <- createUserRecordAt db (AgentUserId auId) (isTrue userChatRelay) service p activateNewUser ts mapM_ (setUserServers db user ts) uss createPresetContactCards db user `catchAllErrors` \_ -> pure () createNoteFolder db user pure user - atomically . writeTVar u $ Just user + when activateNewUser $ atomically . writeTVar u $ Just user pure $ CRActiveUser user where createPresetContactCards :: DB.Connection -> User -> ExceptT StoreError IO () @@ -5930,7 +5932,7 @@ chatCommandP = (cName, shortDescr) <- profileNameDescr service <- (" service=" *> onOffP) <|> pure False let profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing, badge = Nothing, contactDomain = Nothing} - pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service} + pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef relay, clientService = BoolDef service, keepActiveUser = BoolDef False} newBotUserP = do files_ <- optional $ "files=" *> onOffP <* A.space service <- ("service=" *> onOffP <* A.space) <|> pure False @@ -5939,7 +5941,7 @@ chatCommandP = Just True -> Nothing _ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}} profile = Just Profile {displayName = cName, fullName = "", shortDescr, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences, badge = Nothing, contactDomain = Nothing} - pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service} + pure NewUser {profile, pastTimestamp = False, userChatRelay = BoolDef False, clientService = BoolDef service, keepActiveUser = BoolDef False} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index c0e44277a4..611427b525 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -155,7 +155,8 @@ data NewUser = NewUser { profile :: Maybe Profile, pastTimestamp :: Bool, userChatRelay :: BoolDef, - clientService :: BoolDef + clientService :: BoolDef, + keepActiveUser :: BoolDef } deriving (Show) diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index efe2097504..785ed093cb 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -52,6 +52,7 @@ chatProfileTests = do it "reject profile image that is too large" testSetProfileImageTooLarge it "set profile image from file" testSetProfileImageFromFile it "use multiword profile names" testMultiWordProfileNames + it "create user without keepActiveUser activates it" testCreateUserWithoutKeepActiveUser it "auto-accept group invitations" testAutoAcceptGroupInvitations it "auto-accept group invitations on a second profile" testAutoAcceptGroupInvitationsSecondProfile it "auto-accept group invitations on an inactive profile" testAutoAcceptGroupInvitationsInactiveProfile @@ -156,6 +157,7 @@ shortLinkTests = do it "connect to prepared contact incognito (via address)" testShortLinkAddressConnectPreparedContactIncognito it "change prepared contact user" testShortLinkChangePreparedContactUser it "change prepared contact user, new user has contact with the same name" testShortLinkChangePreparedContactUserDuplicate + it "create user keeping active user, then change prepared contact user" testCreateUserKeepingActiveUser it "connect to prepared group incognito" testShortLinkConnectPreparedGroupIncognito it "change prepared group user" testShortLinkChangePreparedGroupUser it "change prepared group user, new user has group with the same name" testShortLinkChangePreparedGroupUserDuplicate @@ -4050,6 +4052,38 @@ testShortLinkChangePreparedContactUser = testChat2 aliceProfile bobProfile test bob @@@ [] bob `hasContactProfiles` ["bob"] +testCreateUserKeepingActiveUser :: HasCallStack => TestParams -> IO () +testCreateUserKeepingActiveUser = testChat2 aliceProfile bobProfile test + where + test alice bob = do + bob ##> "/_create user {\"profile\":{\"displayName\":\"robert\",\"fullName\":\"\"},\"pastTimestamp\":false,\"keepActiveUser\":true}" + showActiveUser bob "robert" + bob ##> "/u" + showActiveUser bob "bob (Bob)" + bob ##> "/users" + bob <## "bob (Bob) (active)" + bob <## "robert" + + alice ##> "/_connect 1" + (shortLink, fullLink) <- getInvitations alice + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "invitation link: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "alice: contact is prepared" + + bob ##> "/_set contact user @4 2" + bob <## "contact alice changed from user bob to user robert" + +testCreateUserWithoutKeepActiveUser :: HasCallStack => TestParams -> IO () +testCreateUserWithoutKeepActiveUser = testChat aliceProfile test + where + test alice = do + alice ##> "/_create user {\"profile\":{\"displayName\":\"alisa\",\"fullName\":\"\"},\"pastTimestamp\":false}" + showActiveUser alice "alisa" + alice ##> "/u" + showActiveUser alice "alisa" + testShortLinkChangePreparedContactUserDuplicate :: HasCallStack => TestParams -> IO () testShortLinkChangePreparedContactUserDuplicate = testChat2 aliceProfile bobProfile test where