diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs index d9f091a13a..809c67101d 100644 --- a/apps/simplex-broadcast-bot/src/Broadcast/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -94,6 +94,6 @@ mkChatOpts BroadcastBotOpts {coreOptions, botDisplayName} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName, allowFiles = False}, + createBot = Just CreateBotOpts {botDisplayName, allowFiles = False, clientService = False}, maintenance = False } diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs index fb82c27b78..e093678b73 100644 --- a/apps/simplex-directory-service/src/Directory/Options.hs +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -36,6 +36,7 @@ data DirectoryOpts = DirectoryOpts directoryLog :: Maybe FilePath, migrateDirectoryLog :: Maybe MigrateLog, serviceName :: T.Text, + clientService :: Bool, runCLI :: Bool, searchResults :: Int, webFolder :: Maybe FilePath, @@ -135,6 +136,11 @@ directoryOpts appDir defaultDbName = do <> help "The display name of the directory service bot, without *'s and spaces (SimpleX Directory)" <> value "SimpleX Directory" ) + clientService <- + switch + ( long "client-service" + <> help "Use client service certificate" + ) runCLI <- switch ( long "run-cli" @@ -162,6 +168,7 @@ directoryOpts appDir defaultDbName = do directoryLog, migrateDirectoryLog, serviceName = T.pack serviceName, + clientService, runCLI, searchResults = 10, webFolder, @@ -180,7 +187,7 @@ getDirectoryOpts appDir defaultDbName = versionAndUpdate = versionStr <> "\n" <> updateStr mkChatOpts :: DirectoryOpts -> ChatOpts -mkChatOpts DirectoryOpts {coreOptions, serviceName} = +mkChatOpts DirectoryOpts {coreOptions, serviceName, clientService} = ChatOpts { coreOptions, chatCmd = "", @@ -194,7 +201,7 @@ mkChatOpts DirectoryOpts {coreOptions, serviceName} = autoAcceptFileSize = 0, muteNotifications = True, markRead = False, - createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False}, + createBot = Just CreateBotOpts {botDisplayName = serviceName, allowFiles = False, clientService}, maintenance = False } diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 62c5dff3b5..3343a746a3 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -2665,6 +2665,7 @@ SubscribeError: **Record type**: - profile: [Profile](#profile)? - pastTimestamp: bool +- clientService: bool --- @@ -3723,6 +3724,7 @@ Handshake: - sendRcptsSmallGroups: bool - autoAcceptMemberContacts: bool - userMemberProfileUpdatedAt: UTCTime? +- clientService: bool - uiThemes: [UIThemeEntityOverrides](#uithemeentityoverrides)? diff --git a/cabal.project b/cabal.project index 89a38840b7..5579fac67c 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: bafdbc1dec778021eacbf621f1467ca78287d2a4 + tag: 596e7b31069d623469f46002aeeb1ce118222737 source-repository-package type: git diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 5e3309238b..55bc6b1920 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -2953,6 +2953,7 @@ export namespace NetworkError { export interface NewUser { profile?: Profile pastTimestamp: boolean + clientService: boolean } export interface NoteFolder { @@ -4408,6 +4409,7 @@ export interface User { sendRcptsSmallGroups: boolean autoAcceptMemberContacts: boolean userMemberProfileUpdatedAt?: string // ISO-8601 timestamp + clientService: boolean uiThemes?: UIThemeEntityOverrides } diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 2a5f980fd0..11e4f11e74 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bafdbc1dec778021eacbf621f1467ca78287d2a4" = "09nkjrwb3p822m01p3lnmmfh2jdfyf0jl9hvryzkpl12xvlsl61c"; + "https://github.com/simplex-chat/simplexmq.git"."596e7b31069d623469f46002aeeb1ce118222737" = "14vagl7ww5gyld3lwa3c4sn49spp1sanwbjnfq9ff5p65p9qnk2l"; "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/simplex-chat.cabal b/simplex-chat.cabal index 155fc0d4d2..43e2e96e04 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -124,6 +124,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 + Simplex.Chat.Store.Postgres.Migrations.M20251225_client_services else exposed-modules: Simplex.Chat.Archive @@ -271,6 +272,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 + Simplex.Chat.Store.SQLite.Migrations.M20251225_client_services other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index ace3984566..4970cd91f6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -122,7 +122,7 @@ createChatDatabase chatDbOpts migrationConfig = runExceptT $ do agentStore <- ExceptT $ createAgentStore (toDBOpts chatDbOpts agentSuffix False []) migrationConfig pure ChatDatabase {chatStore, agentStore} -newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO ChatController +newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Bool -> IO (Either AgentErrorType ChatController) newChatController ChatDatabase {chatStore, agentStore} user @@ -132,8 +132,6 @@ newChatController let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} - firstTime = dbNew chatStore - currentUser <- newTVarIO user randomPresetServers <- chooseRandomServers presetServers' let rndSrvs = L.toList randomPresetServers operatorWithId (i, op) = (\o -> o {operatorId = DBEntityId i}) <$> pOperator op @@ -141,84 +139,87 @@ newChatController agentSMP <- randomServerCfgs "agent SMP servers" SPSMP opDomains rndSrvs agentXFTP <- randomServerCfgs "agent XFTP servers" SPXFTP opDomains rndSrvs let randomAgentServers = RandomAgentServers {smpServers = agentSMP, xftpServers = agentXFTP} - currentRemoteHost <- newTVarIO Nothing servers <- withTransaction chatStore $ \db -> agentServers db config randomPresetServers randomAgentServers - smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode - agentAsync <- newTVarIO Nothing - random <- liftIO C.newRandom - eventSeq <- newTVarIO 0 - inputQ <- newTBQueueIO tbqSize - outputQ <- newTBQueueIO tbqSize - subscriptionMode <- newTVarIO SMSubscribe - chatLock <- newEmptyTMVarIO - entityLocks <- TM.emptyIO - sndFiles <- newTVarIO M.empty - rcvFiles <- newTVarIO M.empty - currentCalls <- TM.emptyIO - localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName - multicastSubscribers <- newTMVarIO 0 - remoteSessionSeq <- newTVarIO 0 - remoteHostSessions <- TM.emptyIO - remoteHostsFolder <- newTVarIO Nothing - remoteCtrlSession <- newTVarIO Nothing - filesFolder <- newTVarIO optFilesFolder - chatStoreChanged <- newTVarIO False - deliveryTaskWorkers <- TM.emptyIO - deliveryJobWorkers <- TM.emptyIO - expireCIThreads <- TM.emptyIO - expireCIFlags <- TM.emptyIO - cleanupManagerAsync <- newTVarIO Nothing - timedItemThreads <- TM.emptyIO - chatActivated <- newTVarIO True - showLiveItems <- newTVarIO False - encryptLocalFiles <- newTVarIO False - tempDirectory <- newTVarIO optTempDirectory - assetsDirectory <- newTVarIO Nothing - contactMergeEnabled <- newTVarIO True - pure - ChatController - { firstTime, - currentUser, - randomPresetServers, - randomAgentServers, - currentRemoteHost, - smpAgent, - agentAsync, - chatStore, - chatStoreChanged, - random, - eventSeq, - inputQ, - outputQ, - subscriptionMode, - chatLock, - entityLocks, - sndFiles, - rcvFiles, - currentCalls, - localDeviceName, - multicastSubscribers, - remoteSessionSeq, - remoteHostSessions, - remoteHostsFolder, - remoteCtrlSession, - config, - filesFolder, - deliveryTaskWorkers, - deliveryJobWorkers, - expireCIThreads, - expireCIFlags, - cleanupManagerAsync, - timedItemThreads, - chatActivated, - showLiveItems, - encryptLocalFiles, - tempDirectory, - assetsDirectory, - logFilePath = logFile, - contactMergeEnabled - } + runExceptT (getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode) + >>= mapM (mkChatController config randomPresetServers randomAgentServers) where + mkChatController config randomPresetServers randomAgentServers smpAgent = do + currentUser <- newTVarIO user + currentRemoteHost <- newTVarIO Nothing + agentAsync <- newTVarIO Nothing + random <- liftIO C.newRandom + eventSeq <- newTVarIO 0 + inputQ <- newTBQueueIO tbqSize + outputQ <- newTBQueueIO tbqSize + subscriptionMode <- newTVarIO SMSubscribe + chatLock <- newEmptyTMVarIO + entityLocks <- TM.emptyIO + sndFiles <- newTVarIO M.empty + rcvFiles <- newTVarIO M.empty + currentCalls <- TM.emptyIO + localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName + multicastSubscribers <- newTMVarIO 0 + remoteSessionSeq <- newTVarIO 0 + remoteHostSessions <- TM.emptyIO + remoteHostsFolder <- newTVarIO Nothing + remoteCtrlSession <- newTVarIO Nothing + filesFolder <- newTVarIO optFilesFolder + chatStoreChanged <- newTVarIO False + deliveryTaskWorkers <- TM.emptyIO + deliveryJobWorkers <- TM.emptyIO + expireCIThreads <- TM.emptyIO + expireCIFlags <- TM.emptyIO + cleanupManagerAsync <- newTVarIO Nothing + timedItemThreads <- TM.emptyIO + chatActivated <- newTVarIO True + showLiveItems <- newTVarIO False + encryptLocalFiles <- newTVarIO False + tempDirectory <- newTVarIO optTempDirectory + assetsDirectory <- newTVarIO Nothing + contactMergeEnabled <- newTVarIO True + pure + ChatController + { firstTime = dbNew chatStore, + currentUser, + randomPresetServers, + randomAgentServers, + currentRemoteHost, + smpAgent, + agentAsync, + chatStore, + chatStoreChanged, + random, + eventSeq, + inputQ, + outputQ, + subscriptionMode, + chatLock, + entityLocks, + sndFiles, + rcvFiles, + currentCalls, + localDeviceName, + multicastSubscribers, + remoteSessionSeq, + remoteHostSessions, + remoteHostsFolder, + remoteCtrlSession, + config, + filesFolder, + deliveryTaskWorkers, + deliveryJobWorkers, + expireCIThreads, + expireCIFlags, + cleanupManagerAsync, + timedItemThreads, + chatActivated, + showLiveItems, + encryptLocalFiles, + tempDirectory, + assetsDirectory, + logFilePath = logFile, + contactMergeEnabled + } presetServers' :: PresetServers presetServers' = presetServers {operators = operators', netCfg = netCfg'} where @@ -250,8 +251,8 @@ newChatController ops <- getUpdateServerOperators db presetOps (null users) let opDomains = operatorDomains $ mapMaybe snd ops (smp', xftp') <- unzip <$> mapM (getServers ops opDomains) users - -- TODO [certs rcv] load user profile service settings from database - pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, useServices = M.empty, presetDomains, presetServers = L.toList allPresetServers} + let useServices = M.fromList $ map (\User {agentUserId = AgentUserId uId, clientService} -> (uId, isTrue clientService)) users + pure InitialAgentServers {smp = M.fromList (optServers smp' smpServers), xftp = M.fromList (optServers xftp' xftpServers), ntf, netCfg, useServices, presetDomains, presetServers = L.toList allPresetServers} where optServers :: [(UserId, NonEmpty (ServerCfg p))] -> [ProtoServerWithAuth p] -> [(UserId, NonEmpty (ServerCfg p))] optServers srvs overrides_ = case L.nonEmpty overrides_ of diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8003f66324..1f797a79b4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -1336,6 +1336,9 @@ data SQLiteError = SQLiteErrorNotADatabase | SQLiteError {dbError :: String} throwDBError :: DatabaseError -> CM () throwDBError = throwError . ChatErrorDatabase +chatErrorAgent :: AgentErrorType -> ChatError +chatErrorAgent e = ChatErrorAgent e (AgentConnId B.empty) Nothing + -- TODO review errors, some of it can be covered by HTTP2 errors data RemoteHostError = RHEMissing -- No remote session matches this identifier @@ -1565,7 +1568,7 @@ withAgent :: (AgentClient -> ExceptT AgentErrorType IO a) -> CM a withAgent action = asks smpAgent >>= liftIO . runExceptT . action - >>= liftEither . first (\e -> ChatErrorAgent e (AgentConnId "") Nothing) + >>= liftEither . first chatErrorAgent withAgent' :: (AgentClient -> IO a) -> CM' a withAgent' action = asks smpAgent >>= liftIO . action diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 131b420bd9..b405c4ee16 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -53,11 +53,15 @@ simplexChatCore cfg@ChatConfig {confirmMigrations, testView, chatHooks} opts@Cha run db@ChatDatabase {chatStore} = do u_ <- getSelectActiveUser chatStore let backgroundMode = not maintenance - cc <- newChatController db u_ cfg opts backgroundMode - u <- maybe (createActiveUser cc createBot) pure u_ - unless testView $ putStrLn $ "Current user: " <> userStr u - unless maintenance $ forM_ (preStartHook chatHooks) ($ cc) - runSimplexChat opts u cc chat + newChatController db u_ cfg opts backgroundMode >>= \case + Left e -> do + putStrLn $ "Error starting chat: " <> show e + exitFailure + Right cc -> do + u <- maybe (createActiveUser cc createBot) pure u_ + unless testView $ putStrLn $ "Current user: " <> userStr u + unless maintenance $ forM_ (preStartHook chatHooks) ($ cc) + runSimplexChat opts u cc chat runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController -> IO ()) -> IO () runSimplexChat ChatOpts {maintenance} u cc@ChatController {config = ChatConfig {chatHooks}} chat @@ -102,9 +106,9 @@ getSelectActiveUser st = do createActiveUser :: ChatController -> Maybe CreateBotOpts -> IO User createActiveUser cc = \case - Just CreateBotOpts {botDisplayName, allowFiles} -> do + Just CreateBotOpts {botDisplayName, allowFiles, clientService} -> do let preferences = if allowFiles then Nothing else Just emptyChatPrefs {files = Just FilesPreference {allow = FANo}} - createUser exitFailure $ (mkProfile botDisplayName) {peerType = Just CPTBot, preferences} + createUser exitFailure clientService $ (mkProfile botDisplayName) {peerType = Just CPTBot, preferences} Nothing -> do putStrLn "No user profiles found, it will be created now.\n\ @@ -115,10 +119,10 @@ createActiveUser cc = \case where loop = do displayName <- T.pack <$> getWithPrompt "display name" - createUser loop $ mkProfile displayName + createUser loop False $ mkProfile displayName mkProfile displayName = Profile {displayName, fullName = "", shortDescr = Nothing, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing} - createUser onError p = - execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False}) 0 `runReaderT` cc >>= \case + createUser onError clientService p = + execChatCommand' (CreateActiveUser NewUser {profile = Just p, pastTimestamp = False, clientService = BoolDef clientService}) 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 121a317667..f3a218a9d4 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -334,7 +334,7 @@ parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> CM ChatResponse processChatCommand vr nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser NewUser {profile, pastTimestamp} -> do + CreateActiveUser NewUser {profile, pastTimestamp, clientService} -> do forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser @@ -343,10 +343,10 @@ processChatCommand vr nm = \case when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} (uss, (smp', xftp')) <- chooseServers =<< readTVarIO u - auId <- withAgent $ \a -> createUser a smp' xftp' + auId <- withAgent $ \a -> createUser a clientService smp' xftp' ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> do - user <- createUserRecordAt db (AgentUserId auId) p True ts + user <- createUserRecordAt db (AgentUserId auId) (isTrue clientService) p True ts mapM_ (setUserServers db user ts) uss createPresetContactCards db user `catchAllErrors` \_ -> pure () createNoteFolder db user @@ -1581,7 +1581,7 @@ processChatCommand vr nm = \case pure $ CRChatItemTTL user (Just ttl) GetChatItemTTL -> withUser' $ \User {userId} -> do processChatCommand vr nm $ APIGetChatItemTTL userId - APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ + APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> CRNetworkConfig <$> lift getNetworkConfig SetNetworkConfig simpleNetCfg -> do @@ -4162,7 +4162,7 @@ agentSubscriber = do q <- asks $ subQ . smpAgent forever (atomically (readTBQueue q) >>= process) `E.catchAny` \e -> do - eToView' $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) (AgentConnId "") Nothing + eToView' $ chatErrorAgent $ CRITICAL True $ "Message reception stopped: " <> show e E.throwIO e where process :: (ACorrId, AEntityId, AEvt) -> CM' () @@ -4772,16 +4772,18 @@ chatCommandP = quoted = A.char '\'' *> A.takeTill (== '\'') <* A.char '\'' newUserP = do (cName, shortDescr) <- profileNameDescr + service <- (" service=" *> onOffP) <|> pure False let profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Nothing, preferences = Nothing} - pure NewUser {profile, pastTimestamp = False} + pure NewUser {profile, pastTimestamp = False, clientService = BoolDef service} newBotUserP = do files_ <- optional $ "files=" *> onOffP <* A.space + service <- ("service=" *> onOffP <* A.space) <|> pure False (cName, shortDescr) <- profileNameDescr let preferences = case files_ of Just True -> Nothing _ -> Just (emptyChatPrefs :: Preferences) {files = Just FilesPreference {allow = FANo}} profile = Just Profile {displayName = cName, fullName = "", shortDescr, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences} - pure NewUser {profile, pastTimestamp = False} + pure NewUser {profile, pastTimestamp = False, clientService = BoolDef service} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 52b40774c5..71db543e0e 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1879,7 +1879,7 @@ deliverMessagesB msgReqs = do Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it prepareBatch (Right req) (Right ar) = Right (req, ar) prepareBatch (Left ce) _ = Left ce -- restore original ChatError - prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae (AgentConnId "") Nothing + prepareBatch _ (Left ae) = Left $ chatErrorAgent ae createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption)) createDelivery db ((Connection {connId}, _, (_, msgIds)), (agentMsgId, pqEnc')) = do Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index da810a6cb5..b6e79a4469 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -105,7 +105,7 @@ processAgentMessage _ _ (DEL_RCVQS delQs) = processAgentMessage _ _ (DEL_CONNS connIds) = toView $ CEvtAgentConnsDeleted $ L.map AgentConnId connIds processAgentMessage _ "" (ERR e) = - eToView $ ChatErrorAgent e (AgentConnId "") Nothing + eToView $ chatErrorAgent e processAgentMessage corrId connId msg = do lockEntity <- critical connId (withStore (`getChatLockEntity` AgentConnId connId)) withEntityLock "processAgentMessage" lockEntity $ do @@ -136,6 +136,11 @@ processAgentMessageNoConn = \case UP srv conns -> serverEvent srv SSActive conns SUSPENDED -> toView CEvtChatSuspended DEL_USER agentUserId -> toView $ CEvtAgentUserDeleted agentUserId + -- TODO [certs rcv] chat events + SERVICE_ALL _ -> pure () + SERVICE_DOWN _ _ -> pure () + SERVICE_UP _ _ -> pure () + SERVICE_END _ _ -> pure () ERRS cErrs -> errsEvent $ L.toList cErrs where hostEvent :: ChatEvent -> CM () @@ -1393,7 +1398,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless shouldDelConns $ withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing -- If showCritical is True, then these errors don't result in ACK and show user visible alert -- This prevents losing the message that failed to be processed. - Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) (AgentConnId "") Nothing + Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ chatErrorAgent $ CRITICAL True message Left e -> do withLog (eInfo <> " error: " <> tshow e) $ ackMsg msgMeta Nothing throwError e @@ -2874,10 +2879,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = fromGroupId_ = Just groupId, fromGroupMemberId_ = Just (groupMemberId' m), fromGroupMemberConnId_ = Just mConnId, - groupDirectInvStartedConnection = isTrue $ autoAcceptMemberContacts user + groupDirectInvStartedConnection = autoAcceptMemberContacts user } joinExistingContact subMode mCt@Contact {contactId = mContactId} - | isTrue (autoAcceptMemberContacts user) = do + | autoAcceptMemberContacts user = do (cmdId, acId) <- joinConn subMode mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv @@ -2895,7 +2900,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectRcv mCt') (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing createItems mCt' m createNewContact subMode - | isTrue (autoAcceptMemberContacts user) = do + | autoAcceptMemberContacts user = do (cmdId, acId) <- joinConn subMode -- [incognito] reuse membership incognito profile (mCt, m') <- withStore $ \db -> do diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 3c8f170b0d..a4e573d877 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -49,6 +49,7 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) +import Simplex.Messaging.Agent.Protocol (AgentErrorType) import Simplex.Messaging.Agent.Store.Interface (closeDBStore, reopenDBStore) import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Crypto as C @@ -72,6 +73,7 @@ data DBMigrationResult | DBMErrorNotADatabase {dbFile :: String} | DBMErrorMigration {dbFile :: String, migrationError :: MigrationError} | DBMErrorSQL {dbFile :: String, migrationSQLError :: String} + | DBMAgentError {agentError :: AgentErrorType} deriving (Show) $(JQ.deriveToJSON (sumTypeJSON $ dropPrefix "DBM") ''DBMigrationResult) @@ -297,12 +299,12 @@ chatMigrateInitKey chatDbOpts keepKey confirm backgroundMode = runExceptT $ do let migrationConfig = MigrationConfig confirmMigrations (Just "") chatStore <- migrate createChatStore (toDBOpts chatDbOpts chatSuffix keepKey chatDBFunctions) migrationConfig agentStore <- migrate createAgentStore (toDBOpts chatDbOpts agentSuffix keepKey []) migrationConfig - liftIO $ initialize chatStore ChatDatabase {chatStore, agentStore} + ExceptT $ initialize chatStore ChatDatabase {chatStore, agentStore} where opts = mobileChatOpts $ removeDbKey chatDbOpts initialize st db = do - user_ <- getActiveUser_ st - newChatController db user_ defaultMobileConfig opts backgroundMode + user_ <- liftIO $ getActiveUser_ st + first DBMAgentError <$> newChatController db user_ defaultMobileConfig opts backgroundMode migrate createStore dbOpts confirmMigrations = ExceptT $ (first (DBMErrorMigration errDbStr) <$> createStore dbOpts confirmMigrations) diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index afc01e0493..345f9ef623 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -73,7 +73,8 @@ data CoreChatOpts = CoreChatOpts data CreateBotOpts = CreateBotOpts { botDisplayName :: Text, - allowFiles :: Bool + allowFiles :: Bool, + clientService :: Bool } data ChatCmdLog = CCLAll | CCLMessages | CCLNone @@ -376,6 +377,11 @@ chatOptsP appDir defaultDbName = do ( long "create-bot-allow-files" <> help "Flag for created bot to allow files (only allowed together with --create-bot option)" ) + createBotClientService <- + switch + ( long "create-bot-client-service" + <> help "Flag for created bot to use client service certificate" + ) maintenance <- switch ( long "maintenance" @@ -397,9 +403,10 @@ chatOptsP appDir defaultDbName = do muteNotifications, markRead, createBot = case createBotDisplayName of - Just botDisplayName -> Just CreateBotOpts {botDisplayName, allowFiles = createBotAllowFiles} + Just botDisplayName -> Just CreateBotOpts {botDisplayName, allowFiles = createBotAllowFiles, clientService = createBotClientService} Nothing | createBotAllowFiles -> error "--create-bot-allow-files option requires --create-bot-name option" + | createBotClientService -> error "--create-bot-client-service option requires --create-bot-name option" | otherwise -> Nothing, maintenance } diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 255af5f318..a0c37150b5 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -535,7 +535,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo Left e -> eToView' $ ChatErrorRemoteCtrl $ RCEProtocolError e takeRCStep :: RCStepTMVar a -> CM a -takeRCStep = liftError' (\e -> ChatErrorAgent {agentError = RCP e, agentConnId = AgentConnId "", connectionEntity_ = Nothing}) . atomically . takeTMVar +takeRCStep = liftError' (chatErrorAgent . RCP) . atomically . takeTMVar type GetChunk = Int -> IO ByteString diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20251225_client_services.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20251225_client_services.hs new file mode 100644 index 0000000000..54553893be --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20251225_client_services.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20251225_client_services where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20251225_client_services :: Text +m20251225_client_services = + [r| +ALTER TABLE users ADD COLUMN client_service SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20251225_client_services :: Text +down_m20251225_client_services = + [r| +ALTER TABLE users DROP COLUMN client_service; +|] diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index af46c14b83..008502d7df 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -126,10 +126,10 @@ import Database.SQLite.Simple.QQ (sql) #endif createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User -createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime +createUserRecord db auId p activeUser = createUserRecordAt db auId False p activeUser =<< liftIO getCurrentTime -createUserRecordAt :: DB.Connection -> AgentUserId -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User -createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDescr, image, peerType, preferences = userPreferences} activeUser currentTs = +createUserRecordAt :: DB.Connection -> AgentUserId -> Bool -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User +createUserRecordAt db (AgentUserId auId) clientService Profile {displayName, fullName, shortDescr, image, peerType, preferences = userPreferences} activeUser currentTs = checkConstraint SEDuplicateName . liftIO $ do when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" let showNtfs = True @@ -139,8 +139,8 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe order <- getNextActiveOrder db DB.execute db - "INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?)" - (auId, displayName, BI activeUser, order, BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, currentTs, currentTs) + "INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, client_service, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?,?)" + ((auId, displayName, BI activeUser, order, BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, BI clientService) :. (currentTs, currentTs)) userId <- insertedRowId db DB.execute db @@ -157,7 +157,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, shortDe (profileId, displayName, userId, BI True, currentTs, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, Nothing) + pure $ toUser $ (userId, auId, contactId, profileId, BI activeUser, order) :. (displayName, fullName, shortDescr, image, Nothing, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, Nothing, Nothing, Nothing, BI clientService, Nothing) -- TODO [mentions] getUsersInfo :: DB.Connection -> IO [UserInfo] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 0358ae621d..445931e7bd 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -146,6 +146,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector -- import Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 +import Simplex.Chat.Store.SQLite.Migrations.M20251225_client_services import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -290,8 +291,9 @@ schemaMigrations = ("20250922_remove_unused_connections", m20250922_remove_unused_connections, Just down_m20250922_remove_unused_connections), ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), - ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector) - -- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2) + ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), + -- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2), + ("20251225_client_services", m20251225_client_services, Just down_m20251225_client_services) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251225_client_services.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251225_client_services.hs new file mode 100644 index 0000000000..a5f37427ff --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251225_client_services.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20251225_client_services where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20251225_client_services :: Query +m20251225_client_services = + [sql| +ALTER TABLE users ADD COLUMN client_service INTEGER NOT NULL DEFAULT 0; +|] + +down_m20251225_client_services :: Query +down_m20251225_client_services = + [sql| +ALTER TABLE users DROP COLUMN client_service; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index efbadf7c53..d1c8579e01 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -420,6 +420,16 @@ Plan: SCAN ntf_tokens_to_delete USE TEMP B-TREE FOR DISTINCT +Query: + SELECT c.service_id, c.service_queue_count, c.service_queue_ids_hash + FROM client_services c + JOIN servers s ON s.host = c.host AND s.port = c.port + WHERE c.user_id = ? AND c.host = ? AND c.port = ? AND COALESCE(c.server_key_hash, s.key_hash) = ? AND service_id IS NOT NULL + +Plan: +SEARCH s USING PRIMARY KEY (host=? AND port=?) +SEARCH c USING INDEX idx_server_certs_user_id_host_port (user_id=? AND host=? AND port=?) + Query: SELECT confirmation_id, ratchet_state, own_conn_info, sender_key, e2e_snd_pub_key, sender_conn_info, smp_reply_queues, smp_client_version FROM conn_confirmations @@ -590,11 +600,11 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_rcv_id (conn_ Query: INSERT INTO rcv_queues - ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, + ( host, port, rcv_id, rcv_service_assoc, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, queue_mode, status, to_subscribe, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash, link_id, link_key, link_priv_sig_key, link_enc_fixed_data, ntf_public_key, ntf_private_key, ntf_id, rcv_ntf_dh_secret - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); Plan: @@ -804,7 +814,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -819,7 +829,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -834,7 +844,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -849,7 +859,7 @@ SEARCH c USING PRIMARY KEY (conn_id=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -864,7 +874,7 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?) Query: SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status, c.enable_ntfs, q.client_notice_id, - q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, + q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_service_assoc, q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret, q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data FROM rcv_queues q @@ -990,6 +1000,7 @@ SEARCH snd_queues USING COVERING INDEX idx_snd_queue_id (conn_id=? AND snd_queue Query: DELETE FROM users WHERE user_id = 2 Plan: SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=?) SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_user_id (user_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_user_id (user_id=?) SEARCH rcv_files USING COVERING INDEX idx_rcv_files_user_id (user_id=?) @@ -998,6 +1009,7 @@ SEARCH connections USING COVERING INDEX idx_connections_user (user_id=?) Query: DELETE FROM users WHERE user_id = ? Plan: SEARCH users USING INTEGER PRIMARY KEY (rowid=?) +SEARCH client_services USING COVERING INDEX idx_server_certs_user_id_host_port (user_id=?) SEARCH deleted_snd_chunk_replicas USING COVERING INDEX idx_deleted_snd_chunk_replicas_user_id (user_id=?) SEARCH snd_files USING COVERING INDEX idx_snd_files_user_id (user_id=?) SEARCH rcv_files USING COVERING INDEX idx_rcv_files_user_id (user_id=?) 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 91f2ea3899..194160884b 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -5378,7 +5378,7 @@ SEARCH server_operators USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5390,7 +5390,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5403,7 +5403,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5416,7 +5416,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5430,7 +5430,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5443,7 +5443,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5456,7 +5456,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5469,7 +5469,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -5482,7 +5482,7 @@ SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?) Query: SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id @@ -6083,7 +6083,7 @@ Plan: Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, short_link_large_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) Plan: -Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?) +Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, auto_accept_member_contacts, client_service, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?,?,?) Plan: Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 34336a38ee..22452fdfaf 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -38,7 +38,8 @@ CREATE TABLE users( user_member_profile_updated_at TEXT, ui_themes TEXT, active_order INTEGER NOT NULL DEFAULT 0, - auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, -- 1 for active user + auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0, + client_service INTEGER NOT NULL DEFAULT 0, -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE RESTRICT diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 15ec3ec49d..ef08e119eb 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -532,15 +532,15 @@ userQuery :: Query userQuery = [sql| SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.short_descr, ucp.image, ucp.contact_link, ucp.chat_peer_type, ucp.preferences, - u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes + u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.auto_accept_member_contacts, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.client_service, u.ui_themes FROM users u JOIN contacts uct ON uct.contact_id = u.contact_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id |] -toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User -toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = - User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts = BoolDef autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} +toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64) :. (ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, BoolInt, Maybe UIThemeEntityOverrides) -> User +toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder) :. (displayName, fullName, shortDescr, image, contactLink, peerType, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, BI autoAcceptMemberContacts, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, BI clientService, uiThemes)) = + User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, autoAcceptMemberContacts, viewPwdHash, userMemberProfileUpdatedAt, clientService = BoolDef clientService, uiThemes} where profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, preferences = userPreferences, localAlias = ""} fullPreferences = fullPreferences' userPreferences diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index c08c0827ae..9d6e3db6e3 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -132,15 +132,17 @@ data User = User showNtfs :: Bool, sendRcptsContacts :: Bool, sendRcptsSmallGroups :: Bool, - autoAcceptMemberContacts :: BoolDef, + autoAcceptMemberContacts :: Bool, userMemberProfileUpdatedAt :: Maybe UTCTime, + clientService :: BoolDef, uiThemes :: Maybe UIThemeEntityOverrides } deriving (Show) data NewUser = NewUser { profile :: Maybe Profile, - pastTimestamp :: Bool + pastTimestamp :: Bool, + clientService :: BoolDef } deriving (Show) diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 4e1106c0d8..084d5d9f5e 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -1378,7 +1378,7 @@ groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfil viewNewMemberContactReceivedInv :: User -> Contact -> GroupInfo -> GroupMember -> [StyledString] viewNewMemberContactReceivedInv user ct@Contact {localDisplayName = c} g m - | isTrue (autoAcceptMemberContacts user) = + | autoAcceptMemberContacts user = [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] | otherwise = [ ttyGroup' g <> " " <> ttyMember m <> " requests to create direct contact with you", diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index 42d59cfa9f..03cc5aaf48 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -104,6 +104,7 @@ mkDirectoryOpts TestParams {tmpPath = ps} superUsers ownersGroup webFolder = directoryLog = Just $ ps "directory_service.log", migrateDirectoryLog = Nothing, serviceName = "SimpleX Directory", + clientService = False, runCLI = False, searchResults = 3, webFolder, diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d0b186dd94..36e6b7a0a8 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -314,7 +314,7 @@ startTestChat_ :: TestParams -> ChatDatabase -> ChatConfig -> ChatOpts -> User - startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user = do t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts - cc <- newChatController db (Just user) cfg opts False + Right cc <- newChatController db (Just user) cfg opts False void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") 0 `runReaderT` cc chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry diff --git a/tests/JSONFixtures.hs b/tests/JSONFixtures.hs index f3364dac81..f4068d3d14 100644 --- a/tests/JSONFixtures.hs +++ b/tests/JSONFixtures.hs @@ -17,10 +17,10 @@ activeUserExistsTagged :: LB.ByteString activeUserExistsTagged = "{\"error\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}" activeUserSwift :: LB.ByteString -activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}}}}" +activeUserSwift = "{\"result\":{\"_owsf\":true,\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"clientService\":false}}}}" activeUserTagged :: LB.ByteString -activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false}}}" +activeUserTagged = "{\"result\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"\",\"shortDescr\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"files\":{\"allow\":\"always\"},\"calls\":{\"allow\":\"yes\"},\"sessions\":{\"allow\":\"no\"},\"commands\":[]},\"activeUser\":true,\"activeOrder\":1,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true,\"autoAcceptMemberContacts\":false,\"clientService\":false}}}" chatStartedSwift :: LB.ByteString chatStartedSwift = "{\"result\":{\"_owsf\":true,\"chatStarted\":{}}}"