From bc1e8b6de78ef0c61d80dabb50a9773103237e47 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:38:22 +0000 Subject: [PATCH] core: allow creating a user without making it active Adds keepActiveUser to NewUser. When set, the new user is created but the current active user is preserved. This is needed to create a profile for an invitation: the prepared chat is resolved under the active user by APIChangePreparedContactUser, so the profile that owns the invitation has to stay active until the chat has been moved to the new one. Creating and activating in one step makes that reassignment impossible without switching back and forth. BoolDef gives the field omittedField = False, so clients that do not send it - iOS, the CLI, and any older caller - keep the current behaviour of activating the new user. keepActiveUser is ignored when there is no active user to keep, which would otherwise leave the app with none at all. Note the response is still CRActiveUser: it carries the created user, which is not the active one on this path. Documented at the field. Regenerates the API docs and the generated TypeScript and Python client types, which are checked by the Bot API docs tests. The generator emits BoolDef fields as required in the client types, so the two hand-maintained clients that build a NewUser literal - simplex-chat-python and simplex-chat-nodejs - stop type-checking until the field is added there too. No test covers those; a4e3a1ea1 did the same when clientService was added. --- bots/api/TYPES.md | 1 + .../types/typescript/src/types.ts | 1 + packages/simplex-chat-nodejs/src/api.ts | 2 +- .../simplex-chat-python/src/simplex_chat/api.py | 2 +- .../src/simplex_chat/types/_types.py | 1 + src/Simplex/Chat/Core.hs | 2 +- src/Simplex/Chat/Library/Commands.hs | 16 ++++++++++------ src/Simplex/Chat/Types.hs | 6 +++++- 8 files changed, 21 insertions(+), 10 deletions(-) diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 4546a6aaa7..62eebd0e71 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -3054,6 +3054,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 5faf084dce..4b41a0e5db 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -3348,6 +3348,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 51de063329..35bca24f53 100644 --- a/packages/simplex-chat-python/src/simplex_chat/api.py +++ b/packages/simplex-chat-python/src/simplex_chat/api.py @@ -642,7 +642,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 b00a559f92..043c924edb 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -2337,6 +2337,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 c382a6dc8e..7b0ee24f95 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 88cb4532ef..5cbb9e5a30 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -415,7 +415,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 @@ -427,17 +427,21 @@ 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 + -- keepActiveUser can only be honoured when there is a user to keep, + -- otherwise it would leave no active user at all. + 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 () @@ -5907,7 +5911,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 @@ -5916,7 +5920,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 7bccd04b71..8645ea7ab4 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -154,7 +154,11 @@ data NewUser = NewUser { profile :: Maybe Profile, pastTimestamp :: Bool, userChatRelay :: BoolDef, - clientService :: BoolDef + clientService :: BoolDef, + -- when set, the user is created without becoming active, preserving the current + -- one; absent/False activates it as before. The response is CRActiveUser either + -- way - it carries the created user, which is then not the active one. + keepActiveUser :: BoolDef } deriving (Show)