diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index cb92dc2f7..14c921a97 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -481,7 +481,8 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a } suspend fun testSMPServer(smpServer: String): SMPTestFailure? { - val r = sendCmd(CC.TestSMPServer(smpServer)) + val userId = chatModel.currentUser.value?.userId ?: run { throw Exception("testSMPServer: no current user") } + val r = sendCmd(CC.TestSMPServer(userId, smpServer)) return when (r) { is CR.SmpTestResult -> r.smpTestFailure else -> { @@ -1615,7 +1616,7 @@ sealed class CC { class APIGetGroupLink(val groupId: Long): CC() class APIGetUserSMPServers(val userId: Long): CC() class APISetUserSMPServers(val userId: Long, val smpServers: List): CC() - class TestSMPServer(val smpServer: String): CC() + class TestSMPServer(val userId: Long, val smpServer: String): CC() class APISetChatItemTTL(val userId: Long, val seconds: Long?): CC() class APIGetChatItemTTL(val userId: Long): CC() class APISetNetworkConfig(val networkConfig: NetCfg): CC() @@ -1686,7 +1687,7 @@ sealed class CC { is APIGetGroupLink -> "/_get link #$groupId" is APIGetUserSMPServers -> "/_smp $userId" is APISetUserSMPServers -> "/_smp $userId ${smpServersStr(smpServers)}" - is TestSMPServer -> "/smp test $smpServer" + is TestSMPServer -> "/smp test $userId $smpServer" is APISetChatItemTTL -> "/_ttl $userId ${chatItemTTLStr(seconds)}" is APIGetChatItemTTL -> "/_ttl $userId" is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 382237eca..a7f41b570 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -323,7 +323,8 @@ func setUserSMPServers(smpServers: [ServerCfg]) async throws { } func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> { - let r = await chatSendCmd(.testSMPServer(smpServer: smpServer)) + guard let userId = ChatModel.shared.currentUser?.userId else { throw RuntimeError("testSMPServer: no current user") } + let r = await chatSendCmd(.testSMPServer(userId: userId, smpServer: smpServer)) if case let .smpTestResult(testFailure) = r { if let t = testFailure { return .failure(t) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ad44234e7..789aaf2d9 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -48,7 +48,7 @@ public enum ChatCommand { case apiGetGroupLink(groupId: Int64) case apiGetUserSMPServers(userId: Int64) case apiSetUserSMPServers(userId: Int64, smpServers: [ServerCfg]) - case testSMPServer(smpServer: String) + case testSMPServer(userId: Int64, smpServer: String) case apiSetChatItemTTL(userId: Int64, seconds: Int64?) case apiGetChatItemTTL(userId: Int64) case apiSetNetworkConfig(networkConfig: NetCfg) @@ -132,7 +132,7 @@ public enum ChatCommand { case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" case let .apiGetUserSMPServers(userId): return "/_smp \(userId)" case let .apiSetUserSMPServers(userId, smpServers): return "/_smp \(userId) \(smpServersStr(smpServers: smpServers))" - case let .testSMPServer(smpServer): return "/smp test \(smpServer)" + case let .testSMPServer(userId, smpServer): return "/smp test \(userId) \(smpServer)" case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))" case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)" case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))" diff --git a/cabal.project b/cabal.project index cc2007892..35fa723da 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,12 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 058e3ac55e8577280267f9341ccd7d3e971bc51a + tag: 8e024590bc2b4428e64e625a9c2392908fc5912e + +source-repository-package + type: git + location: https://github.com/simplex-chat/hs-socks.git + tag: a30cc7a79a08d8108316094f8f2f82a0c5e1ac51 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 27b95fbfd..b47943f56 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,6 @@ { - "https://github.com/simplex-chat/simplexmq.git"."058e3ac55e8577280267f9341ccd7d3e971bc51a" = "1rw0j3d5higdrq5klsgnj8b8zfh08g5zv72hqcm7wkw1mmllpfrk"; + "https://github.com/simplex-chat/simplexmq.git"."8e024590bc2b4428e64e625a9c2392908fc5912e" = "0rgsf1jz2dpqbdpdfpajsi8gry47jl8jqgw13dfxr3ll9v7pr4sf"; + "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; "https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0"; "https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 93c734e06..8747829b4 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -74,6 +74,7 @@ library Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status Simplex.Chat.Migrations.M20221230_idxs Simplex.Chat.Migrations.M20230107_connections_auth_err_counter + Simplex.Chat.Migrations.M20230111_users_agent_user_id Simplex.Chat.Mobile Simplex.Chat.Options Simplex.Chat.ProfileGenerator diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2642a856e..011576dfa 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -94,7 +94,7 @@ defaultChatConfig = }, yesToMigrations = False, defaultServers = - InitialAgentServers + DefaultAgentServers { smp = _defaultSMPServers, ntf = _defaultNtfServers, netCfg = defaultNetworkConfig @@ -162,19 +162,25 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen showLiveItems <- newTVarIO False pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIsAsync, expireCIs, cleanupManagerAsync, timedItemThreads, showLiveItems} where - configServers :: InitialAgentServers + configServers :: DefaultAgentServers configServers = - let smp' = fromMaybe (smp defaultServers) (nonEmpty smpServers) + let smp' = fromMaybe (smp (defaultServers :: DefaultAgentServers)) (nonEmpty smpServers) in defaultServers {smp = smp', netCfg = networkConfig} agentServers :: ChatConfig -> IO InitialAgentServers - agentServers config@ChatConfig {defaultServers = ss@InitialAgentServers {smp}} = do - smp' <- maybe (pure smp) userServers user - pure ss {smp = smp'} + agentServers config@ChatConfig {defaultServers = DefaultAgentServers {smp, ntf, netCfg}} = do + users <- withTransaction chatStore getUsers + smp' <- case users of + [] -> pure $ M.fromList [(1, smp)] + _ -> M.fromList <$> initialServers users + pure InitialAgentServers {smp = smp', ntf, netCfg} where + initialServers :: [User] -> IO [(UserId, NonEmpty SMPServerWithAuth)] + initialServers = mapM (\u -> (aUserId u,) <$> userServers u) + userServers :: User -> IO (NonEmpty SMPServerWithAuth) userServers user' = activeAgentServers config <$> withTransaction chatStore (`getSMPServers` user') activeAgentServers :: ChatConfig -> [ServerCfg] -> NonEmpty SMPServerWithAuth -activeAgentServers ChatConfig {defaultServers = InitialAgentServers {smp}} = +activeAgentServers ChatConfig {defaultServers = DefaultAgentServers {smp}} = fromMaybe smp . nonEmpty . map (\ServerCfg {server} -> server) @@ -264,11 +270,17 @@ processChatCommand = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser p -> do u <- asks currentUser - user <- withStore $ \db -> createUser db p True + -- TODO option to choose current user servers + DefaultAgentServers {smp} <- asks $ defaultServers . config + auId <- + withStore' getUsers >>= \case + [] -> pure 1 + _ -> withAgent (`createUser` smp) + user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True atomically . writeTVar u $ Just user pure $ CRActiveUser user ListUsers -> do - users <- withStore' $ \db -> getUsers db + users <- withStore' getUsers pure $ CRUsersList users APISetActiveUser userId -> do u <- asks currentUser @@ -359,7 +371,7 @@ processChatCommand = \case (agentConnId_, fileConnReq) <- if isJust fileInline then pure (Nothing, Nothing) - else bimap Just Just <$> withAgent (\a -> createConnection a True SCMInvitation Nothing) + else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing) let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileConnReq, fileInline} withStore' $ \db -> do @@ -773,7 +785,7 @@ processChatCommand = \case pure CRNtfMessages {user, connEntity, msgTs = msgTs', ntfMessages} APIGetUserSMPServers cmdUserId -> withUser $ \user -> do checkCorrectCmdUser cmdUserId user - ChatConfig {defaultServers = InitialAgentServers {smp = defaultSMPServers}} <- asks config + ChatConfig {defaultServers = DefaultAgentServers {smp = defaultSMPServers}} <- asks config smpServers <- withStore' (`getSMPServers` user) let smpServers' = fromMaybe (L.map toServerCfg defaultSMPServers) $ nonEmpty smpServers pure $ CRUserSMPServers user smpServers' defaultSMPServers @@ -785,11 +797,13 @@ processChatCommand = \case checkCorrectCmdUser cmdUserId user withStore $ \db -> overwriteSMPServers db user smpServers cfg <- asks config - withAgent $ \a -> setSMPServers a $ activeAgentServers cfg smpServers + withAgent $ \a -> setSMPServers a (aUserId user) $ activeAgentServers cfg smpServers pure $ CRCmdOk (Just user) SetUserSMPServers smpServersConfig -> withUser $ \User {userId} -> processChatCommand $ APISetUserSMPServers userId smpServersConfig - TestSMPServer smpServer -> CRSmpTestResult <$> withAgent (`testSMPServerConnection` smpServer) + TestSMPServer cmdUserId smpServer -> withUser $ \user -> do + checkCorrectCmdUser cmdUserId user + CRSmpTestResult <$> (withAgent $ \a -> testSMPServerConnection a (aUserId user) smpServer) APISetChatItemTTL cmdUserId newTTL_ -> withUser' $ \user -> do checkCorrectCmdUser cmdUserId user checkStoreNotChanged $ @@ -921,7 +935,7 @@ processChatCommand = \case -- [incognito] generate profile for connection incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - (connId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnNew incognitoProfile toView $ CRNewContactConnection user conn pure $ CRInvitation user cReq @@ -933,7 +947,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing - connId <- withAgent $ \a -> joinConnection a True cReq . directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq . directMessage $ XInfo profileToSend conn <- withStore' $ \db -> createDirectConnection db userId connId cReq ConnJoined $ incognitoProfile $> profileToSend toView $ CRNewContactConnection user conn pure $ CRSentConfirmation user @@ -957,7 +971,7 @@ processChatCommand = \case processChatCommand $ APIListContacts userId APICreateMyAddress cmdUserId -> withUser $ \user@User {userId} -> withChatLock "createMyAddress" . procCmd $ do checkCorrectCmdUser cmdUserId user - (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact Nothing + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing withStore $ \db -> createUserContactLink db userId connId cReq pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> @@ -1047,7 +1061,7 @@ processChatCommand = \case case contactMember contact members of Nothing -> do gVar <- asks idsDrg - (agentConnId, cReq) <- withAgent $ \a -> createConnection a True SCMInvitation Nothing + (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -1063,7 +1077,7 @@ processChatCommand = \case APIJoinGroup groupId -> withUser $ \user@User {userId} -> do ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId withChatLock "joinGroup" . procCmd $ do - agentConnId <- withAgent $ \a -> joinConnection a True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) + agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) withStore' $ \db -> do createMemberConnection db userId fromMember agentConnId updateGroupMemberStatus db userId fromMember GSMemAccepted @@ -1180,7 +1194,7 @@ processChatCommand = \case unless (memberActive membership) $ throwChatError CEGroupMemberNotActive groupLinkId <- GroupLinkId <$> (asks idsDrg >>= liftIO . (`randomBytes` 16)) let crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, cReq) <- withAgent $ \a -> createConnection a True SCMContact $ Just crClientData + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact $ Just crClientData withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId pure $ CRGroupLinkCreated user gInfo cReq APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do @@ -1388,7 +1402,7 @@ processChatCommand = \case incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing - connId <- withAgent $ \a -> joinConnection a True cReq $ directMessage (XContact profileToSend $ Just xContactId) + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId) let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId toView $ CRNewContactConnection user conn @@ -3563,13 +3577,13 @@ markGroupCIDeleted user gInfo ci@(CChatItem msgDir deletedItem) msgId byUser = d createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId) createAgentConnectionAsync user cmdFunction enableNtfs cMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aCorrId cmdId) enableNtfs cMode + connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode pure (cmdId, connId) joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> m (CommandId, ConnId) joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aCorrId cmdId) enableNtfs cReqUri cInfo + connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo pure (cmdId, connId) allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () @@ -3684,7 +3698,7 @@ getCreateActiveUser st = do loop = do displayName <- getContactName fullName <- T.pack <$> getWithPrompt "full name (optional)" - withTransaction st (\db -> runExceptT $ createUser db Profile {displayName, fullName, image = Nothing, preferences = Nothing} True) >>= \case + withTransaction st (\db -> runExceptT $ createUserRecord db (AgentUserId 1) Profile {displayName, fullName, image = Nothing, preferences = Nothing} True) >>= \case Left SEDuplicateName -> do putStrLn "chosen display name is already used by another profile on this device, choose another one" loop @@ -3848,7 +3862,7 @@ chatCommandP = "/smp_servers " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP), "/smp_servers" $> GetUserSMPServers, "/smp default" $> SetUserSMPServers (SMPServersConfig []), - "/smp test " *> (TestSMPServer <$> strP), + "/smp test " *> (TestSMPServer <$> A.decimal <* A.space <*> strP), "/_smp " *> (APISetUserSMPServers <$> A.decimal <* A.space <*> jsonP), "/smp " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP), "/_smp " *> (APIGetUserSMPServers <$> A.decimal), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 024b5d62a..fae79d8ca 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -43,7 +43,7 @@ import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink) import Simplex.Chat.Types import Simplex.Messaging.Agent (AgentClient) import Simplex.Messaging.Agent.Client (AgentLocks, SMPTestFailure) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, InitialAgentServers, NetworkConfig) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore) @@ -51,7 +51,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags) +import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags, NtfServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport.Client (TransportHost) import System.IO (Handle) @@ -70,7 +70,7 @@ updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-c data ChatConfig = ChatConfig { agentConfig :: AgentConfig, yesToMigrations :: Bool, - defaultServers :: InitialAgentServers, + defaultServers :: DefaultAgentServers, tbqSize :: Natural, fileChunkSize :: Integer, inlineFiles :: InlineFilesConfig, @@ -80,6 +80,12 @@ data ChatConfig = ChatConfig testView :: Bool } +data DefaultAgentServers = DefaultAgentServers + { smp :: NonEmpty SMPServerWithAuth, + ntf :: [NtfServer], + netCfg :: NetworkConfig + } + data InlineFilesConfig = InlineFilesConfig { offerChunks :: Integer, sendChunks :: Integer, @@ -203,7 +209,7 @@ data ChatCommand | GetUserSMPServers | APISetUserSMPServers UserId SMPServersConfig | SetUserSMPServers SMPServersConfig - | TestSMPServer SMPServerWithAuth + | TestSMPServer UserId SMPServerWithAuth | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId diff --git a/src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs b/src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs new file mode 100644 index 000000000..531c776a3 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230111_users_agent_user_id.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230111_users_agent_user_id where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230111_users_agent_user_id :: Query +m20230111_users_agent_user_id = + [sql| +PRAGMA ignore_check_constraints=ON; + +ALTER TABLE users ADD COLUMN agent_user_id INTEGER CHECK (agent_user_id NOT NULL); +UPDATE users SET agent_user_id = 1; + +PRAGMA ignore_check_constraints=OFF; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 84059e9eb..dec698276 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -29,7 +29,8 @@ CREATE TABLE users( local_display_name TEXT NOT NULL UNIQUE, active_user INTEGER NOT NULL DEFAULT 0, created_at TEXT CHECK(created_at NOT NULL), - updated_at TEXT CHECK(updated_at NOT NULL), -- 1 for active user + updated_at TEXT CHECK(updated_at NOT NULL), + agent_user_id INTEGER CHECK(agent_user_id NOT NULL), -- 1 for active user FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index c415f3b94..f690de6fb 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -25,7 +25,7 @@ module Simplex.Chat.Store createChatStore, chatStoreFile, agentStoreFile, - createUser, + createUserRecord, getUsers, setActiveUser, getSetActiveUser, @@ -330,6 +330,7 @@ import Simplex.Chat.Migrations.M20221222_chat_ts import Simplex.Chat.Migrations.M20221223_idx_chat_items_item_status import Simplex.Chat.Migrations.M20221230_idxs import Simplex.Chat.Migrations.M20230107_connections_auth_err_counter +import Simplex.Chat.Migrations.M20230111_users_agent_user_id import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) @@ -390,7 +391,8 @@ schemaMigrations = ("20221222_chat_ts", m20221222_chat_ts), ("20221223_idx_chat_items_item_status", m20221223_idx_chat_items_item_status), ("20221230_idxs", m20221230_idxs), - ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter) + ("20230107_connections_auth_err_counter", m20230107_connections_auth_err_counter), + ("20230111_users_agent_user_id", m20230111_users_agent_user_id) ] -- | The list of migrations in ascending order by date @@ -419,15 +421,15 @@ handleSQLError err e insertedRowId :: DB.Connection -> IO Int64 insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" -createUser :: DB.Connection -> Profile -> Bool -> ExceptT StoreError IO User -createUser db Profile {displayName, fullName, image, preferences = userPreferences} activeUser = +createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User +createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser = checkConstraint SEDuplicateName . liftIO $ do currentTs <- getCurrentTime when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" DB.execute db - "INSERT INTO users (local_display_name, active_user, contact_id, created_at, updated_at) VALUES (?,?,0,?,?)" - (displayName, activeUser, currentTs, currentTs) + "INSERT INTO users (agent_user_id, local_display_name, active_user, contact_id, created_at, updated_at) VALUES (?,?,?,0,?,?)" + (auId, displayName, activeUser, currentTs, currentTs) userId <- insertedRowId db DB.execute db @@ -444,7 +446,7 @@ createUser db Profile {displayName, fullName, image, preferences = userPreferenc (profileId, displayName, userId, True, currentTs, currentTs) contactId <- insertedRowId db DB.execute db "UPDATE users SET contact_id = ? WHERE user_id = ?" (contactId, userId) - pure $ toUser (userId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences) + pure $ toUser (userId, auId, contactId, profileId, activeUser, displayName, fullName, image, userPreferences) getUsers :: DB.Connection -> IO [User] getUsers db = @@ -453,16 +455,16 @@ getUsers db = userQuery :: Query userQuery = [sql| - SELECT u.user_id, u.contact_id, cp.contact_profile_id, u.active_user, u.local_display_name, cp.full_name, cp.image, cp.preferences + SELECT u.user_id, u.agent_user_id, u.contact_id, cp.contact_profile_id, u.active_user, u.local_display_name, cp.full_name, cp.image, cp.preferences FROM users u JOIN contacts ct ON ct.contact_id = u.contact_id JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id |] -toUser :: (UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User -toUser (userId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) = +toUser :: (UserId, UserId, ContactId, ProfileId, Bool, ContactName, Text, Maybe ImageData, Maybe Preferences) -> User +toUser (userId, auId, userContactId, profileId, activeUser, displayName, fullName, image, userPreferences) = let profile = LocalProfile {profileId, displayName, fullName, image, preferences = userPreferences, localAlias = ""} - in User {userId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences = mergePreferences Nothing userPreferences} + in User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, fullPreferences = mergePreferences Nothing userPreferences} setActiveUser :: DB.Connection -> UserId -> IO () setActiveUser db userId = do diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index dd777ff79..f7cac3753 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -17,7 +17,6 @@ import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Notification import Simplex.Chat.Terminal.Output -import Simplex.Messaging.Agent.Env.SQLite (InitialAgentServers (..)) import Simplex.Messaging.Client (defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.Exit (exitFailure) @@ -26,7 +25,7 @@ terminalChatConfig :: ChatConfig terminalChatConfig = defaultChatConfig { defaultServers = - InitialAgentServers + DefaultAgentServers { smp = L.fromList [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index da9e0411f..55d26561f 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -80,8 +80,31 @@ instance IsContact Contact where preferences' Contact {profile = LocalProfile {preferences}} = preferences {-# INLINE preferences' #-} +newtype AgentUserId = AgentUserId UserId + deriving (Eq, Show) + +instance StrEncoding AgentUserId where + strEncode (AgentUserId uId) = strEncode uId + strDecode s = AgentUserId <$> strDecode s + strP = AgentUserId <$> strP + +instance FromJSON AgentUserId where + parseJSON = strParseJSON "AgentUserId" + +instance ToJSON AgentUserId where + toJSON = strToJSON + toEncoding = strToJEncoding + +instance FromField AgentUserId where fromField f = AgentUserId <$> fromField f + +instance ToField AgentUserId where toField (AgentUserId uId) = toField uId + +aUserId :: User -> UserId +aUserId User {agentUserId = AgentUserId uId} = uId + data User = User { userId :: UserId, + agentUserId :: AgentUserId, userContactId :: ContactId, localDisplayName :: ContactName, profile :: LocalProfile, @@ -92,7 +115,7 @@ data User = User instance ToJSON User where toEncoding = J.genericToEncoding J.defaultOptions -type UserId = ContactId +type UserId = Int64 type ContactId = Int64 diff --git a/stack.yaml b/stack.yaml index 87b0fe517..1558136b1 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 058e3ac55e8577280267f9341ccd7d3e971bc51a + commit: 8e024590bc2b4428e64e625a9c2392908fc5912e # - ../direct-sqlcipher - github: simplex-chat/direct-sqlcipher commit: 34309410eb2069b029b8fc1872deb1e0db123294 diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 15674bfef..75b0ddf0d 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -25,7 +25,7 @@ import Simplex.Chat.Options import Simplex.Chat.Store import Simplex.Chat.Terminal import Simplex.Chat.Terminal.Output (newChatTerminal) -import Simplex.Chat.Types (Profile, User (..)) +import Simplex.Chat.Types (AgentUserId (..), Profile, User (..)) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) @@ -108,7 +108,7 @@ testCfgV1 = testCfg {agentConfig = testAgentCfgV1} createTestChat :: ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat cfg opts@ChatOpts {dbKey} dbPrefix profile = do db@ChatDatabase {chatStore} <- createChatDatabase (testDBPrefix <> dbPrefix) dbKey False - Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUser db' profile True + Right user <- withTransaction chatStore $ \db' -> runExceptT $ createUserRecord db' (AgentUserId 1) profile True startTestChat_ db cfg opts user startTestChat :: ChatConfig -> ChatOpts -> String -> IO TestCC diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 580b20696..b0b42864f 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -3800,14 +3800,14 @@ testTestSMPServerConnection :: IO () testTestSMPServerConnection = testChat2 aliceProfile bobProfile $ \alice _ -> do - alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001" + alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:5001" alice <## "SMP server test passed" -- to test with password: -- alice <## "SMP server test failed at CreateQueue, error: SMP AUTH" -- alice <## "Server requires authorization to create queues, check password" - alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001" + alice ##> "/smp test 1 smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001" alice <## "SMP server test passed" - alice ##> "/smp test smp://LcJU@localhost:5001" + alice ##> "/smp test 1 smp://LcJU@localhost:5001" alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:5001 NETWORK" alice <## "Possibly, certificate fingerprint in server address is incorrect" diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index ada96f42b..dc41be904 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -7,7 +7,7 @@ import ChatTests import Control.Monad.Except import Simplex.Chat.Mobile import Simplex.Chat.Store -import Simplex.Chat.Types (Profile (..)) +import Simplex.Chat.Types (AgentUserId (..), Profile (..)) import Test.Hspec mobileTests :: Spec @@ -32,9 +32,9 @@ activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\" activeUser :: String #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" +activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" #else -activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" +activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"no\"},\"fullDelete\":{\"allow\":\"no\"},\"voice\":{\"allow\":\"yes\"}},\"activeUser\":true}}}" #endif chatStarted :: String @@ -93,7 +93,7 @@ testChatApi = withTmpFiles $ do let dbPrefix = testDBPrefix <> "1" f = chatStoreFile dbPrefix st <- createChatStore f "myKey" True - Right _ <- withTransaction st $ \db -> runExceptT $ createUser db aliceProfile {preferences = Nothing} True + Right _ <- withTransaction st $ \db -> runExceptT $ createUserRecord db (AgentUserId 1) aliceProfile {preferences = Nothing} True Right cc <- chatMigrateInit dbPrefix "myKey" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "" Left (DBMErrorNotADatabase _) <- chatMigrateInit dbPrefix "anotherKey"