From 37b78edb91f60a9f78d727c5e78a49c9a4885f41 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:18:26 +0400 Subject: [PATCH 01/22] ios: move Network and servers settings modules to folder (#5110) --- .../AdvancedNetworkSettings.swift | 0 .../NetworkAndServers.swift | 0 .../ProtocolServerView.swift | 0 .../ProtocolServersView.swift | 0 .../ScanProtocolServer.swift | 0 apps/ios/SimpleX.xcodeproj/project.pbxproj | 18 +++++++++++++----- 6 files changed, 13 insertions(+), 5 deletions(-) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/AdvancedNetworkSettings.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/NetworkAndServers.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ProtocolServerView.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ProtocolServersView.swift (100%) rename apps/ios/Shared/Views/UserSettings/{ => NetworkAndServers}/ScanProtocolServer.swift (100%) diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServerView.swift diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ProtocolServersView.swift diff --git a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift similarity index 100% rename from apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift rename to apps/ios/Shared/Views/UserSettings/NetworkAndServers/ScanProtocolServer.swift diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2b1160061c..7aaa439adb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -912,10 +912,9 @@ 5CB924DF27A8678B00ACCCDD /* UserSettings */ = { isa = PBXGroup; children = ( + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */, 5CB924D627A8563F00ACCCDD /* SettingsView.swift */, 5CB346E62868D76D001FD2EF /* NotificationsView.swift */, - 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, - 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, 5CADE79929211BB900072E13 /* PreferencesView.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, @@ -923,9 +922,6 @@ 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, - 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, - 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, - 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */, @@ -1056,6 +1052,18 @@ path = Database; sourceTree = ""; }; + 643B3B4C2CCFD34B0083A2CF /* NetworkAndServers */ = { + isa = PBXGroup; + children = ( + 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, + 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, + 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 5C9C2DA6289957AE00CC63B1 /* AdvancedNetworkSettings.swift */, + 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */, + ); + path = NetworkAndServers; + sourceTree = ""; + }; 6440CA01288AEC770062C672 /* Group */ = { isa = PBXGroup; children = ( From 97df069730e2d63b3eb7b644b127a7e3cc7b03ed Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 4 Nov 2024 13:28:57 +0000 Subject: [PATCH 02/22] core: add support for server operators (#4961) * core: add support for server operators * migration * update schema and queries, rfc * add usage conditions tables * core: server operators new apis draft * update * conditions * update * add get conditions api * add get conditions API * WIP * compiles * fix schema * core: ui logic in types (#5139) * update --------- Co-authored-by: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> --- cabal.project | 2 +- docs/rfcs/2024-10-27-server-operators.md | 24 ++++ package.yaml | 1 + scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 10 ++ src/Simplex/Chat.hs | 50 +++++++- src/Simplex/Chat/Controller.hs | 25 +++- .../Migrations/M20241027_server_operators.hs | 70 +++++++++++ src/Simplex/Chat/Migrations/chat_schema.sql | 39 +++++++ src/Simplex/Chat/Operators.hs | 110 ++++++++++++++++++ src/Simplex/Chat/Operators/Conditions.hs | 19 +++ src/Simplex/Chat/Store/Migrations.hs | 4 +- src/Simplex/Chat/Store/Profiles.hs | 77 ++++++++++-- src/Simplex/Chat/Terminal.hs | 8 +- src/Simplex/Chat/View.hs | 24 ++-- tests/ChatClient.hs | 7 +- tests/RandomServers.hs | 4 +- 17 files changed, 440 insertions(+), 36 deletions(-) create mode 100644 docs/rfcs/2024-10-27-server-operators.md create mode 100644 src/Simplex/Chat/Migrations/M20241027_server_operators.hs create mode 100644 src/Simplex/Chat/Operators.hs create mode 100644 src/Simplex/Chat/Operators/Conditions.hs diff --git a/cabal.project b/cabal.project index c9b8b11722..61ce04a569 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: ffecf200d4874dfa34f6d15b269964c0115a54ca + tag: ff05a465ee15ac7ae2c14a9fb703a18564950631 source-repository-package type: git diff --git a/docs/rfcs/2024-10-27-server-operators.md b/docs/rfcs/2024-10-27-server-operators.md new file mode 100644 index 0000000000..5456d28f08 --- /dev/null +++ b/docs/rfcs/2024-10-27-server-operators.md @@ -0,0 +1,24 @@ +# Server operators + +## Problem + +All preconfigured servers operated by a single company create a risk that user connections can be analysed by aggregating transport information from these servers. + +The solution is to have more than one operator servers pre-configured in the app. + +For operators to be protected from any violations of rights of other users or third parties by the users who use servers of these operators, the users have to explicitely accept conditions of use with the operator, in the same way they accept conditions of use with SimpleX Chat Ltd by downloading the app. + +## Solution + +Allow to assign operators to servers, both with preconfigured operators and servers, and with user-defined operators. Agent added support for server roles, chat app could: +- allow assigning server roles only on the operator level. +- only on server level. +- on both, with server roles overriding operator roles (that would require a different type for server for chat app). + +For simplicity of both UX and logic it is probably better to allow assigning roles only on operators' level, and servers without set operators can be used for both roles. + +For agreements, it is sufficient to record the signatures of these agreements on users' devices, together with the copy of signed agreement (or its hash and version) in a separate table. The terms themselves could be: +- included in the app - either in code or in migration. +- referenced with a stable link to a particular commit. + +The first solution seems better, as it avoids any third party dependency, and the agreement size is relatively small (~31kb), to reduce size we can store it compressed. diff --git a/package.yaml b/package.yaml index 94dc13ad2e..2fc50a3532 100644 --- a/package.yaml +++ b/package.yaml @@ -29,6 +29,7 @@ dependencies: - email-validate == 2.3.* - exceptions == 0.10.* - filepath == 1.4.* + - file-embed == 0.0.15.* - http-types == 0.12.* - http2 >= 4.2.2 && < 4.3 - memory == 0.18.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 8de91675e3..3e0f103641 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ffecf200d4874dfa34f6d15b269964c0115a54ca" = "0kb8hq37fc5g198wq7dswnlwjzk67q8rrzil2dii5lc6xfr47jbs"; + "https://github.com/simplex-chat/simplexmq.git"."ff05a465ee15ac7ae2c14a9fb703a18564950631" = "1gv4nwqzbqkj7y3ffkiwkr4qwv52vdzppsds5vsfqaayl14rzmgp"; "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 96d16f5004..c7d603457c 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -150,10 +150,13 @@ library Simplex.Chat.Migrations.M20240920_user_order Simplex.Chat.Migrations.M20241008_indexes Simplex.Chat.Migrations.M20241010_contact_requests_contact_id + Simplex.Chat.Migrations.M20241027_server_operators Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.WebRTC + Simplex.Chat.Operators + Simplex.Chat.Operators.Conditions Simplex.Chat.Options Simplex.Chat.ProfileGenerator Simplex.Chat.Protocol @@ -213,6 +216,7 @@ library , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -276,6 +280,7 @@ executable simplex-bot , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -340,6 +345,7 @@ executable simplex-bot-advanced , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -407,6 +413,7 @@ executable simplex-broadcast-bot , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -472,6 +479,7 @@ executable simplex-chat , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -543,6 +551,7 @@ executable simplex-directory-service , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , http-types ==0.12.* , http2 >=4.2.2 && <4.3 @@ -642,6 +651,7 @@ test-suite simplex-chat-test , directory ==1.3.* , email-validate ==2.3.* , exceptions ==0.10.* + , file-embed ==0.0.15.* , filepath ==1.4.* , generic-random ==1.5.* , http-types ==0.12.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 885d4303c8..380f6c5d24 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -67,6 +67,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.Batch (MsgBatch (..), batchMessages) import Simplex.Chat.Messages.CIContent import Simplex.Chat.Messages.CIContent.Events +import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol @@ -97,7 +98,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -152,7 +153,7 @@ defaultChatConfig = { smp = _defaultSMPServers, useSMP = 4, ntf = _defaultNtfServers, - xftp = L.map (presetServerCfg True) defaultXFTPServers, + xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig }, @@ -181,7 +182,7 @@ _defaultSMPServers :: NonEmpty (ServerCfg 'PSMP) _defaultSMPServers = L.fromList $ map - (presetServerCfg True) + (presetServerCfg True allRoles operatorSimpleXChat) [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", @@ -195,12 +196,15 @@ _defaultSMPServers = "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" ] <> map - (presetServerCfg False) + (presetServerCfg False allRoles operatorSimpleXChat) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] +operatorSimpleXChat :: Maybe OperatorId +operatorSimpleXChat = Just 1 + _defaultNtfServers :: [NtfServer] _defaultNtfServers = [ "ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,5ex3mupcazy3zlky64ab27phjhijpemsiby33qzq3pliejipbtx5xgad.onion" @@ -1484,8 +1488,11 @@ processChatCommand' vr = \case pure $ CRConnNtfMessages ntfMsgs APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config - servers <- withFastStore' (`getProtocolServers` user) - pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p (useServers cfg p servers) (cfgServers p defaultServers) + srvs <- withFastStore' (`getProtocolServers` user) + ts <- liftIO getCurrentTime + operators <- withFastStore' $ \db -> getServerOperators db ts + let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) + pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) @@ -1501,6 +1508,37 @@ processChatCommand' vr = \case lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv + APIGetServerOperators -> pure $ chatCmdError Nothing "not supported" + APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" + APIGetUserServers userId -> withUserId userId $ \user -> + pure $ chatCmdError (Just user) "not supported" + APISetUserServers userId _userServers -> withUserId userId $ \user -> + pure $ chatCmdError (Just user) "not supported" + APIValidateServers _userServers -> + -- response is CRUserServersValidation + pure $ chatCmdError Nothing "not supported" + APIGetUsageConditions -> do + -- TODO + -- get current conditions + -- get latest accepted conditions (from operators) + ts <- liftIO getCurrentTime + let usageConditions = + UsageConditions + { conditionsId = 1, + conditionsCommit = "abc", + notifiedAt = Nothing, + createdAt = ts + } + pure + CRUsageConditions + { usageConditions = usageConditions, + conditionsText = usageConditionsText, + acceptedConditions = Nothing + } + APISetConditionsNotified _conditionsId -> do + pure $ chatCmdError Nothing "not supported" + APIAcceptConditions _conditionsId _opIds -> + pure $ chatCmdError Nothing "not supported" APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b39b4d7456..bd2cee3e50 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -57,6 +57,7 @@ import Simplex.Chat.Call import Simplex.Chat.Markdown (MarkdownList) import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types @@ -70,7 +71,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, OperatorId, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -352,6 +353,14 @@ data ChatCommand | SetUserProtoServers AProtoServersConfig | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth + | APIGetServerOperators + | APISetServerOperators (NonEmpty (OperatorId, Bool)) + | APIGetUserServers UserId + | APISetUserServers UserId (NonEmpty UserServers) + | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation + | APIGetUsageConditions + | APISetConditionsNotified Int64 + | APIAcceptConditions Int64 (NonEmpty OperatorId) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -577,8 +586,12 @@ data ChatResponse | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserProtoServers {user :: User, servers :: AUserProtoServers} + | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} + | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} + | CRUserServers {userServers :: [UserServers]} + | CRUserServersValidation {serverErrors :: [UserServersError]} + | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} @@ -948,6 +961,12 @@ data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) ( deriving instance Show AProtoServersConfig +data UserServersError + = USEStorageMissing + | USEProxyMissing + | USEDuplicate {server :: AProtoServerWithAuth} + deriving (Show) + data UserProtoServers p = UserProtoServers { serverProtocol :: SProtocolType p, protoServers :: NonEmpty (ServerCfg p), @@ -1526,6 +1545,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) + $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''ContactSubStatus) diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs new file mode 100644 index 0000000000..bc9f40bddf --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20241027_server_operators where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20241027_server_operators :: Query +m20241027_server_operators = + [sql| +CREATE TABLE server_operators ( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + role_storage INTEGER NOT NULL DEFAULT 1, + role_proxy INTEGER NOT NULL DEFAULT 1, + accepted_conditions_commit TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +ALTER TABLE protocol_servers ADD COLUMN server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL; + +CREATE TABLE usage_conditions ( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE operator_usage_conditions ( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers(server_operator_id); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); + +INSERT INTO server_operators + (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) + VALUES (1, 'simplex', 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); +INSERT INTO server_operators + (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) + VALUES (2, 'xyz', 'XYZ', 'XYZ Ltd', 'xyz.com', 0); + +-- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; +|] + +down_m20241027_server_operators :: Query +down_m20241027_server_operators = + [sql| +DROP INDEX idx_operator_usage_conditions_conditions_commit; +DROP INDEX idx_operator_usage_conditions_server_operator_id; +DROP INDEX idx_protocol_servers_server_operator_id; + +ALTER TABLE protocol_servers DROP COLUMN server_operator_id; + +DROP TABLE operator_usage_conditions; +DROP TABLE usage_conditions; +DROP TABLE server_operators; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 2619a5c4e5..07c363eda9 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -450,6 +450,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', + server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL, UNIQUE(user_id, host, port) ); CREATE TABLE xftp_file_descriptions( @@ -589,6 +590,34 @@ CREATE TABLE note_folders( unread_chat INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE app_settings(app_settings TEXT NOT NULL); +CREATE TABLE server_operators( + server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_tag TEXT, + trade_name TEXT NOT NULL, + legal_name TEXT, + server_domains TEXT, + enabled INTEGER NOT NULL DEFAULT 1, + role_storage INTEGER NOT NULL DEFAULT 1, + role_proxy INTEGER NOT NULL DEFAULT 1, + accepted_conditions_commit TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE usage_conditions( + usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + conditions_commit TEXT NOT NULL UNIQUE, + notified_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE operator_usage_conditions( + operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, + server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, + server_operator_tag TEXT, + conditions_commit TEXT NOT NULL, + accepted_at TEXT, + created_at TEXT NOT NULL DEFAULT(datetime('now')) +); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -890,3 +919,13 @@ CREATE INDEX idx_received_probes_group_member_id on received_probes( group_member_id ); CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); +CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers( + server_operator_id +); +CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( + server_operator_id +); +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( + server_operator_id, + conditions_commit +); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs new file mode 100644 index 0000000000..9a2dac0b1b --- /dev/null +++ b/src/Simplex/Chat/Operators.hs @@ -0,0 +1,110 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TemplateHaskell #-} + +module Simplex.Chat.Operators where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE +import qualified Data.Aeson.TH as JQ +import Data.FileEmbed +import Data.Int (Int64) +import Data.List.NonEmpty (NonEmpty) +import Data.Text (Text) +import Data.Time.Clock (UTCTime) +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Language.Haskell.TH.Syntax (lift) +import Simplex.Chat.Operators.Conditions +import Simplex.Chat.Types.Util (textParseJSON) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerRoles) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Util (safeDecodeUtf8) + +usageConditionsCommit :: Text +usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" + +usageConditionsText :: Text +usageConditionsText = + $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") + in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] + ) + +data OperatorTag = OTSimplex | OTXyz + deriving (Show) + +instance FromField OperatorTag where fromField = fromTextField_ textDecode + +instance ToField OperatorTag where toField = toField . textEncode + +instance FromJSON OperatorTag where + parseJSON = textParseJSON "OperatorTag" + +instance ToJSON OperatorTag where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance TextEncoding OperatorTag where + textDecode = \case + "simplex" -> Just OTSimplex + "xyz" -> Just OTXyz + _ -> Nothing + textEncode = \case + OTSimplex -> "simplex" + OTXyz -> "xyz" + +data UsageConditions = UsageConditions + { conditionsId :: Int64, + conditionsCommit :: Text, + notifiedAt :: Maybe UTCTime, + createdAt :: UTCTime + } + deriving (Show) + +data UsageConditionsAction + = UCAReview {operators :: [ServerOperator], deadline :: Maybe UTCTime, showNotice :: Bool} + | UCAAccepted {operators :: [ServerOperator]} + deriving (Show) + +-- TODO UI logic +usageConditionsAction :: UsageConditionsAction +usageConditionsAction = UCAAccepted [] + +data ConditionsAcceptance + = CAAccepted {acceptedAt :: UTCTime} + | CARequired {deadline :: Maybe UTCTime} + deriving (Show) + +data ServerOperator = ServerOperator + { operatorId :: OperatorId, + operatorTag :: Maybe OperatorTag, + tradeName :: Text, + legalName :: Maybe Text, + serverDomains :: [Text], + acceptedConditions :: ConditionsAcceptance, + enabled :: Bool, + roles :: ServerRoles + } + deriving (Show) + +data UserServers = UserServers + { operator :: ServerOperator, + smpServers :: NonEmpty (ProtoServerWithAuth 'PSMP), + xftpServers :: NonEmpty (ProtoServerWithAuth 'PXFTP) + } + deriving (Show) + +$(JQ.deriveJSON defaultJSON ''UsageConditions) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) + +$(JQ.deriveJSON defaultJSON ''ServerOperator) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) + +$(JQ.deriveJSON defaultJSON ''UserServers) diff --git a/src/Simplex/Chat/Operators/Conditions.hs b/src/Simplex/Chat/Operators/Conditions.hs new file mode 100644 index 0000000000..55cf8b658d --- /dev/null +++ b/src/Simplex/Chat/Operators/Conditions.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Operators.Conditions where + +import Data.Char (isSpace) +import Data.Text (Text) +import qualified Data.Text as T + +stripFrontMatter :: Text -> Text +stripFrontMatter = + T.unlines + . dropWhile ("# " `T.isPrefixOf`) -- strip title + . dropWhile (T.all isSpace) + . dropWhile fm + . (\ls -> let ls' = dropWhile (not . fm) ls in if null ls' then ls else ls') + . dropWhile fm + . T.lines + where + fm = ("---" `T.isPrefixOf`) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index e2d12e78d7..e33f2336cc 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -114,6 +114,7 @@ import Simplex.Chat.Migrations.M20240827_calls_uuid import Simplex.Chat.Migrations.M20240920_user_order import Simplex.Chat.Migrations.M20241008_indexes import Simplex.Chat.Migrations.M20241010_contact_requests_contact_id +import Simplex.Chat.Migrations.M20241027_server_operators import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -227,7 +228,8 @@ schemaMigrations = ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid), ("20240920_user_order", m20240920_user_order, Just down_m20240920_user_order), ("20241008_indexes", m20241008_indexes, Just down_m20241008_indexes), - ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id) + ("20241010_contact_requests_contact_id", m20241010_contact_requests_contact_id, Just down_m20241010_contact_requests_contact_id), + ("20241027_server_operators", m20241027_server_operators, Just down_m20241027_server_operators) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index fb9774a54e..fe2cc737fb 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -47,7 +47,9 @@ module Simplex.Chat.Store.Profiles getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, + -- overwriteOperatorsAndServers, overwriteProtocolServers, + getServerOperators, createCall, deleteCalls, getCalls, @@ -76,6 +78,7 @@ import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call import Simplex.Chat.Messages +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared @@ -83,7 +86,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -521,20 +524,25 @@ getProtocolServers db User {userId} = <$> DB.query db [sql| - SELECT host, port, key_hash, basic_auth, preset, tested, enabled - FROM protocol_servers - WHERE user_id = ? AND protocol = ?; + SELECT s.host, s.port, s.key_hash, s.basic_auth, s.server_operator_id, s.preset, s.tested, s.enabled, o.role_storage, o.role_proxy + FROM protocol_servers s + LEFT JOIN server_operators o USING (server_operator_id) + WHERE s.user_id = ? AND s.protocol = ? |] (userId, decodeLatin1 $ strEncode protocol) where protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) = + toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Maybe OperatorId, Bool, Maybe Bool, Bool, Maybe Bool, Maybe Bool) -> ServerCfg p + toServerCfg (host, port, keyHash, auth_, operator, preset, tested, enabled, storage_, proxy_) = let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - in ServerCfg {server, preset, tested, enabled} + roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} + in ServerCfg {server, operator, preset, tested, enabled, roles} -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +-- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] +-- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () overwriteProtocolServers db User {userId} servers = + -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) @@ -549,9 +557,62 @@ overwriteProtocolServers db User {userId} servers = |] ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () + -- Right <$> getProtocolServers db user where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p +getServerOperators :: DB.Connection -> UTCTime -> IO [ServerOperator] +getServerOperators db ts = + map toOperator + <$> DB.query_ + db + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy + FROM server_operators; + |] + where + -- TODO get conditions state + toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = + let roles = ServerRoles {storage, proxy} + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions = CAAccepted ts, enabled, roles} + +-- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] +-- updateServerOperators_ db operators = do +-- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" +-- let (existing, new) = partition (isJust . operatorId) operators +-- existing' <- mapM (\op -> upsertExisting op $> op) existing +-- new' <- mapM insertNew new +-- pure $ existing' <> new' +-- where +-- upsertExisting ServerOperator {operatorId, name, preset, enabled, roles = ServerRoles {storage, proxy}} +-- | preset = +-- DB.execute +-- db +-- [sql| +-- UPDATE server_operators +-- SET enabled = ?, role_storage = ?, role_proxy = ? +-- WHERE server_operator_id = ? +-- |] +-- (enabled, storage, proxy, operatorId) +-- | otherwise = +-- DB.execute +-- db +-- [sql| +-- INSERT INTO server_operators (server_operator_id, name, preset, enabled, role_storage, role_proxy) +-- VALUES (?,?,?,?,?,?) +-- |] +-- (operatorId, name, preset, enabled, storage, proxy) +-- insertNew op@ServerOperator {name, preset, enabled, roles = ServerRoles {storage, proxy}} = do +-- DB.execute +-- db +-- [sql| +-- INSERT INTO server_operators (name, preset, enabled, role_storage, role_proxy) +-- VALUES (?,?,?,?,?) +-- |] +-- (name, preset, enabled, storage, proxy) +-- opId <- insertedRowId db +-- pure op {operatorId = Just opId} + createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do currentTs <- getCurrentTime diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 5cc695db04..e38a34d45f 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -13,7 +13,7 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB -import Simplex.Chat (defaultChatConfig) +import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) @@ -21,7 +21,7 @@ import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Agent.Env.SQLite (presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (allRoles, presetServerCfg) import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.IO (hFlush, hSetEcho, stdin, stdout) @@ -34,14 +34,14 @@ terminalChatConfig = { smp = L.fromList $ map - (presetServerCfg True) + (presetServerCfg True allRoles operatorSimpleXChat) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ], useSMP = 3, ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = L.map (presetServerCfg True) defaultXFTPServers, + xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index ade36476c7..c53e5a2749 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -19,7 +19,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (groupBy, intercalate, intersperse, partition, sortOn) +import Data.List (foldl', groupBy, intercalate, intersperse, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -42,6 +42,7 @@ import Simplex.Chat.Help import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types @@ -95,8 +96,12 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChats chats -> viewChats ts tz chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] - CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView + CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure + CRServerOperators {} -> [] + CRUserServers {} -> [] + CRUserServersValidation _ -> [] + CRUsageConditions {} -> [] CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile @@ -1209,8 +1214,8 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] -viewUserServers :: AUserProtoServers -> Bool -> [StyledString] -viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) testView = +viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] +viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = customServers <> if testView then [] @@ -1228,8 +1233,8 @@ viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, preset pName = protocolName p customServers = if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers presetServers - else viewServers protoServers + then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers + else viewServers operators protoServers protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1326,8 +1331,11 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> [StyledString] -viewServers = map (plain . B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList +viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] +viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList + where + ops :: Map (Maybe Int64) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators + viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 75b85d7a5f..d435af186e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -423,11 +423,10 @@ smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], tbqSize = 1, - -- serverTbqSize = 1, - msgQueueQuota = 16, msgStoreType = AMSType SMSMemory, - maxJournalMsgCount = 1000, - maxJournalStateLines = 1000, + msgQueueQuota = 16, + maxJournalMsgCount = 24, + maxJournalStateLines = 4, queueIdBytes = 12, msgIdBytes = 6, storeLogFile = Nothing, diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index 0c6baa71bb..e0b1939c9e 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -9,7 +9,7 @@ import Control.Monad (replicateM) import qualified Data.List.NonEmpty as L import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers) import Simplex.Chat.Controller (ChatConfig (..)) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec @@ -18,6 +18,8 @@ randomServersTests = describe "choosig random servers" $ do it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers it "should keep all 6 XFTP servers" testRandomXFTPServers +deriving instance Eq ServerRoles + deriving instance Eq (ServerCfg p) testRandomSMPServers :: IO () From 5a8bf9106e61b63bb91a9c7e58f27503a6a3db3a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 4 Nov 2024 16:30:43 +0000 Subject: [PATCH 03/22] core: preset servers and operators (WIP) --- src/Simplex/Chat.hs | 120 +++++++++++++----- src/Simplex/Chat/Controller.hs | 9 +- .../Migrations/M20241027_server_operators.hs | 3 +- src/Simplex/Chat/Operators.hs | 25 +++- 4 files changed, 116 insertions(+), 41 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 380f6c5d24..1c27db7ba7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -138,6 +138,34 @@ import qualified UnliftIO.Exception as E import UnliftIO.IO (hClose, hSeek, hTell, openFile) import UnliftIO.STM +operatorSimpleXChat :: ServerOperator +operatorSimpleXChat = + ServerOperator + { operatorId = Nothing, + operatorTag = Just OTSimplex, + appVendor = True, + tradeName = "SimpleX Chat", + legalName = Just "SimpleX Chat Ltd", + serverDomains = ["simplex.im"], + acceptedConditions = CARequired Nothing, + enabled = True, + roles = allRoles + } + +operatorXYZ :: ServerOperator +operatorXYZ = + ServerOperator + { operatorId = Nothing, + operatorTag = Just OTXyz, + appVendor = False, + tradeName = "XYZ", + legalName = Just "XYZ Ltd", + serverDomains = ["xyz.com"], + acceptedConditions = CARequired Nothing, + enabled = False, + roles = ServerRoles {storage = False, proxy = True} + } + defaultChatConfig :: ChatConfig defaultChatConfig = ChatConfig @@ -148,13 +176,25 @@ defaultChatConfig = }, chatVRange = supportedChatVRange, confirmMigrations = MCConsole, - defaultServers = - DefaultAgentServers - { smp = _defaultSMPServers, - useSMP = 4, + presetServers = + PresetServers + { operators = + [ PresetOperatorServers + { operator = operatorSimpleXChat, + smpServers = simplexChatSMPServers, + useSMP = 4, + xftpServers = L.map (PresetServer True) defaultXFTPServers, + useXFTP = 3 + }, + PresetOperatorServers + { operator = operatorXYZ, + smpServers = xyzSMPServers, + useSMP = 3, + xftpServers = xyzXFTPServers, + useXFTP = 3, + } + ], ntf = _defaultNtfServers, - xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, - useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig }, tbqSize = 1024, @@ -178,32 +218,52 @@ defaultChatConfig = chatHooks = defaultChatHooks } -_defaultSMPServers :: NonEmpty (ServerCfg 'PSMP) -_defaultSMPServers = - L.fromList $ - map - (presetServerCfg True allRoles operatorSimpleXChat) - [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", - "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", - "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", - "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", - "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", - "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", - "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", - "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", - "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", - "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", - "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" +simplexChatSMPServers :: NonEmpty (PresetServer 'PSMP) +simplexChatSMPServers = + L.map + (PresetServer True) + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", + "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", + "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", + "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", + "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", + "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", + "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", + "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" + ] + <> L.map + (PresetServer False) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] - <> map - (presetServerCfg False allRoles operatorSimpleXChat) - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ] -operatorSimpleXChat :: Maybe OperatorId -operatorSimpleXChat = Just 1 +xyzSMPServers :: NonEmpty (PresetServer 'PSMP) +xyzSMPServers = + L.map + (PresetServer True) + [ "smp://abcd@smp1.xyz.com", + "smp://abcd@smp2.xyz.com", + "smp://abcd@smp3.xyz.com", + "smp://abcd@smp4.xyz.com", + "smp://abcd@smp5.xyz.com", + "smp://abcd@smp6.xyz.com" + ] + +xyzXFTPServers :: NonEmpty (PresetServer 'PXFTP) +xyzXFTPServers = + L.map + (PresetServer True) + [ "xftp://abcd@xftp1.xyz.com", + "xftp://abcd@xftp2.xyz.com", + "xftp://abcd@xftp3.xyz.com", + "xftp://abcd@xftp4.xyz.com", + "xftp://abcd@xftp5.xyz.com", + "xftp://abcd@xftp6.xyz.com" + ] _defaultNtfServers :: [NtfServer] _defaultNtfServers = diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bd2cee3e50..a9c07d6b06 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -133,7 +133,7 @@ data ChatConfig = ChatConfig { agentConfig :: AgentConfig, chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, - defaultServers :: DefaultAgentServers, + presetServers :: PresetServers, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -173,12 +173,9 @@ defaultChatHooks = eventHook = \_ -> pure } -data DefaultAgentServers = DefaultAgentServers - { smp :: NonEmpty (ServerCfg 'PSMP), - useSMP :: Int, +data PresetServers = PresetServers + { operators :: NonEmpty PresetOperatorServers, ntf :: [NtfServer], - xftp :: NonEmpty (ServerCfg 'PXFTP), - useXFTP :: Int, netCfg :: NetworkConfig } diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index bc9f40bddf..42c28be675 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -11,13 +11,13 @@ m20241027_server_operators = CREATE TABLE server_operators ( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, + app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, role_storage INTEGER NOT NULL DEFAULT 1, role_proxy INTEGER NOT NULL DEFAULT 1, - accepted_conditions_commit TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); @@ -37,6 +37,7 @@ CREATE TABLE operator_usage_conditions ( server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, server_operator_tag TEXT, conditions_commit TEXT NOT NULL, + conditions_accepted INTEGER NOT NULL, accepted_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 9a2dac0b1b..3fea13fce8 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -29,10 +29,13 @@ import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" +previousConditionsCommit :: Text +previousConditionsCommit = "edf99fcd1d7d38d2501d19608b94c084cf00f2ac" + usageConditionsText :: Text usageConditionsText = $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") - in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] + in [| stripFrontMatter (safeDecodeUtf8 $(lift s)) |] ) data OperatorTag = OTSimplex | OTXyz @@ -83,6 +86,7 @@ data ConditionsAcceptance data ServerOperator = ServerOperator { operatorId :: OperatorId, operatorTag :: Maybe OperatorTag, + appVendor :: Bool, tradeName :: Text, legalName :: Maybe Text, serverDomains :: [Text], @@ -93,12 +97,25 @@ data ServerOperator = ServerOperator deriving (Show) data UserServers = UserServers - { operator :: ServerOperator, - smpServers :: NonEmpty (ProtoServerWithAuth 'PSMP), - xftpServers :: NonEmpty (ProtoServerWithAuth 'PXFTP) + { operator :: Maybe ServerOperator, + smpServers :: [ServerCfg 'PSMP], + xftpServers :: [ServerCfg 'PXFTP] } deriving (Show) +data PresetOperatorServers = PresetOperatorServers + { operator :: ServerOperator, + smpServers :: NonEmpty (PresetServer 'PSMP), + xftpServers :: NonEmpty (PresetServer 'PXFTP), + useSMP :: Int, + useXFTP :: Int + } + +data PresetServer p = PresetServer + { useServer :: Bool, + server :: ProtoServerWithAuth p + } + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) From bdaec30fa084e4c18964035d432abc42a55288a7 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:11:03 +0400 Subject: [PATCH 04/22] core: getServerOperators, getUserServers, getUsageConditions apis wip (#5141) --- src/Simplex/Chat.hs | 31 +++++++------ src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Operators.hs | 38 ++++++++++++---- src/Simplex/Chat/Store/Profiles.hs | 73 ++++++++++++++++++++++++------ src/Simplex/Chat/Store/Shared.hs | 1 + 5 files changed, 107 insertions(+), 38 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 380f6c5d24..bd165ea5e6 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1489,8 +1489,7 @@ processChatCommand' vr = \case APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config srvs <- withFastStore' (`getProtocolServers` user) - ts <- liftIO getCurrentTime - operators <- withFastStore' $ \db -> getServerOperators db ts + operators <- withFastStore $ \db -> getServerOperators db let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> @@ -1508,27 +1507,31 @@ processChatCommand' vr = \case lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv - APIGetServerOperators -> pure $ chatCmdError Nothing "not supported" + APIGetServerOperators -> do + operators <- withFastStore $ \db -> getServerOperators db + let conditionsAction = usageConditionsAction operators + pure $ CRServerOperators operators conditionsAction APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" - APIGetUserServers userId -> withUserId userId $ \user -> - pure $ chatCmdError (Just user) "not supported" + APIGetUserServers userId -> withUserId userId $ \user -> do + (operators, smpServers, xftpServers) <- withFastStore $ \db -> do + operators <- getServerOperators db + smpServers <- liftIO $ getServers db user SPSMP + xftpServers <- liftIO $ getServers db user SPXFTP + pure (operators, smpServers, xftpServers) + let userServers = groupByOperator operators smpServers xftpServers + pure $ CRUserServers user userServers + where + getServers :: (ProtocolTypeI p) => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] + getServers db user _p = getProtocolServers db user APISetUserServers userId _userServers -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "not supported" APIValidateServers _userServers -> -- response is CRUserServersValidation pure $ chatCmdError Nothing "not supported" APIGetUsageConditions -> do + usageConditions <- withFastStore $ \db -> getCurrentUsageConditions db -- TODO - -- get current conditions -- get latest accepted conditions (from operators) - ts <- liftIO getCurrentTime - let usageConditions = - UsageConditions - { conditionsId = 1, - conditionsCommit = "abc", - notifiedAt = Nothing, - createdAt = ts - } pure CRUsageConditions { usageConditions = usageConditions, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bd2cee3e50..2cb8e0cd42 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -589,7 +589,7 @@ data ChatResponse | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} - | CRUserServers {userServers :: [UserServers]} + | CRUserServers {user :: User, userServers :: [UserServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 9a2dac0b1b..ff110e2ada 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -12,7 +13,9 @@ import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) -import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Time.Clock (UTCTime) import Database.SQLite.Simple.FromField (FromField (..)) @@ -20,10 +23,10 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerRoles) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Protocol (ProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -72,8 +75,8 @@ data UsageConditionsAction deriving (Show) -- TODO UI logic -usageConditionsAction :: UsageConditionsAction -usageConditionsAction = UCAAccepted [] +usageConditionsAction :: [ServerOperator] -> UsageConditionsAction +usageConditionsAction _operators = UCAAccepted [] data ConditionsAcceptance = CAAccepted {acceptedAt :: UTCTime} @@ -93,12 +96,31 @@ data ServerOperator = ServerOperator deriving (Show) data UserServers = UserServers - { operator :: ServerOperator, - smpServers :: NonEmpty (ProtoServerWithAuth 'PSMP), - xftpServers :: NonEmpty (ProtoServerWithAuth 'PXFTP) + { operator :: Maybe ServerOperator, + smpServers :: [ServerCfg 'PSMP], + xftpServers :: [ServerCfg 'PXFTP] } deriving (Show) +groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] +groupByOperator srvOperators smpSrvs xftpSrvs = + map createOperatorServers (M.toList combinedMap) + where + srvOperatorId :: ServerCfg p -> Maybe Int64 + srvOperatorId ServerCfg {operator} = operator + operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) + operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing + initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) + initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] + smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs + combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs + createOperatorServers (key, (groupedSmps, groupedXftps)) = + UserServers + { operator = fromMaybe Nothing (M.lookup key operatorMap), + smpServers = groupedSmps, + xftpServers = groupedXftps + } + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index fe2cc737fb..d6627505f3 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,6 +50,7 @@ module Simplex.Chat.Store.Profiles -- overwriteOperatorsAndServers, overwriteProtocolServers, getServerOperators, + getCurrentUsageConditions, createCall, deleteCalls, getCalls, @@ -73,7 +74,8 @@ import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time.Clock (UTCTime (..), getCurrentTime) +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call @@ -540,7 +542,7 @@ getProtocolServers db User {userId} = -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () overwriteProtocolServers db User {userId} servers = -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do @@ -556,25 +558,66 @@ overwriteProtocolServers db User {userId} servers = VALUES (?,?,?,?,?,?,?,?,?,?,?) |] ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) - pure $ Right () -- Right <$> getProtocolServers db user + pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p -getServerOperators :: DB.Connection -> UTCTime -> IO [ServerOperator] -getServerOperators db ts = - map toOperator - <$> DB.query_ - db - [sql| - SELECT server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy - FROM server_operators; +getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] +getServerOperators db = do + conditions <- getCurrentUsageConditions db + liftIO $ + map (toOperator conditions) + <$> DB.query_ + db + [sql| + SELECT + so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, + so.server_domains, so.enabled, so.role_storage, so.role_proxy, + LastOperatorConditions.conditions_commit, LastOperatorConditions.accepted_at + FROM server_operators so + LEFT JOIN ( + SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) + FROM operator_usage_conditions + GROUP BY server_operator_id + ) LastOperatorConditions ON LastOperatorConditions.server_operator_id = so.server_operator_id |] where - -- TODO get conditions state - toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = - let roles = ServerRoles {storage, proxy} - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions = CAAccepted ts, enabled, roles} + toOperator :: + UsageConditions -> + ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) + :. (Maybe Text, Maybe UTCTime) + ) -> + ServerOperator + toOperator + UsageConditions {conditionsCommit, createdAt} + ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) + :. (operatorConditionsCommit_, acceptedAt_) + ) = + let roles = ServerRoles {storage, proxy} + acceptedConditions = case (operatorConditionsCommit_, acceptedAt_) of + (Nothing, _) -> CARequired Nothing + (Just operatorConditionsCommit, Just acceptedAt) + | conditionsCommit == operatorConditionsCommit -> CAAccepted acceptedAt + _ -> CARequired (Just $ conditionsDeadline createdAt) + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions, enabled, roles} + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) + +getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions +getCurrentUsageConditions db = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query_ + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id DESC LIMIT 1 + |] + +toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions +toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = + UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index f9a8685ec8..083079e2ea 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -127,6 +127,7 @@ data StoreError | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlDuplicateCA | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} + | SEUsageConditionsNotFound deriving (Show, Exception) $(J.deriveJSON (sumTypeJSON $ dropPrefix "SE") ''StoreError) From 2998e3af3fc8ef7b5738ba5dfc89227d03300069 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 4 Nov 2024 19:44:45 +0000 Subject: [PATCH 05/22] usageConditionsToAdd --- src/Simplex/Chat/Operators.hs | 44 ++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index c0264e5b35..2e12dff39c 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} @@ -13,6 +14,9 @@ import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) +import Data.List (find) +import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) @@ -26,7 +30,7 @@ import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtocolType (..)) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -119,6 +123,44 @@ data PresetServer p = PresetServer server :: ProtoServerWithAuth p } +data UpdatedUsageConditions = UpdatedUsageConditions + { currentConditions :: UsageConditions, + conditionsToAdd :: [UsageConditions], + updatedConditions :: NonEmpty UsageConditions + } + +-- this function should be called inside DB transaction to update conditions in the database +-- it returns (current conditions record in the final list, conditions to add, all conditions) +usageConditionsToAdd :: Text -> Text -> UTCTime -> [UsageConditions] -> UpdatedUsageConditions +usageConditionsToAdd prevCommit currCommit createdAt conds = case L.nonEmpty conds of + Nothing -> + UpdatedUsageConditions + { currentConditions = currCond, + conditionsToAdd = [prevCond, currCond], + updatedConditions = [prevCond, currCond] + } + where + prevCond = conditions 1 prevCommit + currCond = conditions 2 currCommit + Just conds' -> case find ((currCommit ==) . conditionsCommit) conds of + Just currCond -> + UpdatedUsageConditions + { currentConditions = currCond, + conditionsToAdd = [], + updatedConditions = conds' + } + Nothing -> + UpdatedUsageConditions + { currentConditions = currCond, + conditionsToAdd = [currCond], + updatedConditions = conds' <> [currCond] + } + where + cId = maximum (map conditionsId conds) + 1 + currCond = conditions cId currCommit + where + conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt} + groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] groupByOperator srvOperators smpSrvs xftpSrvs = map createOperatorServers (M.toList combinedMap) From d4a47f1ccedc4db3500917d3895acf06d55380d5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 4 Nov 2024 20:07:28 +0000 Subject: [PATCH 06/22] simplify --- src/Simplex/Chat/Operators.hs | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 2e12dff39c..2aca5f7177 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -16,7 +16,6 @@ import Data.FileEmbed import Data.Int (Int64) import Data.List (find) import Data.List.NonEmpty (NonEmpty) -import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe) @@ -123,38 +122,17 @@ data PresetServer p = PresetServer server :: ProtoServerWithAuth p } -data UpdatedUsageConditions = UpdatedUsageConditions - { currentConditions :: UsageConditions, - conditionsToAdd :: [UsageConditions], - updatedConditions :: NonEmpty UsageConditions - } - -- this function should be called inside DB transaction to update conditions in the database -- it returns (current conditions record in the final list, conditions to add, all conditions) -usageConditionsToAdd :: Text -> Text -> UTCTime -> [UsageConditions] -> UpdatedUsageConditions -usageConditionsToAdd prevCommit currCommit createdAt conds = case L.nonEmpty conds of - Nothing -> - UpdatedUsageConditions - { currentConditions = currCond, - conditionsToAdd = [prevCond, currCond], - updatedConditions = [prevCond, currCond] - } +usageConditionsToAdd :: Text -> Text -> UTCTime -> [UsageConditions] -> (UsageConditions, [UsageConditions]) +usageConditionsToAdd prevCommit currCommit createdAt = \case + [] -> (currCond, [prevCond, currCond]) where prevCond = conditions 1 prevCommit currCond = conditions 2 currCommit - Just conds' -> case find ((currCommit ==) . conditionsCommit) conds of - Just currCond -> - UpdatedUsageConditions - { currentConditions = currCond, - conditionsToAdd = [], - updatedConditions = conds' - } - Nothing -> - UpdatedUsageConditions - { currentConditions = currCond, - conditionsToAdd = [currCond], - updatedConditions = conds' <> [currCond] - } + conds -> case find ((currCommit ==) . conditionsCommit) conds of + Just currCond -> (currCond, []) + Nothing -> (currCond, [currCond]) where cId = maximum (map conditionsId conds) + 1 currCond = conditions cId currCommit From 3b0205b25f5a3377d0b5c6162f21d6cc82b4565a Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:15:20 +0400 Subject: [PATCH 07/22] core: setServerOperators, getUsageConditions api wip (#5145) --- src/Simplex/Chat.hs | 16 ++-- src/Simplex/Chat/Controller.hs | 2 +- .../Migrations/M20241027_server_operators.hs | 10 +- src/Simplex/Chat/Migrations/chat_schema.sql | 2 +- src/Simplex/Chat/Operators.hs | 15 ++- src/Simplex/Chat/Store/Profiles.hs | 91 ++++++++++++++++--- 6 files changed, 105 insertions(+), 31 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bd165ea5e6..b083134e2c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1511,7 +1511,10 @@ processChatCommand' vr = \case operators <- withFastStore $ \db -> getServerOperators db let conditionsAction = usageConditionsAction operators pure $ CRServerOperators operators conditionsAction - APISetServerOperators _operators -> pure $ chatCmdError Nothing "not supported" + APISetServerOperators operatorsEnabled -> do + operators <- withFastStore $ \db -> setServerOperators db operatorsEnabled + let conditionsAction = usageConditionsAction operators + pure $ CRServerOperators operators conditionsAction APIGetUserServers userId -> withUserId userId $ \user -> do (operators, smpServers, xftpServers) <- withFastStore $ \db -> do operators <- getServerOperators db @@ -1529,14 +1532,15 @@ processChatCommand' vr = \case -- response is CRUserServersValidation pure $ chatCmdError Nothing "not supported" APIGetUsageConditions -> do - usageConditions <- withFastStore $ \db -> getCurrentUsageConditions db - -- TODO - -- get latest accepted conditions (from operators) + (usageConditions, acceptedConditions) <- withFastStore $ \db -> do + usageConditions <- getCurrentUsageConditions db + acceptedConditions <- getLatestAcceptedConditions db + pure (usageConditions, acceptedConditions) pure CRUsageConditions - { usageConditions = usageConditions, + { usageConditions, conditionsText = usageConditionsText, - acceptedConditions = Nothing + acceptedConditions } APISetConditionsNotified _conditionsId -> do pure $ chatCmdError Nothing "not supported" diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2cb8e0cd42..81e7a9980b 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -354,7 +354,7 @@ data ChatCommand | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators - | APISetServerOperators (NonEmpty (OperatorId, Bool)) + | APISetServerOperators (NonEmpty OperatorEnabled) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UserServers) | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index bc9f40bddf..fc0ca21e54 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -11,13 +11,13 @@ m20241027_server_operators = CREATE TABLE server_operators ( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, + app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, role_storage INTEGER NOT NULL DEFAULT 1, role_proxy INTEGER NOT NULL DEFAULT 1, - accepted_conditions_commit TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); @@ -46,11 +46,11 @@ CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_ CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); INSERT INTO server_operators - (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) - VALUES (1, 'simplex', 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); + (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) + VALUES (1, 'simplex', 1, 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); INSERT INTO server_operators - (server_operator_id, server_operator_tag, trade_name, legal_name, server_domains, enabled) - VALUES (2, 'xyz', 'XYZ', 'XYZ Ltd', 'xyz.com', 0); + (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) + VALUES (2, 'xyz', 0, 'XYZ', 'XYZ Ltd', 'xyz.com', 0); -- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; |] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 07c363eda9..1541f36b60 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -593,13 +593,13 @@ CREATE TABLE app_settings(app_settings TEXT NOT NULL); CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, + app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, enabled INTEGER NOT NULL DEFAULT 1, role_storage INTEGER NOT NULL DEFAULT 1, role_proxy INTEGER NOT NULL DEFAULT 1, - accepted_conditions_commit TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index ff110e2ada..6fc5663085 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -79,7 +79,7 @@ usageConditionsAction :: [ServerOperator] -> UsageConditionsAction usageConditionsAction _operators = UCAAccepted [] data ConditionsAcceptance - = CAAccepted {acceptedAt :: UTCTime} + = CAAccepted {acceptedAt :: Maybe UTCTime} | CARequired {deadline :: Maybe UTCTime} deriving (Show) @@ -89,7 +89,14 @@ data ServerOperator = ServerOperator tradeName :: Text, legalName :: Maybe Text, serverDomains :: [Text], - acceptedConditions :: ConditionsAcceptance, + conditionsAcceptance :: ConditionsAcceptance, + enabled :: Bool, + roles :: ServerRoles + } + deriving (Show) + +data OperatorEnabled = OperatorEnabled + { operatorId :: OperatorId, enabled :: Bool, roles :: ServerRoles } @@ -106,10 +113,10 @@ groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] - groupByOperator srvOperators smpSrvs xftpSrvs = map createOperatorServers (M.toList combinedMap) where - srvOperatorId :: ServerCfg p -> Maybe Int64 srvOperatorId ServerCfg {operator} = operator + opId ServerOperator {operatorId} = operatorId operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) - operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing + operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index d6627505f3..259d08d9ad 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -50,7 +50,9 @@ module Simplex.Chat.Store.Profiles -- overwriteOperatorsAndServers, overwriteProtocolServers, getServerOperators, + setServerOperators, getCurrentUsageConditions, + getLatestAcceptedConditions, createCall, deleteCalls, getCalls, @@ -72,7 +74,7 @@ import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) -import Data.Text (Text) +import Data.Text (Text, splitOn) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) @@ -565,44 +567,80 @@ overwriteProtocolServers db User {userId} servers = getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] getServerOperators db = do - conditions <- getCurrentUsageConditions db + now <- liftIO getCurrentTime + currentConditions <- getCurrentUsageConditions db + latestAcceptedConditions <- getLatestAcceptedConditions db liftIO $ - map (toOperator conditions) + map (toOperator now currentConditions latestAcceptedConditions) <$> DB.query_ db [sql| SELECT so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, so.server_domains, so.enabled, so.role_storage, so.role_proxy, - LastOperatorConditions.conditions_commit, LastOperatorConditions.accepted_at + AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at FROM server_operators so LEFT JOIN ( SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) FROM operator_usage_conditions GROUP BY server_operator_id - ) LastOperatorConditions ON LastOperatorConditions.server_operator_id = so.server_operator_id + ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id |] where toOperator :: + UTCTime -> UsageConditions -> + Maybe UsageConditions -> ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) :. (Maybe Text, Maybe UTCTime) ) -> ServerOperator toOperator - UsageConditions {conditionsCommit, createdAt} + now + UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} + latestAcceptedConditions_ ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) - :. (operatorConditionsCommit_, acceptedAt_) + :. (operatorCommit_, acceptedAt_) ) = let roles = ServerRoles {storage, proxy} - acceptedConditions = case (operatorConditionsCommit_, acceptedAt_) of + serverDomains = splitOn "," domains + conditionsAcceptance = case (latestAcceptedConditions_, operatorCommit_) of + -- no conditions were ever accepted for any operator(s) + -- (shouldn't happen as there should always be record for SimpleX Chat) (Nothing, _) -> CARequired Nothing - (Just operatorConditionsCommit, Just acceptedAt) - | conditionsCommit == operatorConditionsCommit -> CAAccepted acceptedAt - _ -> CARequired (Just $ conditionsDeadline createdAt) - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains = [domains], acceptedConditions, enabled, roles} - conditionsDeadline :: UTCTime -> UTCTime - conditionsDeadline = addUTCTime (31 * nominalDay) + -- no conditions were ever accepted for this operator + (_, Nothing) -> CARequired Nothing + (Just UsageConditions {conditionsCommit = latestAcceptedCommit}, Just operatorCommit) + | latestAcceptedCommit == currentCommit -> + if operatorCommit == latestAcceptedCommit + then -- current conditions were accepted for operator + CAAccepted acceptedAt_ + else -- current conditions were NOT accepted for operator, but were accepted for other operator(s) + CARequired Nothing + | otherwise -> + if operatorCommit == latestAcceptedCommit + then -- new conditions available, latest accepted conditions were accepted for operator + conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) + CARequired Nothing + in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} + conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> ConditionsAcceptance + conditionsRequiredOrDeadline createdAt notifiedAtOrNow = + if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt + then CARequired (Just $ conditionsDeadline notifiedAtOrNow) + else CARequired Nothing + where + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) + +setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO [ServerOperator] +setServerOperators db operatorsEnabled = do + liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> + DB.execute + db + "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" + (enabled, storage, proxy, operatorId) + getServerOperators db getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions getCurrentUsageConditions db = @@ -619,6 +657,31 @@ toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} +getLatestAcceptedConditions :: DB.Connection -> ExceptT StoreError IO (Maybe UsageConditions) +getLatestAcceptedConditions db = do + (latestAcceptedCommit_ :: Maybe Text) <- + liftIO $ + maybeFirstRow fromOnly $ + DB.query_ + db + [sql| + SELECT conditions_commit + FROM operator_usage_conditions + WHERE conditions_accepted = 1 + ORDER BY accepted_at DESC + LIMIT 1 + |] + forM latestAcceptedCommit_ $ \latestAcceptedCommit -> + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ? + |] + (Only latestAcceptedCommit) + -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do -- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" From 601ddf97ce1faaab88668d4e6292c68d188c6901 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 5 Nov 2024 16:29:30 +0000 Subject: [PATCH 08/22] WIP --- .../Migrations/M20241027_server_operators.hs | 1 - src/Simplex/Chat/Operators.hs | 168 +++++++++++++----- 2 files changed, 126 insertions(+), 43 deletions(-) diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index cf1c91e401..fc0ca21e54 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -37,7 +37,6 @@ CREATE TABLE operator_usage_conditions ( server_operator_id INTEGER REFERENCES server_operators (server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE, server_operator_tag TEXT, conditions_commit TEXT NOT NULL, - conditions_accepted INTEGER NOT NULL, accepted_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 72d3f639eb..cb8bbfe1d5 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -4,21 +4,25 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} module Simplex.Chat.Operators where +import Control.Monad (foldM) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed +import Data.Foldable1 (fold1) import Data.Int (Int64) -import Data.List (find) +import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) import Data.Time.Clock (UTCTime) import Database.SQLite.Simple.FromField (FromField (..)) @@ -26,10 +30,10 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -45,7 +49,7 @@ usageConditionsText = ) data OperatorTag = OTSimplex | OTXyz - deriving (Show) + deriving (Eq, Show) instance FromField OperatorTag where fromField = fromTextField_ textDecode @@ -90,7 +94,7 @@ data ConditionsAcceptance deriving (Show) data ServerOperator = ServerOperator - { operatorId :: OperatorId, + { operatorId :: Maybe OperatorId, operatorTag :: Maybe OperatorTag, appVendor :: Bool, tradeName :: Text, @@ -103,23 +107,32 @@ data ServerOperator = ServerOperator deriving (Show) data OperatorEnabled = OperatorEnabled - { operatorId :: OperatorId, - enabled :: Bool, - roles :: ServerRoles + { operatorId' :: OperatorId, + enabled' :: Bool, + roles' :: ServerRoles } deriving (Show) data UserServers = UserServers { operator :: Maybe ServerOperator, - smpServers :: [ServerCfg 'PSMP], - xftpServers :: [ServerCfg 'PXFTP] + smpServers :: [UserServer 'PSMP], + xftpServers :: [UserServer 'PXFTP] + } + deriving (Show) + +data UserServer p = UserServer + { serverId :: Maybe Int64, + serverOperatorId :: Maybe OperatorId, + server :: ProtoServerWithAuth p, + tested :: Maybe Bool, + enabled :: Bool } deriving (Show) data PresetOperatorServers = PresetOperatorServers { operator :: ServerOperator, - smpServers :: NonEmpty (PresetServer 'PSMP), - xftpServers :: NonEmpty (PresetServer 'PXFTP), + presetSMPServers :: NonEmpty (PresetServer 'PSMP), + presetXFTPServers :: NonEmpty (PresetServer 'PXFTP), useSMP :: Int, useXFTP :: Int } @@ -129,41 +142,105 @@ data PresetServer p = PresetServer server :: ProtoServerWithAuth p } --- this function should be called inside DB transaction to update conditions in the database --- it returns (current conditions record in the final list, conditions to add, all conditions) -usageConditionsToAdd :: Text -> Text -> UTCTime -> [UsageConditions] -> (UsageConditions, [UsageConditions]) -usageConditionsToAdd prevCommit currCommit createdAt = \case - [] -> (currCond, [prevCond, currCond]) +-- This function should be used inside DB transaction to update conditions in the database +-- it returns (conditions to mark as accepted to SimpleX operator, conditions to add) +usageConditionsToAdd :: Bool -> Text -> Text -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, [UsageConditions]) +usageConditionsToAdd newUser prevCommit sourceCommit createdAt = \case + [] + | newUser -> (Just sourceCond, [sourceCond]) + | otherwise -> (Just prevCond, [prevCond, sourceCond]) where prevCond = conditions 1 prevCommit - currCond = conditions 2 currCommit - conds -> case find ((currCommit ==) . conditionsCommit) conds of - Just currCond -> (currCond, []) - Nothing -> (currCond, [currCond]) - where - cId = maximum (map conditionsId conds) + 1 - currCond = conditions cId currCommit + sourceCond = conditions 2 sourceCommit + conds -> (Nothing, if hasSourceCond then [] else [sourceCond]) + where + hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds + sourceCond = conditions cId sourceCommit + cId = maximum (map conditionsId conds) + 1 where conditions cId commit = UsageConditions {conditionsId = cId, conditionsCommit = commit, notifiedAt = Nothing, createdAt} -groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] -groupByOperator srvOperators smpSrvs xftpSrvs = - map createOperatorServers (M.toList combinedMap) +-- This function should be used inside DB transaction to update operators. +-- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, +-- and preserves custom operators without tags for forward compatibility. +updatedServerOperators :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [ServerOperator] +updatedServerOperators presetSrvs storedOps = + foldr addPreset [] presetSrvs <> filter (isNothing . operatorTag) storedOps -- TODO remove domains of preset operators from custom where - srvOperatorId ServerCfg {operator} = operator - opId ServerOperator {operatorId} = operatorId - operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) - operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing - initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) - initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] - smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs - combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs - createOperatorServers (key, (groupedSmps, groupedXftps)) = - UserServers - { operator = fromMaybe Nothing (M.lookup key operatorMap), - smpServers = groupedSmps, - xftpServers = groupedXftps - } + addPreset PresetOperatorServers {operator = presetOp} = (storedOp' :) + where + storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of + Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> + presetOp {operatorId, conditionsAcceptance, enabled, roles} + Nothing -> presetOp + + +data UpdatedServers p = UpdatedServers + { toUpsert :: [UserServer p], + toDelete :: [Int64] + } + +-- This function should be used inside DB transaction to update servers. +-- It assumes that the list of operators was amended using updatedServerOperators, +-- that [ServerOperator] has the same operators as [PresetOperatorServers], +-- and that they all have serverOperatorId set. +updatedUserServers :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> Either String ([UserServer 'PSMP], [UserServer 'PXFTP]) +updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do + smpSrvs' <- updatedSrvs useSMP smpSrvs =<< presetSrvsToStore presetSMPServers + xftpSrvs' <- updatedSrvs useXFTP xftpSrvs =<< presetSrvsToStore presetXFTPServers + pure (smpSrvs', xftpSrvs') + where + presetSrvsToStore :: forall p. (PresetOperatorServers -> NonEmpty (PresetServer p)) -> Either String (NonEmpty (Bool, UserServer p)) + presetSrvsToStore presetSel = fold1 <$> mapM operatorSrvs presetSrvs + where + operatorSrvs :: PresetOperatorServers -> Either String (NonEmpty (Bool, UserServer p)) + operatorSrvs op@PresetOperatorServers {operator} = case find ((operatorTag operator ==) . operatorTag) storedOps of + Nothing -> Left "preset operator not stored" + Just op' -> Right $ L.map (userSrv op') (presetSel op) + userSrv op PresetServer {server, useServer} = + let srv = UserServer {serverId = Nothing, serverOperatorId = operatorId op, server, tested = Nothing, enabled = False} + in (useServer, srv) + + updatedSrvs :: forall p. (PresetOperatorServers -> Int) -> [UserServer p] -> NonEmpty (Bool, UserServer p) -> Either String [UserServer p] + updatedSrvs useSel storedSrvs presetSrvs = + fmap enabledSrvs . addOtherServers =<< foldM updatedSrv (storedSrvs', []) presetSrvs + where + storedSrvs' :: Map (ProtoServerWithAuth p) (UserServer p) + storedSrvs' = foldl' (\m us@UserServer {server} -> M.insert server us m) M.empty storedSrvs + updatedSrv :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> (Bool, UserServer p) -> Either String (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) + updatedSrv srvs srv = undefined + addOtherServers :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> Either String [(Bool, UserServer p)] + addOtherServers = undefined + enabledSrvs :: [(Bool, UserServer p)] -> [UserServer p] + enabledSrvs = undefined + + -- addSrv srv@ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} uss = + -- case find (\us -> any [\h -> any (\d -> d `T.isSuffixOf` ) serverDomains (operator us)] host) uss of + -- Just opId + -- where + -- hasOperatorDomain ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} us + + + -- addSrv srv uss = ... а тут просто найти оператора в списке и вставить ему сервер через add и как то ругнуться если его нет (но такого не должно быть). Либо вообще есть вариант сразу читать в этом формате - сначала прочитать операторов и в цикле читать серверы каждого - это вот может быть еще проще + +-- groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] +-- groupByOperator srvOperators smpSrvs xftpSrvs = +-- map createOperatorServers (M.toList combinedMap) +-- where +-- srvOperatorId ServerCfg {operator} = operator +-- opId ServerOperator {operatorId} = operatorId +-- operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) +-- operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing +-- initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) +-- initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] +-- smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs +-- combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs +-- createOperatorServers (key, (groupedSmps, groupedXftps)) = +-- UserServers +-- { operator = fromMaybe Nothing (M.lookup key operatorMap), +-- smpServers = groupedSmps, +-- xftpServers = groupedXftps +-- } $(JQ.deriveJSON defaultJSON ''UsageConditions) @@ -173,4 +250,11 @@ $(JQ.deriveJSON defaultJSON ''ServerOperator) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) +instance ProtocolTypeI p => ToJSON (UserServer p) where + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer) + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer) + +instance ProtocolTypeI p => FromJSON (UserServer p) where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer) + $(JQ.deriveJSON defaultJSON ''UserServers) From 2da89c2cf1d155e228979c6460c5dddb0cca73c3 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:40:33 +0400 Subject: [PATCH 09/22] core: setConditionsNotified, acceptConditions, setUserServers, validateServers apis wip (#5147) --- src/Simplex/Chat.hs | 39 +++++---- src/Simplex/Chat/Controller.hs | 14 +--- src/Simplex/Chat/Operators.hs | 65 +++++++++++++-- src/Simplex/Chat/Store/Profiles.hs | 127 ++++++++++++++++++++++------- 4 files changed, 181 insertions(+), 64 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index b083134e2c..69b78ba9d4 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1489,7 +1489,7 @@ processChatCommand' vr = \case APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do cfg@ChatConfig {defaultServers} <- asks config srvs <- withFastStore' (`getProtocolServers` user) - operators <- withFastStore $ \db -> getServerOperators db + (operators, _) <- withFastStore $ \db -> getServerOperators db let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) pure $ CRUserProtoServers {user, servers, operators} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> @@ -1508,44 +1508,51 @@ processChatCommand' vr = \case TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv APIGetServerOperators -> do - operators <- withFastStore $ \db -> getServerOperators db - let conditionsAction = usageConditionsAction operators + (operators, conditionsAction) <- withFastStore $ \db -> getServerOperators db pure $ CRServerOperators operators conditionsAction APISetServerOperators operatorsEnabled -> do - operators <- withFastStore $ \db -> setServerOperators db operatorsEnabled - let conditionsAction = usageConditionsAction operators + (operators, conditionsAction) <- withFastStore $ \db -> setServerOperators db operatorsEnabled pure $ CRServerOperators operators conditionsAction APIGetUserServers userId -> withUserId userId $ \user -> do (operators, smpServers, xftpServers) <- withFastStore $ \db -> do - operators <- getServerOperators db + (operators, _) <- getServerOperators db smpServers <- liftIO $ getServers db user SPSMP xftpServers <- liftIO $ getServers db user SPXFTP pure (operators, smpServers, xftpServers) let userServers = groupByOperator operators smpServers xftpServers pure $ CRUserServers user userServers where - getServers :: (ProtocolTypeI p) => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] getServers db user _p = getProtocolServers db user - APISetUserServers userId _userServers -> withUserId userId $ \user -> - pure $ chatCmdError (Just user) "not supported" - APIValidateServers _userServers -> - -- response is CRUserServersValidation - pure $ chatCmdError Nothing "not supported" + APISetUserServers userId userServers -> withUserId userId $ \user -> do + let errors = validateUserServers userServers + unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) + withFastStore $ \db -> setUserServers db user userServers + -- TODO set protocol servers for agent + ok_ + APIValidateServers userServers -> do + let errors = validateUserServers userServers + pure $ CRUserServersValidation errors APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db acceptedConditions <- getLatestAcceptedConditions db pure (usageConditions, acceptedConditions) + -- TODO if db commit is different from source commit, conditionsText should be nothing in response pure CRUsageConditions { usageConditions, conditionsText = usageConditionsText, acceptedConditions } - APISetConditionsNotified _conditionsId -> do - pure $ chatCmdError Nothing "not supported" - APIAcceptConditions _conditionsId _opIds -> - pure $ chatCmdError Nothing "not supported" + APISetConditionsNotified conditionsId -> do + currentTs <- liftIO getCurrentTime + withFastStore' $ \db -> setConditionsNotified db conditionsId currentTs + ok_ + APIAcceptConditions conditionsId operators -> do + currentTs <- liftIO getCurrentTime + (operators', conditionsAction) <- withFastStore $ \db -> acceptConditions db conditionsId operators currentTs + pure $ CRServerOperators operators' conditionsAction APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 81e7a9980b..cbfa0969d4 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -71,7 +71,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, OperatorId, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -360,7 +360,7 @@ data ChatCommand | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 - | APIAcceptConditions Int64 (NonEmpty OperatorId) + | APIAcceptConditions Int64 (NonEmpty ServerOperator) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -588,7 +588,7 @@ data ChatResponse | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} - | CRServerOperators {operators :: [ServerOperator], conditionsAction :: UsageConditionsAction} + | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} | CRUserServers {user :: User, userServers :: [UserServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} @@ -961,12 +961,6 @@ data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) ( deriving instance Show AProtoServersConfig -data UserServersError - = USEStorageMissing - | USEProxyMissing - | USEDuplicate {server :: AProtoServerWithAuth} - deriving (Show) - data UserProtoServers p = UserProtoServers { serverProtocol :: SProtocolType p, protoServers :: NonEmpty (ServerCfg p), @@ -1545,8 +1539,6 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) -$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) - $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''ContactSubStatus) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6fc5663085..5e32807ddc 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -13,20 +13,22 @@ import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) +import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) -import Data.Time.Clock (UTCTime) +import Data.Time (addUTCTime) +import Data.Time.Clock (UTCTime, nominalDay) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (ProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -74,9 +76,30 @@ data UsageConditionsAction | UCAAccepted {operators :: [ServerOperator]} deriving (Show) --- TODO UI logic -usageConditionsAction :: [ServerOperator] -> UsageConditionsAction -usageConditionsAction _operators = UCAAccepted [] +usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction +usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do + let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators + if null enabledOperators + then Nothing + else + if all conditionsAccepted enabledOperators + then + let acceptedForOperators = filter conditionsAccepted operators + in Just $ UCAAccepted acceptedForOperators + else + let acceptForOperators = filter (not . conditionsAccepted) enabledOperators + deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + showNotice = isNothing notifiedAt + in Just $ UCAReview acceptForOperators deadline showNotice + +conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime +conditionsRequiredOrDeadline createdAt notifiedAtOrNow = + if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt + then Just $ conditionsDeadline notifiedAtOrNow + else Nothing -- required + where + conditionsDeadline :: UTCTime -> UTCTime + conditionsDeadline = addUTCTime (31 * nominalDay) data ConditionsAcceptance = CAAccepted {acceptedAt :: Maybe UTCTime} @@ -95,6 +118,11 @@ data ServerOperator = ServerOperator } deriving (Show) +conditionsAccepted :: ServerOperator -> Bool +conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of + CAAccepted {} -> True + _ -> False + data OperatorEnabled = OperatorEnabled { operatorId :: OperatorId, enabled :: Bool, @@ -128,6 +156,27 @@ groupByOperator srvOperators smpSrvs xftpSrvs = xftpServers = groupedXftps } +data UserServersError + = USEStorageMissing + | USEProxyMissing + | USEDuplicate {server :: AProtoServerWithAuth} + deriving (Show) + +validateUserServers :: NonEmpty UserServers -> [UserServersError] +validateUserServers userServers = + let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] + proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] + -- TODO duplicate errors + -- allSMPServers = + -- map (\ServerCfg {server} -> server) $ + -- concatMap (\UserServers {smpServers} -> smpServers) userServers + in storageMissing_ <> proxyMissing_ -- <> duplicateErrors + where + canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool + canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of + Just ServerOperator {roles} -> roleSel roles + Nothing -> not (null smpServers) && not (null xftpServers) + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) @@ -137,3 +186,5 @@ $(JQ.deriveJSON defaultJSON ''ServerOperator) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''UserServers) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 259d08d9ad..f4f574c3d7 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -53,6 +53,9 @@ module Simplex.Chat.Store.Profiles setServerOperators, getCurrentUsageConditions, getLatestAcceptedConditions, + setConditionsNotified, + acceptConditions, + setUserServers, createCall, deleteCalls, getCalls, @@ -76,8 +79,7 @@ import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) import Data.Text (Text, splitOn) import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time (addUTCTime) -import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) +import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call @@ -542,6 +544,7 @@ getProtocolServers db User {userId} = roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} in ServerCfg {server, operator, preset, tested, enabled, roles} +-- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () @@ -565,27 +568,29 @@ overwriteProtocolServers db User {userId} servers = where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p -getServerOperators :: DB.Connection -> ExceptT StoreError IO [ServerOperator] +getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) getServerOperators db = do now <- liftIO getCurrentTime currentConditions <- getCurrentUsageConditions db latestAcceptedConditions <- getLatestAcceptedConditions db - liftIO $ - map (toOperator now currentConditions latestAcceptedConditions) - <$> DB.query_ - db - [sql| - SELECT - so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, - so.server_domains, so.enabled, so.role_storage, so.role_proxy, - AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at - FROM server_operators so - LEFT JOIN ( - SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) - FROM operator_usage_conditions - GROUP BY server_operator_id - ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id - |] + operators <- + liftIO $ + map (toOperator now currentConditions latestAcceptedConditions) + <$> DB.query_ + db + [sql| + SELECT + so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, + so.server_domains, so.enabled, so.role_storage, so.role_proxy, + AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at + FROM server_operators so + LEFT JOIN ( + SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) + FROM operator_usage_conditions + GROUP BY server_operator_id + ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id + |] + pure (operators, usageConditionsAction operators currentConditions now) where toOperator :: UTCTime -> @@ -620,20 +625,12 @@ getServerOperators db = do | otherwise -> if operatorCommit == latestAcceptedCommit then -- new conditions available, latest accepted conditions were accepted for operator - conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) CARequired Nothing in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} - conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> ConditionsAcceptance - conditionsRequiredOrDeadline createdAt notifiedAtOrNow = - if notifiedAtOrNow < addUTCTime (14 * nominalDay) createdAt - then CARequired (Just $ conditionsDeadline notifiedAtOrNow) - else CARequired Nothing - where - conditionsDeadline :: UTCTime -> UTCTime - conditionsDeadline = addUTCTime (31 * nominalDay) -setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO [ServerOperator] +setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) setServerOperators db operatorsEnabled = do liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> DB.execute @@ -667,7 +664,6 @@ getLatestAcceptedConditions db = do [sql| SELECT conditions_commit FROM operator_usage_conditions - WHERE conditions_accepted = 1 ORDER BY accepted_at DESC LIMIT 1 |] @@ -682,6 +678,77 @@ getLatestAcceptedConditions db = do |] (Only latestAcceptedCommit) +setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () +setConditionsNotified db conditionsId notifiedAt = + DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, conditionsId) + +acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) +acceptConditions db conditionsId operators acceptedAt = do + UsageConditions {conditionsCommit} <- getUsageConditionsById_ db conditionsId + liftIO $ forM_ operators $ \ServerOperator {operatorId, operatorTag} -> + DB.execute + db + [sql| + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at) + VALUES (?,?,?,?) + |] + (operatorId, operatorTag, conditionsCommit, acceptedAt) + getServerOperators db + +getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions +getUsageConditionsById_ db conditionsId = + ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ + DB.query + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE usage_conditions_id = ? + |] + (Only conditionsId) + +setUserServers :: DB.Connection -> User -> NonEmpty UserServers -> ExceptT StoreError IO () +setUserServers db User {userId} userServers = do + currentTs <- liftIO getCurrentTime + forM_ userServers $ do + \UserServers {operator, smpServers, xftpServers} -> do + forM_ operator $ \op -> liftIO $ updateOperator currentTs op + overwriteServers currentTs operator smpServers + overwriteServers currentTs operator xftpServers + where + updateOperator :: UTCTime -> ServerOperator -> IO () + updateOperator currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + |] + (enabled, storage, proxy, operatorId, currentTs) + overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [ServerCfg p] -> ExceptT StoreError IO () + overwriteServers currentTs serverOperator servers = + checkConstraint SEUniqueID . ExceptT $ do + case serverOperator of + Nothing -> + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) + Just ServerOperator {operatorId} -> + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) + forM_ servers $ \ServerCfg {server, operator, preset, tested, enabled} -> do + let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, operator, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, operator) :. (preset, tested, enabled, userId, currentTs, currentTs)) + pure $ Right () + where + protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + -- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] -- updateServerOperators_ db operators = do -- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" From 6128a248693d55518819290fc2d7cfbb701684a0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 6 Nov 2024 08:09:02 +0000 Subject: [PATCH 10/22] database entity IDs --- src/Simplex/Chat.hs | 2 +- src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Operators.hs | 211 +++++++++++++++++++++-------- src/Simplex/Chat/Store/Profiles.hs | 17 ++- 4 files changed, 165 insertions(+), 67 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 60feda2a66..83649712a7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -191,7 +191,7 @@ defaultChatConfig = smpServers = xyzSMPServers, useSMP = 3, xftpServers = xyzXFTPServers, - useXFTP = 3, + useXFTP = 3 } ], ntf = _defaultNtfServers, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8fe5de6a9b..2597a685dd 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -85,7 +85,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 8126a91de4..b8a97c00ea 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -1,10 +1,15 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} module Simplex.Chat.Operators where @@ -17,12 +22,15 @@ import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Foldable1 (fold1) import Data.Int (Int64) +import Data.Kind (Type) import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isNothing) +import Data.Set (Set) +import qualified Data.Set as S import Data.Text (Text) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) @@ -49,8 +57,32 @@ usageConditionsText = in [| stripFrontMatter (safeDecodeUtf8 $(lift s)) |] ) +data EntityStored = ESStored | ESNew + +data SEntityStored (s :: EntityStored) where + SESStored :: SEntityStored 'ESStored + SESNew :: SEntityStored 'ESNew + +data DBEntityId' (s :: EntityStored) where + DBEntityId :: Int64 -> DBEntityId' 'ESStored + NewDBEntity :: DBEntityId' 'ESNew + +deriving instance Show (DBEntityId' s) + +type DBEntityId = DBEntityId' 'ESStored + +type NewDBEntity = DBEntityId' 'ESNew + +data ADBEntityId = forall s. AEI (SEntityStored s) (DBEntityId' s) + +pattern ADBEntityId :: Int64 -> ADBEntityId +pattern ADBEntityId i = AEI SESStored (DBEntityId i) + +pattern ANewDBEntity :: ADBEntityId +pattern ANewDBEntity = AEI SESNew NewDBEntity + data OperatorTag = OTSimplex | OTXyz - deriving (Eq, Show) + deriving (Eq, Ord, Show) instance FromField OperatorTag where fromField = fromTextField_ textDecode @@ -72,6 +104,12 @@ instance TextEncoding OperatorTag where OTSimplex -> "simplex" OTXyz -> "xyz" +-- this and other types only define instances of serialization for known DB IDs only, +-- entities without IDs cannot be serialized to JSON +instance FromField DBEntityId where fromField f = DBEntityId <$> fromField f + +instance ToField DBEntityId where toField (DBEntityId i) = toField i + data UsageConditions = UsageConditions { conditionsId :: Int64, conditionsCommit :: Text, @@ -115,8 +153,14 @@ data ConditionsAcceptance | CARequired {deadline :: Maybe UTCTime} deriving (Show) -data ServerOperator = ServerOperator - { operatorId :: Maybe OperatorId, +type ServerOperator = ServerOperator' DBEntityId + +type NewServerOperator = ServerOperator' NewDBEntity + +type AServerOperator = ServerOperator' ADBEntityId + +data ServerOperator' s = ServerOperator + { operatorId :: s, operatorTag :: Maybe OperatorTag, appVendor :: Bool, tradeName :: Text, @@ -128,6 +172,9 @@ data ServerOperator = ServerOperator } deriving (Show) +aServerOperator :: ServerOperator -> AServerOperator +aServerOperator op@ServerOperator {operatorId = DBEntityId opId} = op {operatorId = ADBEntityId opId} + conditionsAccepted :: ServerOperator -> Bool conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of CAAccepted {} -> True @@ -140,15 +187,25 @@ data OperatorEnabled = OperatorEnabled } deriving (Show) -data UserServers = UserServers - { operator :: Maybe ServerOperator, - smpServers :: [UserServer 'PSMP], - xftpServers :: [UserServer 'PXFTP] +type UserServers = UserServers' DBEntityId + +type AUserServers = UserServers' ADBEntityId + +data UserServers' s = UserServers + { operator :: Maybe (ServerOperator' s), + smpServers :: [UserServer' s 'PSMP], + xftpServers :: [UserServer' s 'PXFTP] } deriving (Show) -data UserServer p = UserServer - { serverId :: Maybe Int64, +type UserServer p = UserServer' DBEntityId p + +type NewUserServer p = UserServer' NewDBEntity p + +type AUserServer p = UserServer' ADBEntityId p + +data UserServer' s p = UserServer + { serverId :: s, serverOperatorId :: Maybe OperatorId, server :: ProtoServerWithAuth p, tested :: Maybe Bool, @@ -157,7 +214,7 @@ data UserServer p = UserServer deriving (Show) data PresetOperatorServers = PresetOperatorServers - { operator :: ServerOperator, + { operator :: NewServerOperator, presetSMPServers :: NonEmpty (PresetServer 'PSMP), presetXFTPServers :: NonEmpty (PresetServer 'PXFTP), useSMP :: Int, @@ -190,56 +247,78 @@ usageConditionsToAdd newUser prevCommit sourceCommit createdAt = \case -- This function should be used inside DB transaction to update operators. -- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, -- and preserves custom operators without tags for forward compatibility. -updatedServerOperators :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [ServerOperator] +updatedServerOperators :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [AServerOperator] updatedServerOperators presetSrvs storedOps = - foldr addPreset [] presetSrvs <> filter (isNothing . operatorTag) storedOps -- TODO remove domains of preset operators from custom + foldr addPreset [] presetSrvs + <> map aServerOperator (filter (isNothing . operatorTag) storedOps) -- TODO remove domains of preset operators from custom where addPreset PresetOperatorServers {operator = presetOp} = (storedOp' :) where storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of - Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> - presetOp {operatorId, conditionsAcceptance, enabled, roles} - Nothing -> presetOp - - -data UpdatedServers p = UpdatedServers - { toUpsert :: [UserServer p], - toDelete :: [Int64] - } + Just ServerOperator {operatorId = DBEntityId opId, conditionsAcceptance, enabled, roles} -> + presetOp {operatorId = ADBEntityId opId, conditionsAcceptance, enabled, roles} + Nothing -> presetOp {operatorId = ANewDBEntity} -- This function should be used inside DB transaction to update servers. -- It assumes that the list of operators was amended using updatedServerOperators, -- that [ServerOperator] has the same operators as [PresetOperatorServers], -- and that they all have serverOperatorId set. -updatedUserServers :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> Either String ([UserServer 'PSMP], [UserServer 'PXFTP]) -updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do - smpSrvs' <- updatedSrvs useSMP smpSrvs =<< presetSrvsToStore presetSMPServers - xftpSrvs' <- updatedSrvs useXFTP xftpSrvs =<< presetSrvsToStore presetXFTPServers - pure (smpSrvs', xftpSrvs') +-- +-- presets -> stored or user-supplied servers, possibly with incorrect operators +updatedUserServers' :: NonEmpty PresetOperatorServers -> [UserServers] -> ([AUserServers], NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) +updatedUserServers' presetSrvs storedSrvs = (userServers, agentSMPServers, agentXFTPServers) where - presetSrvsToStore :: forall p. (PresetOperatorServers -> NonEmpty (PresetServer p)) -> Either String (NonEmpty (Bool, UserServer p)) - presetSrvsToStore presetSel = fold1 <$> mapM operatorSrvs presetSrvs - where - operatorSrvs :: PresetOperatorServers -> Either String (NonEmpty (Bool, UserServer p)) - operatorSrvs op@PresetOperatorServers {operator} = case find ((operatorTag operator ==) . operatorTag) storedOps of - Nothing -> Left "preset operator not stored" - Just op' -> Right $ L.map (userSrv op') (presetSel op) - userSrv op PresetServer {server, useServer} = - let srv = UserServer {serverId = Nothing, serverOperatorId = operatorId op, server, tested = Nothing, enabled = False} - in (useServer, srv) + userServers = undefined + agentSMPServers = undefined + agentXFTPServers = undefined + -- make set of known tags of preset operators + knownPresetOps :: Set (Maybe OperatorTag) + knownPresetOps = foldl' (\s PresetOperatorServers {operator} -> S.insert (operatorTag operator) s) S.empty presetSrvs - updatedSrvs :: forall p. (PresetOperatorServers -> Int) -> [UserServer p] -> NonEmpty (Bool, UserServer p) -> Either String [UserServer p] - updatedSrvs useSel storedSrvs presetSrvs = - fmap enabledSrvs . addOtherServers =<< foldM updatedSrv (storedSrvs', []) presetSrvs - where - storedSrvs' :: Map (ProtoServerWithAuth p) (UserServer p) - storedSrvs' = foldl' (\m us@UserServer {server} -> M.insert server us m) M.empty storedSrvs - updatedSrv :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> (Bool, UserServer p) -> Either String (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) - updatedSrv srvs srv = undefined - addOtherServers :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> Either String [(Bool, UserServer p)] - addOtherServers = undefined - enabledSrvs :: [(Bool, UserServer p)] -> [UserServer p] - enabledSrvs = undefined + -- make map domain -> operator + -- storedSrvs: + -- - remove preset operators with tags not present in presets) + -- - flatten + -- - set correct operators based on domains + -- - split servers to with/without preset operators + -- - make Map (protoserver, stored server record) from servers with preset operators + -- presetSrvs: flatten, update using map above, prepare agent servers, reassemble to userServers + -- add other operators and servers without operator + -- + -- (storedPresets, storedOthers) = partition (isJust . operatorTag . operator) storedSrvs + -- (storedOthersKeep, storeOthersPresets) + -- userServers = foldr addOther (foldr addPreset [] presetSrvs) storedOthers + + +-- updatedUserServers :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> Either String ([UserServer 'PSMP], [UserServer 'PXFTP]) +-- updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do +-- smpSrvs' <- updatedSrvs useSMP smpSrvs =<< presetSrvsToStore presetSMPServers +-- xftpSrvs' <- updatedSrvs useXFTP xftpSrvs =<< presetSrvsToStore presetXFTPServers +-- pure (smpSrvs', xftpSrvs') +-- where +-- presetSrvsToStore :: forall p. (PresetOperatorServers -> NonEmpty (PresetServer p)) -> Either String (NonEmpty (Bool, UserServer p)) +-- presetSrvsToStore presetSel = fold1 <$> mapM operatorSrvs presetSrvs +-- where +-- operatorSrvs :: PresetOperatorServers -> Either String (NonEmpty (Bool, UserServer p)) +-- operatorSrvs op@PresetOperatorServers {operator} = case find ((operatorTag operator ==) . operatorTag) storedOps of +-- Nothing -> Left "preset operator not stored" +-- Just op' -> Right $ L.map (userSrv op') (presetSel op) +-- userSrv op PresetServer {server, useServer} = +-- let srv = UserServer {serverId = Nothing, serverOperatorId = operatorId op, server, tested = Nothing, enabled = False} +-- in (useServer, srv) + +-- updatedSrvs :: forall p. (PresetOperatorServers -> Int) -> [UserServer p] -> NonEmpty (Bool, UserServer p) -> Either String [UserServer p] +-- updatedSrvs useSel storedSrvs presetSrvs = +-- fmap enabledSrvs . addOtherServers =<< foldM updatedSrv (storedSrvs', []) presetSrvs +-- where +-- storedSrvs' :: Map (ProtoServerWithAuth p) (UserServer p) +-- storedSrvs' = foldl' (\m us@UserServer {server} -> M.insert server us m) M.empty storedSrvs +-- updatedSrv :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> (Bool, UserServer p) -> Either String (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) +-- updatedSrv srvs srv = undefined +-- addOtherServers :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> Either String [(Bool, UserServer p)] +-- addOtherServers = undefined +-- enabledSrvs :: [(Bool, UserServer p)] -> [UserServer p] +-- enabledSrvs = undefined -- addSrv srv@ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} uss = -- case find (\us -> any [\h -> any (\d -> d `T.isSuffixOf` ) serverDomains (operator us)] host) uss of @@ -254,11 +333,10 @@ updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do -- groupByOperator srvOperators smpSrvs xftpSrvs = -- map createOperatorServers (M.toList combinedMap) -- where --- srvOperatorId ServerCfg {operator} = operator --- opId ServerOperator {operatorId} = operatorId --- operatorMap :: Map (Maybe Int64) (Maybe ServerOperator) --- operatorMap = M.fromList [(Just (opId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing --- initialMap :: Map (Maybe Int64) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) +-- srvOperatorId ServerCfg {operator} = DBEntityId <$> operator +-- operatorMap :: Map (Maybe DBEntityId) (Maybe ServerOperator) +-- operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing +-- initialMap :: Map (Maybe DBEntityId) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) -- initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] -- smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs -- combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs @@ -290,21 +368,38 @@ validateUserServers userServers = Just ServerOperator {roles} -> roleSel roles Nothing -> not (null smpServers) && not (null xftpServers) +instance ToJSON DBEntityId where + toEncoding (DBEntityId i) = toEncoding i + toJSON (DBEntityId i) = toJSON i + +instance FromJSON DBEntityId where + parseJSON v = DBEntityId <$> parseJSON v + $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) -$(JQ.deriveJSON defaultJSON ''ServerOperator) +instance ToJSON ServerOperator where + toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator') + toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator') + +instance FromJSON ServerOperator where + parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator') $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) instance ProtocolTypeI p => ToJSON (UserServer p) where - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer) - toJSON = $(JQ.mkToJSON defaultJSON ''UserServer) + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') + toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') instance ProtocolTypeI p => FromJSON (UserServer p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer) + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') -$(JQ.deriveJSON defaultJSON ''UserServers) +instance ToJSON UserServers where + toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServers') + toJSON = $(JQ.mkToJSON defaultJSON ''UserServers') + +instance FromJSON UserServers where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServers') $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index f4f574c3d7..567f294d32 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -596,7 +597,7 @@ getServerOperators db = do UTCTime -> UsageConditions -> Maybe UsageConditions -> - ( (OperatorId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) + ( (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) :. (Maybe Text, Maybe UTCTime) ) -> ServerOperator @@ -628,15 +629,16 @@ getServerOperators db = do CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) CARequired Nothing - in ServerOperator {operatorId, operatorTag, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} + in ServerOperator {operatorId, operatorTag, appVendor = False, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} + -- TODO appVendor setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) setServerOperators db operatorsEnabled = do - liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> + liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId', enabled', roles' = ServerRoles {storage, proxy}} -> DB.execute db "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" - (enabled, storage, proxy, operatorId) + (enabled', storage, proxy, operatorId') getServerOperators db getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions @@ -727,7 +729,7 @@ setUserServers db User {userId} userServers = do WHERE server_operator_id = ? |] (enabled, storage, proxy, operatorId, currentTs) - overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [ServerCfg p] -> ExceptT StoreError IO () + overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [UserServer p] -> ExceptT StoreError IO () overwriteServers currentTs serverOperator servers = checkConstraint SEUniqueID . ExceptT $ do case serverOperator of @@ -735,7 +737,7 @@ setUserServers db User {userId} userServers = do DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) Just ServerOperator {operatorId} -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) - forM_ servers $ \ServerCfg {server, operator, preset, tested, enabled} -> do + forM_ servers $ \UserServer {server, serverOperatorId, tested, enabled} -> do let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server DB.execute db @@ -744,7 +746,8 @@ setUserServers db User {userId} userServers = do (protocol, host, port, key_hash, basic_auth, operator, preset, tested, enabled, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, operator) :. (preset, tested, enabled, userId, currentTs, currentTs)) + ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, serverOperatorId) :. (False, tested, enabled, userId, currentTs, currentTs)) + -- take preset from operator pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p From 8396e70e7b82b111f2d2e156d10a92dea4883319 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:13:08 +0400 Subject: [PATCH 11/22] core: validate servers - find servers with duplicate hosts (#5150) --- src/Simplex/Chat/Operators.hs | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 5e32807ddc..cedc3ca6d1 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -14,6 +14,7 @@ import qualified Data.Aeson.TH as JQ import Data.FileEmbed import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) +import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isNothing) @@ -28,7 +29,7 @@ import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), SProtocolType (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -159,23 +160,34 @@ groupByOperator srvOperators smpSrvs xftpSrvs = data UserServersError = USEStorageMissing | USEProxyMissing - | USEDuplicate {server :: AProtoServerWithAuth} + | USEDuplicateSMP {server :: AProtoServerWithAuth} + | USEDuplicateXFTP {server :: AProtoServerWithAuth} deriving (Show) validateUserServers :: NonEmpty UserServers -> [UserServersError] validateUserServers userServers = let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] - -- TODO duplicate errors - -- allSMPServers = - -- map (\ServerCfg {server} -> server) $ - -- concatMap (\UserServers {smpServers} -> smpServers) userServers - in storageMissing_ <> proxyMissing_ -- <> duplicateErrors + + allSMPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {smpServers} -> smpServers) userServers + duplicateSMPServers = findDuplicatesByHost allSMPServers + duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers + + allXFTPServers = map (\ServerCfg {server} -> server) $ concatMap (\UserServers {xftpServers} -> xftpServers) userServers + duplicateXFTPServers = findDuplicatesByHost allXFTPServers + duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers + in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors where canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of Just ServerOperator {roles} -> roleSel roles Nothing -> not (null smpServers) && not (null xftpServers) + findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p] + findDuplicatesByHost servers = + let allHosts = concatMap (L.toList . host . protoServer) servers + hostCounts = M.fromListWith (+) [(host, 1 :: Int) | host <- allHosts] + duplicateHosts = M.keys $ M.filter (> 1) hostCounts + in filter (\srv -> any (`elem` duplicateHosts) (L.toList $ host . protoServer $ srv)) servers $(JQ.deriveJSON defaultJSON ''UsageConditions) From ef0f21a11c082c9f3b9b5bc77d2d70c6bd9ee5ce Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 8 Nov 2024 14:45:00 +0400 Subject: [PATCH 12/22] core: operator apis commands (#5155) --- src/Simplex/Chat.hs | 8 ++++++++ src/Simplex/Chat/Operators.hs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 69b78ba9d4..dad3a6f813 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -8140,6 +8140,14 @@ chatCommandP = "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), "/smp" $> GetUserProtoServers (AProtocolType SPSMP), "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), + "/_operators" $> APIGetServerOperators, + "/_operators " *> (APISetServerOperators <$> jsonP), + "/_user_servers " *> (APIGetUserServers <$> A.decimal), + "/_user_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), + "/_validate_servers " *> (APIValidateServers <$> jsonP), + "/_conditions" $> APIGetUsageConditions, + "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), + "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <* A.space <*> jsonP), "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index cedc3ca6d1..b3f92caaf9 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -195,6 +195,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) $(JQ.deriveJSON defaultJSON ''ServerOperator) +$(JQ.deriveJSON defaultJSON ''OperatorEnabled) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) $(JQ.deriveJSON defaultJSON ''UserServers) From 28105038d47601a4df4694b74dc395328b2304a5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sat, 9 Nov 2024 23:53:18 +0000 Subject: [PATCH 13/22] preset operators and servers (compiles) --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 271 ++++++------ src/Simplex/Chat/Controller.hs | 89 ++-- .../Migrations/M20241027_server_operators.hs | 18 +- src/Simplex/Chat/Operators.hs | 300 +++++++------- src/Simplex/Chat/Stats.hs | 3 +- src/Simplex/Chat/Store/Profiles.hs | 385 ++++++++++-------- src/Simplex/Chat/Terminal.hs | 37 +- src/Simplex/Chat/Terminal/Main.hs | 4 +- src/Simplex/Chat/View.hs | 59 ++- 11 files changed, 580 insertions(+), 590 deletions(-) diff --git a/cabal.project b/cabal.project index 61ce04a569..74f944c37d 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: ff05a465ee15ac7ae2c14a9fb703a18564950631 + tag: 93f30c8edf9243ad2291dd6427d87328e282560a source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3e0f103641..bd2602c1b6 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."ff05a465ee15ac7ae2c14a9fb703a18564950631" = "1gv4nwqzbqkj7y3ffkiwkr4qwv52vdzppsds5vsfqaayl14rzmgp"; + "https://github.com/simplex-chat/simplexmq.git"."93f30c8edf9243ad2291dd6427d87328e282560a" = "1zf0sp9dy6kz4zvyz6mdgmhydps7khcq84n30irp983w1xh7gzs7"; "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/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 72653ef4ad..e899a73eeb 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -6,6 +6,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RankNTypes #-} @@ -98,7 +99,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -138,30 +139,28 @@ import qualified UnliftIO.Exception as E import UnliftIO.IO (hClose, hSeek, hTell, openFile) import UnliftIO.STM -operatorSimpleXChat :: ServerOperator +operatorSimpleXChat :: NewServerOperator operatorSimpleXChat = ServerOperator - { operatorId = Nothing, + { operatorId = DBNewEntity, operatorTag = Just OTSimplex, - appVendor = True, tradeName = "SimpleX Chat", legalName = Just "SimpleX Chat Ltd", serverDomains = ["simplex.im"], - acceptedConditions = CARequired Nothing, + conditionsAcceptance = CARequired Nothing, enabled = True, roles = allRoles } -operatorXYZ :: ServerOperator +operatorXYZ :: NewServerOperator operatorXYZ = ServerOperator - { operatorId = Nothing, + { operatorId = DBNewEntity, operatorTag = Just OTXyz, - appVendor = False, tradeName = "XYZ", legalName = Just "XYZ Ltd", serverDomains = ["xyz.com"], - acceptedConditions = CARequired Nothing, + conditionsAcceptance = CARequired Nothing, enabled = False, roles = ServerRoles {storage = False, proxy = True} } @@ -179,18 +178,18 @@ defaultChatConfig = presetServers = PresetServers { operators = - [ PresetOperatorServers + [ PresetOperator { operator = operatorSimpleXChat, - smpServers = simplexChatSMPServers, + smp = simplexChatSMPServers, useSMP = 4, - xftpServers = L.map (PresetServer True) defaultXFTPServers, + xftp = L.map (presetServer True) defaultXFTPServers, useXFTP = 3 }, - PresetOperatorServers + PresetOperator { operator = operatorXYZ, - smpServers = xyzSMPServers, + smp = xyzSMPServers, useSMP = 3, - xftpServers = xyzXFTPServers, + xftp = xyzXFTPServers, useXFTP = 3 } ], @@ -218,10 +217,10 @@ defaultChatConfig = chatHooks = defaultChatHooks } -simplexChatSMPServers :: NonEmpty (PresetServer 'PSMP) -simplexChatSMPServers = +simplexChatSMPServers :: NonEmpty (NewUserServer 'PSMP) +simplexChatSMPServers = L.map - (PresetServer True) + (presetServer True) [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", @@ -235,16 +234,16 @@ simplexChatSMPServers = "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" ] <> L.map - (PresetServer False) + (presetServer False) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] -xyzSMPServers :: NonEmpty (PresetServer 'PSMP) +xyzSMPServers :: NonEmpty (NewUserServer 'PSMP) xyzSMPServers = L.map - (PresetServer True) + (presetServer True) [ "smp://abcd@smp1.xyz.com", "smp://abcd@smp2.xyz.com", "smp://abcd@smp3.xyz.com", @@ -253,10 +252,10 @@ xyzSMPServers = "smp://abcd@smp6.xyz.com" ] -xyzXFTPServers :: NonEmpty (PresetServer 'PXFTP) +xyzXFTPServers :: NonEmpty (NewUserServer 'PXFTP) xyzXFTPServers = L.map - (PresetServer True) + (presetServer True) [ "xftp://abcd@xftp1.xyz.com", "xftp://abcd@xftp2.xyz.com", "xftp://abcd@xftp3.xyz.com", @@ -300,16 +299,16 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo newChatController ChatDatabase {chatStore, agentStore} user - cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations} + cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do 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, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user currentRemoteHost <- newTVarIO Nothing - servers <- agentServers config + servers <- withTransaction chatStore agentServers smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom @@ -382,28 +381,24 @@ newChatController contactMergeEnabled } where - configServers :: DefaultAgentServers - configServers = - let DefaultAgentServers {smp = defSmp, xftp = defXftp, netCfg} = defaultServers - smp' = maybe defSmp (L.map enabledServerCfg) (nonEmpty smpServers) - xftp' = maybe defXftp (L.map enabledServerCfg) (nonEmpty xftpServers) - in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig netCfg simpleNetCfg} - agentServers :: ChatConfig -> IO InitialAgentServers - agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do - users <- withTransaction chatStore getUsers - smp' <- getUserServers users SPSMP - xftp' <- getUserServers users SPXFTP + PresetServers {operators = presetOps, ntf, netCfg} = presetServers + agentServers :: DB.Connection -> IO InitialAgentServers + agentServers db = do + users <- getUsers db + opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) + smp' <- getUserServers SPSMP users opDomains smpServers + xftp' <- getUserServers SPXFTP users opDomains xftpServers pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers users protocol = case users of - [] -> pure $ M.fromList [(1, cfgServers protocol defServers)] - _ -> M.fromList <$> initialServers + getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> [ProtoServerWithAuth p] -> IO (Map UserId (NonEmpty (ServerCfg p))) + getUserServers protocol users opDomains = maybe get srvCfgs . L.nonEmpty where - initialServers :: IO [(UserId, NonEmpty (ServerCfg p))] - initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users - userServers :: User -> IO (NonEmpty (ServerCfg p)) - userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') + get = do + randomSrvs <- randomPresetServers presetOps + fmap M.fromList $ forM users $ \u -> + (aUserId u,) . useServers opDomains <$> getUpdateUserServers db presetOps randomSrvs u + srvCfgs ss = pure $ M.fromList $ map (\u -> (aUserId u, L.map srvCfg ss)) users + srvCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -446,34 +441,6 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} -useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ServerCfg p) -useServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) . nonEmpty - -randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p]) -randomServers p ChatConfig {defaultServers} = do - let srvs = cfgServers p defaultServers - (enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs - toUse = cfgServersToUse p defaultServers - if length enbldSrvs <= toUse - then pure (srvs, []) - else do - (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs - let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable - srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs - pure (fromMaybe srvs $ L.nonEmpty srvs', srvs') - where - server' ServerCfg {server = ProtoServerWithAuth srv _} = srv - -cfgServers :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> NonEmpty (ServerCfg p) -cfgServers p DefaultAgentServers {smp, xftp} = case p of - SPSMP -> smp - SPXFTP -> xftp - -cfgServersToUse :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> Int -cfgServersToUse p DefaultAgentServers {useSMP, useXFTP} = case p of - SPSMP -> useSMP - SPXFTP -> useXFTP - -- enableSndFiles has no effect when mainApp is True startChatController :: Bool -> Bool -> CM' (Async ()) startChatController mainApp enableSndFiles = do @@ -616,8 +583,9 @@ processChatCommand' vr = \case forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser - (smp, smpServers) <- chooseServers SPSMP - (xftp, xftpServers) <- chooseServers SPXFTP + opDomains <- operatorDomains . fst <$> withFastStore getServerOperators + (smp, smpServers_) <- chooseServers SPSMP opDomains + (xftp, xftpServers_) <- chooseServers SPXFTP opDomains users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ @@ -626,9 +594,10 @@ processChatCommand' vr = \case ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts createPresetContactCards user `catchChatError` \_ -> pure () - withFastStore $ \db -> createNoteFolder db user - storeServers user smpServers - storeServers user xftpServers + withFastStore $ \db -> do + createNoteFolder db user + liftIO $ mapM_ (mapM_ (insertProtocolServer db user ts)) smpServers_ + liftIO $ mapM_ (mapM_ (insertProtocolServer db user ts)) xftpServers_ atomically . writeTVar u $ Just user pure $ CRActiveUser user where @@ -637,18 +606,15 @@ processChatCommand' vr = \case withFastStore $ \db -> do createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile - chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ServerCfg p), [ServerCfg p]) - chooseServers protocol = - asks currentUser >>= readTVarIO >>= \case - Nothing -> asks config >>= liftIO . randomServers protocol - Just user -> chosenServers =<< withFastStore' (`getProtocolServers` user) - where - chosenServers servers = do - cfg <- asks config - pure (useServers cfg protocol servers, servers) - storeServers user servers = - unless (null servers) . withFastStore $ - \db -> overwriteProtocolServers db user servers + chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [(Text, ServerOperator)] -> CM (NonEmpty (ServerCfg p), Maybe (NonEmpty (NewUserServer p))) + chooseServers protocol opDomains = do + PresetServers {operators = presetOps} <- asks $ presetServers . config + randomSrvs <- liftIO $ randomPresetServers presetOps + chatReadVar currentUser >>= \case + Nothing -> pure (useServers opDomains randomSrvs, Just randomSrvs) + Just user -> do + srvs <- withFastStore' $ \db -> getUpdateUserServers db presetOps randomSrvs user + pure (useServers opDomains srvs, Nothing) coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1546,43 +1512,39 @@ processChatCommand' vr = \case msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs pure $ CRConnNtfMessages ntfMsgs - APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do - cfg@ChatConfig {defaultServers} <- asks config - srvs <- withFastStore' (`getProtocolServers` user) - (operators, _) <- withFastStore $ \db -> getServerOperators db - let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) - pure $ CRUserProtoServers {user, servers, operators} - GetUserProtoServers aProtocol -> withUser $ \User {userId} -> - processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) - | null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do - withFastStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers - ok user - | otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled" - SetUserProtoServers serversConfig -> withUser $ \User {userId} -> - processChatCommand $ APISetUserProtoServers userId serversConfig + -- APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do + -- cfg@ChatConfig {defaultServers} <- asks config + -- srvs <- withFastStore' (`getProtocolServers` user) + -- (operators, _) <- withFastStore $ \db -> getServerOperators db + -- let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) + -- pure $ CRUserProtoServers {user, servers, operators} + -- GetUserProtoServers aProtocol -> withUser $ \User {userId} -> + -- processChatCommand $ APIGetUserProtoServers userId aProtocol + -- APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) + -- | null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do + -- withFastStore $ \db -> overwriteProtocolServers db user servers + -- cfg <- asks config + -- lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers + -- ok user + -- | otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled" + -- SetUserProtoServers serversConfig -> withUser $ \User {userId} -> + -- processChatCommand $ APISetUserProtoServers userId serversConfig APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> processChatCommand $ APITestProtoServer userId srv - APIGetServerOperators -> do - (operators, conditionsAction) <- withFastStore $ \db -> getServerOperators db - pure $ CRServerOperators operators conditionsAction - APISetServerOperators operatorsEnabled -> do - (operators, conditionsAction) <- withFastStore $ \db -> setServerOperators db operatorsEnabled - pure $ CRServerOperators operators conditionsAction - APIGetUserServers userId -> withUserId userId $ \user -> do - (operators, smpServers, xftpServers) <- withFastStore $ \db -> do - (operators, _) <- getServerOperators db - smpServers <- liftIO $ getServers db user SPSMP - xftpServers <- liftIO $ getServers db user SPXFTP - pure (operators, smpServers, xftpServers) - let userServers = groupByOperator operators smpServers xftpServers - pure $ CRUserServers user userServers + APIGetServerOperators -> uncurry CRServerOperators <$> withFastStore getServerOperators + APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do + liftIO $ setServerOperators db operatorsEnabled + uncurry CRServerOperators <$> getServerOperators db + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do + (operators, _) <- getServerOperators db + liftIO $ do + smpServers <- getServers db user SPSMP + xftpServers <- getServers db user SPXFTP + CRUserServers user <$> groupByOperator operators smpServers xftpServers where - getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ServerCfg p] + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [UserServer p] getServers db user _p = getProtocolServers db user APISetUserServers userId userServers -> withUserId userId $ \user -> do let errors = validateUserServers userServers @@ -1596,7 +1558,7 @@ processChatCommand' vr = \case APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db - acceptedConditions <- getLatestAcceptedConditions db + acceptedConditions <- liftIO $ getLatestAcceptedConditions db pure (usageConditions, acceptedConditions) -- TODO if db commit is different from source commit, conditionsText should be nothing in response pure @@ -1609,10 +1571,12 @@ processChatCommand' vr = \case currentTs <- liftIO getCurrentTime withFastStore' $ \db -> setConditionsNotified db conditionsId currentTs ok_ - APIAcceptConditions conditionsId operators -> do + -- TODO switch to IDs + APIAcceptConditions conditionsId operators -> withFastStore $ \db -> do currentTs <- liftIO getCurrentTime - (operators', conditionsAction) <- withFastStore $ \db -> acceptConditions db conditionsId operators currentTs - pure $ CRServerOperators operators' conditionsAction + operators' <- L.toList <$> acceptConditions db conditionsId operators currentTs + currentConds <- getCurrentUsageConditions db + pure $ CRServerOperators operators' $ usageConditionsAction operators' currentConds currentTs APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do @@ -1866,7 +1830,7 @@ processChatCommand' vr = \case let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q cfg <- asks config - newUserServers <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (`getProtocolServers` newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2200,7 +2164,7 @@ processChatCommand' vr = \case where changeMemberRole user gInfo members m gEvent = do let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m - assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] + assertUserGroupRole gInfo $ maximum ([GRAdmin, mRole, memRole] :: [GroupMemberRole]) withGroupLock "memberRole" groupId . procCmd $ do unless (mRole == memRole) $ do withFastStore' $ \db -> updateGroupMemberRole db user m memRole @@ -2600,12 +2564,12 @@ processChatCommand' vr = \case agentServersSummary <- lift $ withAgent' getAgentServersSummary cfg <- asks config (users, smpServers, xftpServers) <- - withStore' $ \db -> (,,) <$> getUsers db <*> getServers db cfg user SPSMP <*> getServers db cfg user SPXFTP + withStore' $ \db -> (,,) <$> getUsers db <*> getServers db user SPSMP <*> getServers db user SPXFTP let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers pure $ CRAgentServersSummary user presentedServersSummary where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> ChatConfig -> User -> SProtocolType p -> IO (NonEmpty (ProtocolServer p)) - getServers db cfg user p = L.map (\ServerCfg {server} -> protoServer server) . useServers cfg p <$> getProtocolServers db user + getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] + getServers db user _p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3723,8 +3687,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - cfg <- asks config - knownSrvs <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) + knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (`getProtocolServers` user) pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -3936,7 +3899,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do (sftConns, sfts) <- getSndFileTransferConns (rftConns, rfts) <- getRcvFileTransferConns (pcConns, pcs) <- getPendingContactConns - let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] + let conns = concat ([ctConns, ucConns, mConns, sftConns, rftConns, pcConns] :: [[ConnId]]) pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) -- subscribe using batched commands rs <- withAgent $ \a -> agentBatchSubscribe a conns @@ -4744,7 +4707,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ctItem = AChatItem SCTDirect SMDSnd (DirectChat ct) SWITCH qd phase cStats -> do toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of + when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing QDSnd -> createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing RSYNC rss cryptoErr_ cStats -> @@ -5155,7 +5118,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) - when (phase `elem` [SPStarted, SPCompleted]) $ case qd of + when (phase == SPStarted || phase == SPCompleted) $ case qd of QDRcv -> createInternalChatItem user (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing QDSnd -> createInternalChatItem user (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing RSYNC rss cryptoErr_ cStats -> @@ -6719,15 +6682,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageWarning "x.grp.mem.con: neither member is invitee" where inviteeXGrpMemCon :: GroupMemberIntro -> CM () - inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroReConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroToConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroToConnected + inviteeXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroReConnected -> updateStatus introId GMIntroConnected + GMIntroToConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroToConnected forwardMemberXGrpMemCon :: GroupMemberIntro -> CM () - forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} - | introStatus == GMIntroToConnected = updateStatus introId GMIntroConnected - | introStatus `elem` [GMIntroReConnected, GMIntroConnected] = pure () - | otherwise = updateStatus introId GMIntroReConnected + forwardMemberXGrpMemCon GroupMemberIntro {introId, introStatus} = case introStatus of + GMIntroToConnected -> updateStatus introId GMIntroConnected + GMIntroReConnected -> pure () + GMIntroConnected -> pure () + _ -> updateStatus introId GMIntroReConnected updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> RcvMessage -> UTCTime -> CM () @@ -8192,14 +8157,14 @@ chatCommandP = "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), - "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), - "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), - "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), - "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), - "/smp" $> GetUserProtoServers (AProtocolType SPSMP), - "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), + -- "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), + -- "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), + -- "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), + -- "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), + -- "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), + -- "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), + -- "/smp" $> GetUserProtoServers (AProtocolType SPSMP), + -- "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, "/_operators " *> (APISetServerOperators <$> jsonP), "/_user_servers " *> (APIGetUserServers <$> A.decimal), @@ -8549,7 +8514,7 @@ chatCommandP = onOffP (Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) (pure Nothing) - srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP) + -- srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP) rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') char_ = optional . A.char diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2597a685dd..d974881753 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -35,7 +35,6 @@ import qualified Data.ByteArray as BA import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (ord) -import Data.Constraint (Dict (..)) import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) @@ -71,7 +70,7 @@ import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) @@ -85,7 +84,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -174,7 +173,7 @@ defaultChatHooks = } data PresetServers = PresetServers - { operators :: NonEmpty PresetOperatorServers, + { operators :: NonEmpty PresetOperator, ntf :: [NtfServer], netCfg :: NetworkConfig } @@ -344,20 +343,20 @@ data ChatCommand | APIGetGroupLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} - | APIGetUserProtoServers UserId AProtocolType - | GetUserProtoServers AProtocolType - | APISetUserProtoServers UserId AProtoServersConfig - | SetUserProtoServers AProtoServersConfig - | APITestProtoServer UserId AProtoServerWithAuth + | -- | APIGetUserProtoServers UserId AProtocolType + -- | GetUserProtoServers AProtocolType + -- | APISetUserProtoServers UserId AProtoServersConfig + -- | SetUserProtoServers AProtoServersConfig + APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators | APISetServerOperators (NonEmpty OperatorEnabled) | APIGetUserServers UserId - | APISetUserServers UserId (NonEmpty UserServers) - | APIValidateServers (NonEmpty UserServers) -- response is CRUserServersValidation + | APISetUserServers UserId (NonEmpty UserOperatorServers) + | APIValidateServers (NonEmpty UserOperatorServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 - | APIAcceptConditions Int64 (NonEmpty ServerOperator) + | APIAcceptConditions Int64 (NonEmpty ServerOperator) -- TODO replace with IDs | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -583,10 +582,10 @@ data ChatResponse | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} - | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} + | -- | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} + CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} - | CRUserServers {user :: User, userServers :: [UserServers]} + | CRUserServers {user :: User, userServers :: [UserOperatorServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} | CRUsageConditions {usageConditions :: UsageConditions, conditionsText :: Text, acceptedConditions :: Maybe UsageConditions} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} @@ -951,23 +950,23 @@ instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding -data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} - deriving (Show) +-- data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} +-- deriving (Show) -data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) +-- data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) -deriving instance Show AProtoServersConfig +-- deriving instance Show AProtoServersConfig -data UserProtoServers p = UserProtoServers - { serverProtocol :: SProtocolType p, - protoServers :: NonEmpty (ServerCfg p), - presetServers :: NonEmpty (ServerCfg p) - } - deriving (Show) +-- data UserProtoServers p = UserProtoServers +-- { serverProtocol :: SProtocolType p, +-- protoServers :: NonEmpty (ServerCfg p), +-- presetServers :: NonEmpty (ServerCfg p) +-- } +-- deriving (Show) -data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) +-- data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) -deriving instance Show AUserProtoServers +-- deriving instance Show AUserProtoServers data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show) @@ -1570,28 +1569,28 @@ $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) $(JQ.deriveJSON defaultJSON ''SlowSQLQuery) -instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig) +-- instance ProtocolTypeI p => FromJSON (ProtoServersConfig p) where +-- parseJSON = $(JQ.mkParseJSON defaultJSON ''ProtoServersConfig) -instance ProtocolTypeI p => FromJSON (UserProtoServers p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers) +-- instance ProtocolTypeI p => FromJSON (UserProtoServers p) where +-- parseJSON = $(JQ.mkParseJSON defaultJSON ''UserProtoServers) -instance ProtocolTypeI p => ToJSON (UserProtoServers p) where - toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers) - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) +-- instance ProtocolTypeI p => ToJSON (UserProtoServers p) where +-- toJSON = $(JQ.mkToJSON defaultJSON ''UserProtoServers) +-- toEncoding = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) -instance FromJSON AUserProtoServers where - parseJSON v = J.withObject "AUserProtoServers" parse v - where - parse o = do - AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" - case userProtocol p of - Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v - Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p +-- instance FromJSON AUserProtoServers where +-- parseJSON v = J.withObject "AUserProtoServers" parse v +-- where +-- parse o = do +-- AProtocolType (p :: SProtocolType p) <- o .: "serverProtocol" +-- case userProtocol p of +-- Just Dict -> AUPS <$> J.parseJSON @(UserProtoServers p) v +-- Nothing -> fail $ "AUserProtoServers: unsupported protocol " <> show p -instance ToJSON AUserProtoServers where - toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s - toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s +-- instance ToJSON AUserProtoServers where +-- toJSON (AUPS s) = $(JQ.mkToJSON defaultJSON ''UserProtoServers) s +-- toEncoding (AUPS s) = $(JQ.mkToEncoding defaultJSON ''UserProtoServers) s $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "RCS") ''RemoteCtrlSessionState) diff --git a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs index fc0ca21e54..d84cc5aa73 100644 --- a/src/Simplex/Chat/Migrations/M20241027_server_operators.hs +++ b/src/Simplex/Chat/Migrations/M20241027_server_operators.hs @@ -11,7 +11,6 @@ m20241027_server_operators = CREATE TABLE server_operators ( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, - app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, @@ -22,8 +21,6 @@ CREATE TABLE server_operators ( updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -ALTER TABLE protocol_servers ADD COLUMN server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL; - CREATE TABLE usage_conditions ( usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT, conditions_commit TEXT NOT NULL UNIQUE, @@ -41,18 +38,8 @@ CREATE TABLE operator_usage_conditions ( created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers(server_operator_id); CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions(server_operator_id); -CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(server_operator_id, conditions_commit); - -INSERT INTO server_operators - (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) - VALUES (1, 'simplex', 1, 'SimpleX Chat', 'SimpleX Chat Ltd', 'simplex.im', 1); -INSERT INTO server_operators - (server_operator_id, server_operator_tag, app_vendor, trade_name, legal_name, server_domains, enabled) - VALUES (2, 'xyz', 0, 'XYZ', 'XYZ Ltd', 'xyz.com', 0); - --- UPDATE protocol_servers SET server_operator_id = 1 WHERE host LIKE "%.simplex.im" OR host LIKE "%.simplex.im,%"; +CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions(conditions_commit, server_operator_id); |] down_m20241027_server_operators :: Query @@ -60,9 +47,6 @@ down_m20241027_server_operators = [sql| DROP INDEX idx_operator_usage_conditions_conditions_commit; DROP INDEX idx_operator_usage_conditions_server_operator_id; -DROP INDEX idx_protocol_servers_server_operator_id; - -ALTER TABLE protocol_servers DROP COLUMN server_operator_id; DROP TABLE operator_usage_conditions; DROP TABLE usage_conditions; diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index c46505a6a6..ad962d1fdd 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -4,6 +4,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} @@ -20,10 +21,9 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed -import Data.Foldable1 (fold1) +import Data.IORef import Data.Int (Int64) -import Data.Kind (Type) -import Data.List (find, foldl') +import Data.List (find) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -32,6 +32,7 @@ import Data.Maybe (fromMaybe, isNothing) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) +import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) import Database.SQLite.Simple.FromField (FromField (..)) @@ -39,10 +40,11 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) +import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (safeDecodeUtf8) usageConditionsCommit :: Text @@ -57,29 +59,29 @@ usageConditionsText = in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] ) -data EntityStored = ESStored | ESNew +data DBStored = DBStored | DBNew -data SEntityStored (s :: EntityStored) where - SESStored :: SEntityStored 'ESStored - SESNew :: SEntityStored 'ESNew +data SDBStored (s :: DBStored) where + SDBStored :: SDBStored 'DBStored + SDBNew :: SDBStored 'DBNew -data DBEntityId' (s :: EntityStored) where - DBEntityId :: Int64 -> DBEntityId' 'ESStored - NewDBEntity :: DBEntityId' 'ESNew +data DBEntityId' (s :: DBStored) where + DBEntityId :: Int64 -> DBEntityId' 'DBStored + DBNewEntity :: DBEntityId' 'DBNew deriving instance Show (DBEntityId' s) -type DBEntityId = DBEntityId' 'ESStored +type DBEntityId = DBEntityId' 'DBStored -type NewDBEntity = DBEntityId' 'ESNew +type DBNewEntity = DBEntityId' 'DBNew -data ADBEntityId = forall s. AEI (SEntityStored s) (DBEntityId' s) +data ADBEntityId = forall s. AEI (SDBStored s) (DBEntityId' s) pattern ADBEntityId :: Int64 -> ADBEntityId -pattern ADBEntityId i = AEI SESStored (DBEntityId i) +pattern ADBEntityId i = AEI SDBStored (DBEntityId i) -pattern ANewDBEntity :: ADBEntityId -pattern ANewDBEntity = AEI SESNew NewDBEntity +pattern ADBNewEntity :: ADBEntityId +pattern ADBNewEntity = AEI SDBNew DBNewEntity data OperatorTag = OTSimplex | OTXyz deriving (Eq, Ord, Show) @@ -126,18 +128,16 @@ data UsageConditionsAction usageConditionsAction :: [ServerOperator] -> UsageConditions -> UTCTime -> Maybe UsageConditionsAction usageConditionsAction operators UsageConditions {createdAt, notifiedAt} now = do let enabledOperators = filter (\ServerOperator {enabled} -> enabled) operators - if null enabledOperators - then Nothing - else - if all conditionsAccepted enabledOperators - then - let acceptedForOperators = filter conditionsAccepted operators - in Just $ UCAAccepted acceptedForOperators - else - let acceptForOperators = filter (not . conditionsAccepted) enabledOperators - deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) - showNotice = isNothing notifiedAt - in Just $ UCAReview acceptForOperators deadline showNotice + if + | null enabledOperators -> Nothing + | all conditionsAccepted enabledOperators -> + let acceptedForOperators = filter conditionsAccepted operators + in Just $ UCAAccepted acceptedForOperators + | otherwise -> + let acceptForOperators = filter (not . conditionsAccepted) enabledOperators + deadline = conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + showNotice = isNothing notifiedAt + in Just $ UCAReview acceptForOperators deadline showNotice conditionsRequiredOrDeadline :: UTCTime -> UTCTime -> Maybe UTCTime conditionsRequiredOrDeadline createdAt notifiedAtOrNow = @@ -153,16 +153,15 @@ data ConditionsAcceptance | CARequired {deadline :: Maybe UTCTime} deriving (Show) -type ServerOperator = ServerOperator' DBEntityId +type ServerOperator = ServerOperator' 'DBStored -type NewServerOperator = ServerOperator' NewDBEntity +type NewServerOperator = ServerOperator' 'DBNew -type AServerOperator = ServerOperator' ADBEntityId +data AServerOperator = forall s. ASO (SDBStored s) (ServerOperator' s) data ServerOperator' s = ServerOperator - { operatorId :: s, + { operatorId :: DBEntityId' s, operatorTag :: Maybe OperatorTag, - appVendor :: Bool, tradeName :: Text, legalName :: Maybe Text, serverDomains :: [Text], @@ -172,9 +171,6 @@ data ServerOperator' s = ServerOperator } deriving (Show) -aServerOperator :: ServerOperator -> AServerOperator -aServerOperator op@ServerOperator {operatorId = DBEntityId opId} = op {operatorId = ADBEntityId opId} - conditionsAccepted :: ServerOperator -> Bool conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAcceptance of CAAccepted {} -> True @@ -187,56 +183,67 @@ data OperatorEnabled = OperatorEnabled } deriving (Show) -type UserServers = UserServers' DBEntityId - -type AUserServers = UserServers' ADBEntityId - -data UserServers' s = UserServers - { operator :: Maybe (ServerOperator' s), - smpServers :: [UserServer' s 'PSMP], - xftpServers :: [UserServer' s 'PXFTP] +data UserOperatorServers = UserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [UserServer 'PSMP], + xftpServers :: [UserServer 'PXFTP] } deriving (Show) -type UserServer p = UserServer' DBEntityId p +type UserServer p = UserServer' 'DBStored p -type NewUserServer p = UserServer' NewDBEntity p +type NewUserServer p = UserServer' 'DBNew p -type AUserServer p = UserServer' ADBEntityId p +data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) data UserServer' s p = UserServer - { serverId :: s, - serverOperatorId :: Maybe OperatorId, + { serverId :: DBEntityId' s, server :: ProtoServerWithAuth p, + preset :: Bool, tested :: Maybe Bool, enabled :: Bool } deriving (Show) -data PresetOperatorServers = PresetOperatorServers +data PresetOperator = PresetOperator { operator :: NewServerOperator, - presetSMPServers :: NonEmpty (PresetServer 'PSMP), - presetXFTPServers :: NonEmpty (PresetServer 'PXFTP), + smp :: NonEmpty (NewUserServer 'PSMP), useSMP :: Int, + xftp :: NonEmpty (NewUserServer 'PXFTP), useXFTP :: Int } -data PresetServer p = PresetServer - { useServer :: Bool, - server :: ProtoServerWithAuth p - } +operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> NonEmpty (NewUserServer p) +operatorServers p PresetOperator {smp, xftp} = case p of + SPSMP -> smp + SPXFTP -> xftp + +operatorServersToUse :: UserProtocol p => SProtocolType p -> PresetOperator -> Int +operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of + SPSMP -> useSMP + SPXFTP -> useXFTP + +presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p +presetServer enabled server = + UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled} -- This function should be used inside DB transaction to update conditions in the database -- it returns (conditions to mark as accepted to SimpleX operator, conditions to add) -usageConditionsToAdd :: Bool -> Text -> Text -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, [UsageConditions]) -usageConditionsToAdd newUser prevCommit sourceCommit createdAt = \case +usageConditionsToAdd :: Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions]) +usageConditionsToAdd = usageConditionsToAdd' previousConditionsCommit usageConditionsCommit + +-- This function is used in unit tests +usageConditionsToAdd' :: Text -> Text -> Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions]) +usageConditionsToAdd' prevCommit sourceCommit newUser createdAt = \case [] - | newUser -> (Just sourceCond, [sourceCond]) - | otherwise -> (Just prevCond, [prevCond, sourceCond]) + | newUser -> (Just sourceCond, sourceCond, [sourceCond]) + | otherwise -> (Just prevCond, sourceCond, [prevCond, sourceCond]) where prevCond = conditions 1 prevCommit sourceCond = conditions 2 sourceCommit - conds -> (Nothing, if hasSourceCond then [] else [sourceCond]) + conds + | hasSourceCond -> (Nothing, last conds, []) + | otherwise -> (Nothing, sourceCond, [sourceCond]) where hasSourceCond = any ((sourceCommit ==) . conditionsCommit) conds sourceCond = conditions cId sourceCommit @@ -247,103 +254,88 @@ usageConditionsToAdd newUser prevCommit sourceCommit createdAt = \case -- This function should be used inside DB transaction to update operators. -- It allows to add/remove/update preset operators in the database preserving enabled and roles settings, -- and preserves custom operators without tags for forward compatibility. -updatedServerOperators :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [AServerOperator] -updatedServerOperators presetSrvs storedOps = - foldr addPreset [] presetSrvs - <> map aServerOperator (filter (isNothing . operatorTag) storedOps) -- TODO remove domains of preset operators from custom +updatedServerOperators :: NonEmpty PresetOperator -> [ServerOperator] -> [AServerOperator] +updatedServerOperators presetOps storedOps = + foldr addPreset [] presetOps + <> map (ASO SDBStored) (filter (isNothing . operatorTag) storedOps) where - addPreset PresetOperatorServers {operator = presetOp} = (storedOp' :) + -- TODO remove domains of preset operators from custom + addPreset PresetOperator {operator = presetOp} = (storedOp' :) where storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of - Just ServerOperator {operatorId = DBEntityId opId, conditionsAcceptance, enabled, roles} -> - presetOp {operatorId = ADBEntityId opId, conditionsAcceptance, enabled, roles} - Nothing -> presetOp {operatorId = ANewDBEntity} + Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles} + Nothing -> ASO SDBNew presetOp -- This function should be used inside DB transaction to update servers. -- It assumes that the list of operators was amended using updatedServerOperators, -- that [ServerOperator] has the same operators as [PresetOperatorServers], -- and that they all have serverOperatorId set. --- --- presets -> stored or user-supplied servers, possibly with incorrect operators -updatedUserServers' :: NonEmpty PresetOperatorServers -> [UserServers] -> ([AUserServers], NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) -updatedUserServers' presetSrvs storedSrvs = (userServers, agentSMPServers, agentXFTPServers) +updatedUserServers :: forall p. NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) +updatedUserServers _presetOps randomSrvs = \case + [] -> L.map (AUS SDBNew) randomSrvs + srvs -> + L.map userChanges allPresetServers + `L.appendList` map (AUS SDBStored) (filter customServer srvs) where - userServers = undefined - agentSMPServers = undefined - agentXFTPServers = undefined - -- make set of known tags of preset operators - knownPresetOps :: Set (Maybe OperatorTag) - knownPresetOps = foldl' (\s PresetOperatorServers {operator} -> S.insert (operatorTag operator) s) S.empty presetSrvs + customServer UserServer {preset, server = ProtoServerWithAuth srv _} = + not preset && all (`S.notMember` allPresetHosts) (host srv) + allPresetServers :: NonEmpty (NewUserServer p) + allPresetServers = undefined + allPresetHosts :: Set TransportHost + allPresetHosts = undefined + userChanges :: NewUserServer p -> AUserServer p -- apply changes from stored servers + userChanges = undefined --- make map domain -> operator --- storedSrvs: --- - remove preset operators with tags not present in presets) --- - flatten --- - set correct operators based on domains --- - split servers to with/without preset operators --- - make Map (protoserver, stored server record) from servers with preset operators --- presetSrvs: flatten, update using map above, prepare agent servers, reassemble to userServers --- add other operators and servers without operator --- --- (storedPresets, storedOthers) = partition (isJust . operatorTag . operator) storedSrvs --- (storedOthersKeep, storeOthersPresets) --- userServers = foldr addOther (foldr addPreset [] presetSrvs) storedOthers +randomPresetServers :: NonEmpty PresetOperator -> IO (NonEmpty (NewUserServer p)) +randomPresetServers = undefined --- updatedUserServers :: NonEmpty PresetOperatorServers -> [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> Either String ([UserServer 'PSMP], [UserServer 'PXFTP]) --- updatedUserServers presetSrvs storedOps smpSrvs xftpSrvs = do --- smpSrvs' <- updatedSrvs useSMP smpSrvs =<< presetSrvsToStore presetSMPServers --- xftpSrvs' <- updatedSrvs useXFTP xftpSrvs =<< presetSrvsToStore presetXFTPServers --- pure (smpSrvs', xftpSrvs') +-- randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p]) +-- randomServers p ChatConfig {defaultServers} = do +-- let srvs = operatorServers p defaultServers +-- (enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs +-- toUse = cfgServersToUse p defaultServers +-- if length enbldSrvs <= toUse +-- then pure (srvs, []) +-- else do +-- (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs +-- let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable +-- srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs +-- pure (fromMaybe srvs $ L.nonEmpty srvs', srvs') -- where --- presetSrvsToStore :: forall p. (PresetOperatorServers -> NonEmpty (PresetServer p)) -> Either String (NonEmpty (Bool, UserServer p)) --- presetSrvsToStore presetSel = fold1 <$> mapM operatorSrvs presetSrvs --- where --- operatorSrvs :: PresetOperatorServers -> Either String (NonEmpty (Bool, UserServer p)) --- operatorSrvs op@PresetOperatorServers {operator} = case find ((operatorTag operator ==) . operatorTag) storedOps of --- Nothing -> Left "preset operator not stored" --- Just op' -> Right $ L.map (userSrv op') (presetSel op) --- userSrv op PresetServer {server, useServer} = --- let srv = UserServer {serverId = Nothing, serverOperatorId = operatorId op, server, tested = Nothing, enabled = False} --- in (useServer, srv) +-- server' ServerCfg {server = ProtoServerWithAuth srv _} = srv --- updatedSrvs :: forall p. (PresetOperatorServers -> Int) -> [UserServer p] -> NonEmpty (Bool, UserServer p) -> Either String [UserServer p] --- updatedSrvs useSel storedSrvs presetSrvs = --- fmap enabledSrvs . addOtherServers =<< foldM updatedSrv (storedSrvs', []) presetSrvs --- where --- storedSrvs' :: Map (ProtoServerWithAuth p) (UserServer p) --- storedSrvs' = foldl' (\m us@UserServer {server} -> M.insert server us m) M.empty storedSrvs --- updatedSrv :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> (Bool, UserServer p) -> Either String (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) --- updatedSrv srvs srv = undefined --- addOtherServers :: (Map (ProtoServerWithAuth p) (UserServer p), [(Bool, UserServer p)]) -> Either String [(Bool, UserServer p)] --- addOtherServers = undefined --- enabledSrvs :: [(Bool, UserServer p)] -> [UserServer p] --- enabledSrvs = undefined +useServers :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) +useServers opDomains = L.map agentServer + where + agentServer :: UserServer' s p -> ServerCfg p + agentServer UserServer {server = server@(ProtoServerWithAuth ProtocolServer {host} _), enabled} = + case snd <$> find (\(d, _) -> any (matchingHost d) host) opDomains of + Just ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles} -> + ServerCfg {server, operator = Just opId, enabled = opEnabled && enabled, roles} + Nothing -> + ServerCfg {server, operator = Nothing, enabled, roles = allRoles} + where + matchingHost d = \case + THDomainName h -> d `T.isSuffixOf` T.pack h + _ -> False --- addSrv srv@ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} uss = --- case find (\us -> any [\h -> any (\d -> d `T.isSuffixOf` ) serverDomains (operator us)] host) uss of --- Just opId --- where --- hasOperatorDomain ServerCfg {server = ProtocolServerWithAuth ProtocolServer {host}} us +operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)] +operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] --- addSrv srv uss = ... а тут просто найти оператора в списке и вставить ему сервер через add и как то ругнуться если его нет (но такого не должно быть). Либо вообще есть вариант сразу читать в этом формате - сначала прочитать операторов и в цикле читать серверы каждого - это вот может быть еще проще - --- groupByOperator :: [ServerOperator] -> [ServerCfg 'PSMP] -> [ServerCfg 'PXFTP] -> [UserServers] --- groupByOperator srvOperators smpSrvs xftpSrvs = --- map createOperatorServers (M.toList combinedMap) --- where --- srvOperatorId ServerCfg {operator} = DBEntityId <$> operator --- operatorMap :: Map (Maybe DBEntityId) (Maybe ServerOperator) --- operatorMap = M.fromList [(Just (operatorId op), Just op) | op <- srvOperators] `M.union` M.singleton Nothing Nothing --- initialMap :: Map (Maybe DBEntityId) ([ServerCfg 'PSMP], [ServerCfg 'PXFTP]) --- initialMap = M.fromList [(key, ([], [])) | key <- M.keys operatorMap] --- smpsMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (server : smps, xftps)) (srvOperatorId server) acc) initialMap smpSrvs --- combinedMap = foldr (\server acc -> M.adjust (\(smps, xftps) -> (smps, server : xftps)) (srvOperatorId server) acc) smpsMap xftpSrvs --- createOperatorServers (key, (groupedSmps, groupedXftps)) = --- UserServers --- { operator = fromMaybe Nothing (M.lookup key operatorMap), --- smpServers = groupedSmps, --- xftpServers = groupedXftps --- } +groupByOperator :: [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> IO [UserOperatorServers] +groupByOperator ops smpSrvs xftpSrvs = do + ss <- mapM (\op -> newIORef $ UserOperatorServers (Just op) [] []) ops + custom <- newIORef $ UserOperatorServers Nothing [] [] + domains <- foldM addOpDomains M.empty ss + mapM_ (addServer ss custom domains) smpSrvs + mapM_ (addServer ss custom domains) xftpSrvs + mapM readIORef ss + where + addOpDomains :: Map Text (IORef UserOperatorServers) -> IORef UserOperatorServers -> IO (Map Text (IORef UserOperatorServers)) + addOpDomains _domains _s = undefined + addServer :: [IORef UserOperatorServers] -> IORef UserOperatorServers -> Map Text (IORef UserOperatorServers) -> UserServer p -> IO () + addServer _ss _custom _domains = undefined data UserServersError = USEStorageMissing @@ -352,22 +344,21 @@ data UserServersError | USEDuplicateXFTP {server :: AProtoServerWithAuth} deriving (Show) -validateUserServers :: NonEmpty UserServers -> [UserServersError] +validateUserServers :: NonEmpty UserOperatorServers -> [UserServersError] validateUserServers userServers = let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] - - allSMPServers = map (\UserServer {server} -> server) $ concatMap (\UserServers {smpServers} -> smpServers) userServers + allSMPServers = map (\UserServer {server} -> server) $ concatMap (\UserOperatorServers {smpServers} -> smpServers) userServers duplicateSMPServers = findDuplicatesByHost allSMPServers duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers - allXFTPServers = map (\UserServer {server} -> server) $ concatMap (\UserServers {xftpServers} -> xftpServers) userServers + allXFTPServers = map (\UserServer {server} -> server) $ concatMap (\UserOperatorServers {xftpServers} -> xftpServers) userServers duplicateXFTPServers = findDuplicatesByHost allXFTPServers duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors where - canUseForRole :: (ServerRoles -> Bool) -> UserServers -> Bool - canUseForRole roleSel UserServers {operator, smpServers, xftpServers} = case operator of + canUseForRole :: (ServerRoles -> Bool) -> UserOperatorServers -> Bool + canUseForRole roleSel UserOperatorServers {operator, smpServers, xftpServers} = case operator of Just ServerOperator {roles} -> roleSel roles Nothing -> not (null smpServers) && not (null xftpServers) findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p] @@ -406,11 +397,6 @@ instance ProtocolTypeI p => ToJSON (UserServer p) where instance ProtocolTypeI p => FromJSON (UserServer p) where parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') -instance ToJSON UserServers where - toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServers') - toJSON = $(JQ.mkToJSON defaultJSON ''UserServers') - -instance FromJSON UserServers where - parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServers') +$(JQ.deriveJSON defaultJSON ''UserOperatorServers) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs index 6dd5c79ab1..21ad25b311 100644 --- a/src/Simplex/Chat/Stats.hs +++ b/src/Simplex/Chat/Stats.hs @@ -7,7 +7,6 @@ module Simplex.Chat.Stats where import qualified Data.Aeson.TH as J import Data.List (partition) -import Data.List.NonEmpty (NonEmpty) import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust) @@ -131,7 +130,7 @@ data NtfServerSummary = NtfServerSummary -- - users are passed to exclude hidden users from totalServersSummary; -- - if currentUser is hidden, it should be accounted in totalServersSummary; -- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs -toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> [NtfServer] -> PresentedServersSummary +toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> [SMPServer] -> [XFTPServer] -> [NtfServer] -> PresentedServersSummary toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 567f294d32..d1eb6bf8ed 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -48,8 +49,11 @@ module Simplex.Chat.Store.Profiles getContactWithoutConnViaAddress, updateUserAddressAutoAccept, getProtocolServers, + getUpdateUserServers, -- overwriteOperatorsAndServers, overwriteProtocolServers, + insertProtocolServer, + getUpdateServerOperators, getServerOperators, setServerOperators, getCurrentUsageConditions, @@ -78,10 +82,11 @@ import Data.Int (Int64) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) -import Data.Text (Text, splitOn) +import Data.Text (Text) +import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime (..), getCurrentTime) -import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) +import Database.SQLite.Simple (NamedParam (..), Only (..), Query, (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Call import Simplex.Chat.Messages @@ -93,7 +98,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -525,178 +530,264 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) -getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] +getUpdateUserServers :: forall p. ProtocolTypeI p => DB.Connection -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p)) +getUpdateUserServers db presetOps randomSrvs user = do + ts <- getCurrentTime + srvs <- getProtocolServers db user + let srvs' = updatedUserServers presetOps randomSrvs srvs + mapM (upsertServer ts) srvs' + where + upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) + upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of + DBNewEntity -> insertProtocolServer db user ts s + DBEntityId _ -> updateServer s ts $> s + updateServer :: UserServer p -> UTCTime -> IO () + updateServer UserServer {serverId, server, preset, tested, enabled} ts = + DB.execute + db + [sql| + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, updated_at + WHERE smp_server_id = ? + |] + (serverColumns server :. (preset, tested, enabled, ts, serverId)) + +getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [UserServer p] getProtocolServers db User {userId} = - map toServerCfg + map toUserServer <$> DB.query db [sql| - SELECT s.host, s.port, s.key_hash, s.basic_auth, s.server_operator_id, s.preset, s.tested, s.enabled, o.role_storage, o.role_proxy - FROM protocol_servers s - LEFT JOIN server_operators o USING (server_operator_id) - WHERE s.user_id = ? AND s.protocol = ? + SELECT smp_server_id, host, port, key_hash, basic_auth, preset, tested, enabled + FROM protocol_servers + WHERE user_id = ? AND protocol = ? |] (userId, decodeLatin1 $ strEncode protocol) where protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Maybe OperatorId, Bool, Maybe Bool, Bool, Maybe Bool, Maybe Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, operator, preset, tested, enabled, storage_, proxy_) = + toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> UserServer p + toUserServer (serverId, host, port, keyHash, auth_, preset, tested, enabled) = let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - roles = ServerRoles {storage = fromMaybe True storage_, proxy = fromMaybe True proxy_} - in ServerCfg {server, operator, preset, tested, enabled, roles} + in UserServer {serverId, server, preset, tested, enabled} -- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [UserServer p] -> ExceptT StoreError IO () overwriteProtocolServers db User {userId} servers = -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) - forM_ servers $ \ServerCfg {server, preset, tested, enabled} -> do - let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server + forM_ servers $ \UserServer {serverId, server, preset, tested, enabled} -> do DB.execute db [sql| INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?) + (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) - -- Right <$> getProtocolServers db user + (Only serverId :. serverColumns server :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p +insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) +insertProtocolServer db User {userId} ts srv@UserServer {server, preset, tested, enabled} = do + DB.execute + db + [sql| + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?) + |] + (serverColumns server :. (preset, tested, enabled, userId, ts, ts)) + sId <- insertedRowId db + pure (srv :: NewUserServer p) {serverId = DBEntityId sId} + +serverColumns :: forall p. ProtocolTypeI p => ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) +serverColumns (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = + let protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + auth = safeDecodeUtf8 . unBasicAuth <$> auth_ + in (protocol, host, port, keyHash, auth) + getServerOperators :: DB.Connection -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) getServerOperators db = do - now <- liftIO getCurrentTime - currentConditions <- getCurrentUsageConditions db - latestAcceptedConditions <- getLatestAcceptedConditions db - operators <- - liftIO $ - map (toOperator now currentConditions latestAcceptedConditions) - <$> DB.query_ - db - [sql| - SELECT - so.server_operator_id, so.server_operator_tag, so.trade_name, so.legal_name, - so.server_domains, so.enabled, so.role_storage, so.role_proxy, - AcceptedConditions.conditions_commit, AcceptedConditions.accepted_at - FROM server_operators so - LEFT JOIN ( - SELECT server_operator_id, conditions_commit, accepted_at, MAX(operator_usage_conditions_id) - FROM operator_usage_conditions - GROUP BY server_operator_id - ) AcceptedConditions ON AcceptedConditions.server_operator_id = so.server_operator_id - |] - pure (operators, usageConditionsAction operators currentConditions now) - where - toOperator :: - UTCTime -> - UsageConditions -> - Maybe UsageConditions -> - ( (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) - :. (Maybe Text, Maybe UTCTime) - ) -> - ServerOperator - toOperator - now - UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} - latestAcceptedConditions_ - ( (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) - :. (operatorCommit_, acceptedAt_) - ) = - let roles = ServerRoles {storage, proxy} - serverDomains = splitOn "," domains - conditionsAcceptance = case (latestAcceptedConditions_, operatorCommit_) of - -- no conditions were ever accepted for any operator(s) - -- (shouldn't happen as there should always be record for SimpleX Chat) - (Nothing, _) -> CARequired Nothing - -- no conditions were ever accepted for this operator - (_, Nothing) -> CARequired Nothing - (Just UsageConditions {conditionsCommit = latestAcceptedCommit}, Just operatorCommit) - | latestAcceptedCommit == currentCommit -> - if operatorCommit == latestAcceptedCommit - then -- current conditions were accepted for operator - CAAccepted acceptedAt_ - else -- current conditions were NOT accepted for operator, but were accepted for other operator(s) - CARequired Nothing - | otherwise -> - if operatorCommit == latestAcceptedCommit - then -- new conditions available, latest accepted conditions were accepted for operator - CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) - else -- new conditions available, latest accepted conditions were NOT accepted for operator (were accepted for other operator(s)) - CARequired Nothing - in ServerOperator {operatorId, operatorTag, appVendor = False, tradeName, legalName, serverDomains, conditionsAcceptance, enabled, roles} - -- TODO appVendor + currentConds <- getCurrentUsageConditions db + liftIO $ do + now <- getCurrentTime + latestAcceptedConds_ <- getLatestAcceptedConditions db + let getConds op = (\ca -> op {conditionsAcceptance = ca}) <$> getOperatorConditions_ db op currentConds latestAcceptedConds_ now + operators <- mapM getConds =<< getServerOperators_ db + pure (operators, usageConditionsAction operators currentConds now) -setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) -setServerOperators db operatorsEnabled = do - liftIO $ forM_ operatorsEnabled $ \OperatorEnabled {operatorId', enabled', roles' = ServerRoles {storage, proxy}} -> +setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> IO () +setServerOperators db = + mapM_ $ \OperatorEnabled {operatorId', enabled', roles' = ServerRoles {storage, proxy}} -> DB.execute db "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" (enabled', storage, proxy, operatorId') - getServerOperators db + +getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] +getUpdateServerOperators db presetOps newUser = do + conds <- map toUsageConditions <$> DB.query_ db usageCondsQuery + now <- getCurrentTime + let (acceptForSimplex_, currentConds, condsToAdd) = usageConditionsToAdd newUser now conds + mapM_ insertConditions condsToAdd + latestAcceptedConds_ <- getLatestAcceptedConditions db + ops <- updatedServerOperators presetOps <$> getServerOperators_ db + forM ops $ \(ASO _ op) -> + case operatorId op of + DBNewEntity -> do + op' <- insertOperator op + case (operatorTag op', acceptForSimplex_) of + (Just OTSimplex, Just cond) -> autoAcceptConditions op' cond + _ -> pure op' + DBEntityId _ -> do + updateOperator op + getOperatorConditions_ db op currentConds latestAcceptedConds_ now >>= \case + CARequired Nothing | operatorTag op == Just OTSimplex -> autoAcceptConditions op currentConds + CARequired (Just ts) | ts < now -> autoAcceptConditions op currentConds + ca -> pure op {conditionsAcceptance = ca} + where + insertConditions UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} = + DB.execute + db + [sql| + INSERT INTO usage_conditions + (usage_conditions_id, conditions_commit, notified_at, created_at) + VALUES (?,?,?,?) + |] + (conditionsId, conditionsCommit, notifiedAt, createdAt) + updateOperator :: ServerOperator -> IO () + updateOperator ServerOperator {operatorId, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET trade_name = ?, legal_name = ?, server_domains = ?, enabled = ?, role_storage = ?, role_proxy = ? + WHERE server_operator_id = ? + |] + (tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy, operatorId) + insertOperator :: NewServerOperator -> IO ServerOperator + insertOperator op@ServerOperator {operatorTag, tradeName, legalName, serverDomains, enabled, roles = ServerRoles {storage, proxy}} = do + DB.execute + db + [sql| + INSERT INTO server_operators + (server_operator_tag, trade_name, legal_name, server_domains, enabled, role_storage, role_proxy) + VALUES (?,?,?,?,?,?,?) + |] + (operatorTag, tradeName, legalName, T.intercalate "," serverDomains, enabled, storage, proxy) + opId <- insertedRowId db + pure op {operatorId = DBEntityId opId} + autoAcceptConditions op UsageConditions {conditionsCommit} = + acceptConditions_ db op conditionsCommit Nothing + $> op {conditionsAcceptance = CAAccepted Nothing} + +getServerOperators_ :: DB.Connection -> IO [ServerOperator] +getServerOperators_ db = + map toOperator + <$> DB.query_ + db + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, role_storage, role_proxy, + FROM server_operators + |] + where + toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = + ServerOperator + { operatorId, + operatorTag, + tradeName, + legalName, + serverDomains = T.splitOn "," domains, + conditionsAcceptance = CARequired Nothing, + enabled, + roles = ServerRoles {storage, proxy} + } + +getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance +getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do + case latestAcceptedConds_ of + Nothing -> pure $ CARequired Nothing -- no conditions accepted by any operator + Just UsageConditions {conditionsCommit = latestAcceptedCommit} -> do + operatorAcceptedConds_ <- + maybeFirstRow id $ + DB.query + db + [sql| + SELECT conditions_commit, accepted_at + FROM operator_usage_conditions + WHERE server_operator_id = ? + ORDER BY operator_usage_conditions_id DESC + LIMIT 1 + |] + (Only operatorId) + pure $ case operatorAcceptedConds_ of + Just (operatorCommit, acceptedAt_) + | operatorCommit /= latestAcceptedCommit -> CARequired Nothing -- TODO should we consider this operator disabled? + | currentCommit /= latestAcceptedCommit -> CARequired $ conditionsRequiredOrDeadline createdAt (fromMaybe now notifiedAt) + | otherwise -> CAAccepted acceptedAt_ + _ -> CARequired Nothing -- no conditions were accepted for this operator getCurrentUsageConditions :: DB.Connection -> ExceptT StoreError IO UsageConditions getCurrentUsageConditions db = ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ - DB.query_ - db - [sql| - SELECT usage_conditions_id, conditions_commit, notified_at, created_at - FROM usage_conditions - ORDER BY usage_conditions_id DESC LIMIT 1 - |] + DB.query_ db (usageCondsQuery <> " DESC LIMIT 1") + +usageCondsQuery :: Query +usageCondsQuery = + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + ORDER BY usage_conditions_id + |] toUsageConditions :: (Int64, Text, Maybe UTCTime, UTCTime) -> UsageConditions toUsageConditions (conditionsId, conditionsCommit, notifiedAt, createdAt) = UsageConditions {conditionsId, conditionsCommit, notifiedAt, createdAt} -getLatestAcceptedConditions :: DB.Connection -> ExceptT StoreError IO (Maybe UsageConditions) -getLatestAcceptedConditions db = do - (latestAcceptedCommit_ :: Maybe Text) <- - liftIO $ - maybeFirstRow fromOnly $ - DB.query_ - db - [sql| +getLatestAcceptedConditions :: DB.Connection -> IO (Maybe UsageConditions) +getLatestAcceptedConditions db = + maybeFirstRow toUsageConditions $ + DB.query_ + db + [sql| + SELECT usage_conditions_id, conditions_commit, notified_at, created_at + FROM usage_conditions + WHERE conditions_commit = ( SELECT conditions_commit FROM operator_usage_conditions ORDER BY accepted_at DESC LIMIT 1 - |] - forM latestAcceptedCommit_ $ \latestAcceptedCommit -> - ExceptT . firstRow toUsageConditions SEUsageConditionsNotFound $ - DB.query - db - [sql| - SELECT usage_conditions_id, conditions_commit, notified_at, created_at - FROM usage_conditions - WHERE conditions_commit = ? - |] - (Only latestAcceptedCommit) + ) + |] setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () setConditionsNotified db conditionsId notifiedAt = DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, conditionsId) -acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO ([ServerOperator], Maybe UsageConditionsAction) +acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO (NonEmpty ServerOperator) acceptConditions db conditionsId operators acceptedAt = do UsageConditions {conditionsCommit} <- getUsageConditionsById_ db conditionsId - liftIO $ forM_ operators $ \ServerOperator {operatorId, operatorTag} -> - DB.execute - db - [sql| - INSERT INTO operator_usage_conditions - (server_operator_id, server_operator_tag, conditions_commit, accepted_at) - VALUES (?,?,?,?) - |] - (operatorId, operatorTag, conditionsCommit, acceptedAt) - getServerOperators db + let ts = Just acceptedAt + liftIO $ forM operators $ \op -> + acceptConditions_ db op conditionsCommit ts $> op {conditionsAcceptance = CAAccepted ts} + +acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () +acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = + DB.execute + db + [sql| + INSERT INTO operator_usage_conditions + (server_operator_id, server_operator_tag, conditions_commit, accepted_at) + VALUES (?,?,?,?) + |] + (operatorId, operatorTag, conditionsCommit, acceptedAt) getUsageConditionsById_ :: DB.Connection -> Int64 -> ExceptT StoreError IO UsageConditions getUsageConditionsById_ db conditionsId = @@ -710,11 +801,11 @@ getUsageConditionsById_ db conditionsId = |] (Only conditionsId) -setUserServers :: DB.Connection -> User -> NonEmpty UserServers -> ExceptT StoreError IO () +setUserServers :: DB.Connection -> User -> NonEmpty UserOperatorServers -> ExceptT StoreError IO () setUserServers db User {userId} userServers = do currentTs <- liftIO getCurrentTime forM_ userServers $ do - \UserServers {operator, smpServers, xftpServers} -> do + \UserOperatorServers {operator, smpServers, xftpServers} -> do forM_ operator $ \op -> liftIO $ updateOperator currentTs op overwriteServers currentTs operator smpServers overwriteServers currentTs operator xftpServers @@ -737,58 +828,20 @@ setUserServers db User {userId} userServers = do DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) Just ServerOperator {operatorId} -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) - forM_ servers $ \UserServer {server, serverOperatorId, tested, enabled} -> do - let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server + forM_ servers $ \UserServer {serverId, server, tested, enabled} -> do DB.execute db [sql| INSERT INTO protocol_servers - (protocol, host, port, key_hash, basic_auth, operator, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, serverOperatorId) :. (False, tested, enabled, userId, currentTs, currentTs)) - -- take preset from operator + (Only serverId :. serverColumns server :. (tested, enabled, userId, currentTs, currentTs)) + -- take preset from operator pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p --- updateServerOperators_ :: DB.Connection -> [ServerOperator] -> IO [ServerOperator] --- updateServerOperators_ db operators = do --- DB.execute_ db "DELETE FROM server_operators WHERE preset = 0" --- let (existing, new) = partition (isJust . operatorId) operators --- existing' <- mapM (\op -> upsertExisting op $> op) existing --- new' <- mapM insertNew new --- pure $ existing' <> new' --- where --- upsertExisting ServerOperator {operatorId, name, preset, enabled, roles = ServerRoles {storage, proxy}} --- | preset = --- DB.execute --- db --- [sql| --- UPDATE server_operators --- SET enabled = ?, role_storage = ?, role_proxy = ? --- WHERE server_operator_id = ? --- |] --- (enabled, storage, proxy, operatorId) --- | otherwise = --- DB.execute --- db --- [sql| --- INSERT INTO server_operators (server_operator_id, name, preset, enabled, role_storage, role_proxy) --- VALUES (?,?,?,?,?,?) --- |] --- (operatorId, name, preset, enabled, storage, proxy) --- insertNew op@ServerOperator {name, preset, enabled, roles = ServerRoles {storage, proxy}} = do --- DB.execute --- db --- [sql| --- INSERT INTO server_operators (name, preset, enabled, role_storage, role_proxy) --- VALUES (?,?,?,?,?) --- |] --- (name, preset, enabled, storage, proxy) --- opId <- insertedRowId db --- pure op {operatorId = Just opId} - createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do currentTs <- getCurrentTime diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index e38a34d45f..361e61b953 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -1,6 +1,7 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} module Simplex.Chat.Terminal where @@ -13,15 +14,15 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB -import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) +import Simplex.Chat (_defaultNtfServers, defaultChatConfig, operatorSimpleXChat) import Simplex.Chat.Controller import Simplex.Chat.Core import Simplex.Chat.Help (chatWelcome) +import Simplex.Chat.Operators import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Agent.Env.SQLite (allRoles, presetServerCfg) import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.IO (hFlush, hSetEcho, stdin, stdout) @@ -29,20 +30,24 @@ import System.IO (hFlush, hSetEcho, stdin, stdout) terminalChatConfig :: ChatConfig terminalChatConfig = defaultChatConfig - { defaultServers = - DefaultAgentServers - { smp = - L.fromList $ - map - (presetServerCfg True allRoles operatorSimpleXChat) - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ], - useSMP = 3, - ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = L.map (presetServerCfg True allRoles operatorSimpleXChat) defaultXFTPServers, - useXFTP = L.length defaultXFTPServers, + { presetServers = + PresetServers + { operators = + [ PresetOperator + { operator = operatorSimpleXChat, + smp = + L.map + (presetServer True) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ], + useSMP = 3, + xftp = L.map (presetServer True) defaultXFTPServers, + useXFTP = 3 + } + ], + ntf = _defaultNtfServers, netCfg = defaultNetworkConfig { smpProxyMode = SPMUnknown, diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 64703a3a92..b0eb4dac88 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -10,7 +10,7 @@ import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Network.Socket -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), DefaultAgentServers (DefaultAgentServers, netCfg), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), PresetServers (..), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Terminal @@ -56,7 +56,7 @@ simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServer putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r welcome :: ChatConfig -> ChatOpts -> IO () -welcome ChatConfig {defaultServers = DefaultAgentServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = +welcome ChatConfig {presetServers = PresetServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = mapM_ putStrLn [ versionString versionNumber, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index c53e5a2749..a14ba1317a 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -19,7 +19,7 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) import Data.Function (on) import Data.Int (Int64) -import Data.List (foldl', groupBy, intercalate, intersperse, partition, sortOn) +import Data.List (groupBy, intercalate, intersperse, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -42,7 +42,6 @@ import Simplex.Chat.Help import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types @@ -54,7 +53,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) -import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerCfg (..)) +import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) @@ -96,7 +95,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChats chats -> viewChats ts tz chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] - CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView + -- CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRServerOperators {} -> [] CRUserServers {} -> [] @@ -1214,27 +1213,27 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] -viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] -viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = - customServers - <> if testView - then [] - else - [ "", - "use " <> highlight (srvCmd <> " test ") <> " to test " <> pName <> " server connection", - "use " <> highlight (srvCmd <> " ") <> " to configure " <> pName <> " servers", - "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets" - ] - <> case p of - SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"] - SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"] - where - srvCmd = "/" <> strEncode p - pName = protocolName p - customServers = - if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers - else viewServers operators protoServers +-- viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] +-- viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = +-- customServers +-- <> if testView +-- then [] +-- else +-- [ "", +-- "use " <> highlight (srvCmd <> " test ") <> " to test " <> pName <> " server connection", +-- "use " <> highlight (srvCmd <> " ") <> " to configure " <> pName <> " servers", +-- "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets" +-- ] +-- <> case p of +-- SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"] +-- SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"] +-- where +-- srvCmd = "/" <> strEncode p +-- pName = protocolName p +-- customServers = +-- if null protoServers +-- then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers +-- else viewServers operators protoServers protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1331,11 +1330,11 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] -viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList - where - ops :: Map (Maybe Int64) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators - viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" +-- viewServers :: ProtocolTypeI p => [ServerOperator] -> NonEmpty (ServerCfg p) -> [StyledString] +-- viewServers operators = map (plain . (\ServerCfg {server, operator} -> B.unpack (strEncode server) <> viewOperator operator)) . L.toList +-- where +-- ops :: Map (Maybe DBEntityId) Text = foldl' (\m ServerOperator {operatorId, tradeName} -> M.insert (Just operatorId) tradeName m) M.empty operators +-- viewOperator = maybe "" $ \op -> " (operator " <> maybe (show op) T.unpack (M.lookup (Just op) ops) <> ")" viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo From 90ed503ee04720087bb35050bffda1694babc42b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 10 Nov 2024 12:30:24 +0000 Subject: [PATCH 14/22] update (most tests pass) --- src/Simplex/Chat.hs | 34 ++++++-- src/Simplex/Chat/Migrations/chat_schema.sql | 9 +-- src/Simplex/Chat/Operators.hs | 89 +++++++++------------ src/Simplex/Chat/Store/Profiles.hs | 54 ++++++------- tests/ChatTests/Direct.hs | 6 +- tests/RandomServers.hs | 51 ++++++------ 6 files changed, 122 insertions(+), 121 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index e899a73eeb..85f039a6e8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -40,6 +40,7 @@ import Data.Constraint (Dict (..)) import Data.Either (fromRight, lefts, partitionEithers, rights) import Data.Fixed (div') import Data.Foldable (foldr') +import Data.Foldable1 (fold1) import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) @@ -391,12 +392,12 @@ newChatController pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> [ProtoServerWithAuth p] -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers protocol users opDomains = maybe get srvCfgs . L.nonEmpty + getUserServers p users opDomains = maybe get srvCfgs . L.nonEmpty where get = do - randomSrvs <- randomPresetServers presetOps + randomSrvs <- randomPresetServers p presetOps fmap M.fromList $ forM users $ \u -> - (aUserId u,) . useServers opDomains <$> getUpdateUserServers db presetOps randomSrvs u + (aUserId u,) . useServers opDomains <$> getUpdateUserServers db p presetOps randomSrvs u srvCfgs ss = pure $ M.fromList $ map (\u -> (aUserId u, L.map srvCfg ss)) users srvCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} @@ -441,6 +442,23 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} +randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> IO (NonEmpty (NewUserServer p)) +randomPresetServers p = fmap fold1 . mapM opSrvs + where + opSrvs :: PresetOperator -> IO (NonEmpty (NewUserServer p)) + opSrvs op = do + let srvs = operatorServers p op + (enbldSrvs, dsbldSrvs) = L.partition (\UserServer {enabled} -> enabled) srvs + toUse = operatorServersToUse p op + if length enbldSrvs <= toUse + then pure srvs + else do + (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs + let dsbldSrvs' = map (\srv -> (srv :: NewUserServer p) {enabled = False}) srvsToDisable + srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs + pure $ fromMaybe srvs $ L.nonEmpty srvs' + server' UserServer {server = ProtoServerWithAuth srv _} = srv + -- enableSndFiles has no effect when mainApp is True startChatController :: Bool -> Bool -> CM' (Async ()) startChatController mainApp enableSndFiles = do @@ -596,8 +614,8 @@ processChatCommand' vr = \case createPresetContactCards user `catchChatError` \_ -> pure () withFastStore $ \db -> do createNoteFolder db user - liftIO $ mapM_ (mapM_ (insertProtocolServer db user ts)) smpServers_ - liftIO $ mapM_ (mapM_ (insertProtocolServer db user ts)) xftpServers_ + liftIO $ mapM_ (mapM_ (insertProtocolServer db SPSMP user ts)) smpServers_ + liftIO $ mapM_ (mapM_ (insertProtocolServer db SPXFTP user ts)) xftpServers_ atomically . writeTVar u $ Just user pure $ CRActiveUser user where @@ -607,13 +625,13 @@ processChatCommand' vr = \case createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [(Text, ServerOperator)] -> CM (NonEmpty (ServerCfg p), Maybe (NonEmpty (NewUserServer p))) - chooseServers protocol opDomains = do + chooseServers p opDomains = do PresetServers {operators = presetOps} <- asks $ presetServers . config - randomSrvs <- liftIO $ randomPresetServers presetOps + randomSrvs <- liftIO $ randomPresetServers p presetOps chatReadVar currentUser >>= \case Nothing -> pure (useServers opDomains randomSrvs, Just randomSrvs) Just user -> do - srvs <- withFastStore' $ \db -> getUpdateUserServers db presetOps randomSrvs user + srvs <- withFastStore' $ \db -> getUpdateUserServers db p presetOps randomSrvs user pure (useServers opDomains srvs, Nothing) coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 1541f36b60..94b6de771b 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -450,7 +450,6 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), protocol TEXT NOT NULL DEFAULT 'smp', - server_operator_id INTEGER REFERENCES server_operators ON DELETE SET NULL, UNIQUE(user_id, host, port) ); CREATE TABLE xftp_file_descriptions( @@ -593,7 +592,6 @@ CREATE TABLE app_settings(app_settings TEXT NOT NULL); CREATE TABLE server_operators( server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT, server_operator_tag TEXT, - app_vendor INTEGER NOT NULL, trade_name TEXT NOT NULL, legal_name TEXT, server_domains TEXT, @@ -919,13 +917,10 @@ CREATE INDEX idx_received_probes_group_member_id on received_probes( group_member_id ); CREATE INDEX idx_contact_requests_contact_id ON contact_requests(contact_id); -CREATE INDEX idx_protocol_servers_server_operator_id ON protocol_servers( - server_operator_id -); CREATE INDEX idx_operator_usage_conditions_server_operator_id ON operator_usage_conditions( server_operator_id ); CREATE UNIQUE INDEX idx_operator_usage_conditions_conditions_commit ON operator_usage_conditions( - server_operator_id, - conditions_commit + conditions_commit, + server_operator_id ); diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index ad962d1fdd..eb85752909 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -12,18 +12,20 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Operators where -import Control.Monad (foldM) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed +import Data.Foldable1 (foldMap1) import Data.IORef import Data.Int (Int64) -import Data.List (find) +import Data.List (find, foldl') import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -45,7 +47,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" @@ -228,7 +230,7 @@ presetServer enabled server = UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled} -- This function should be used inside DB transaction to update conditions in the database --- it returns (conditions to mark as accepted to SimpleX operator, conditions to add) +-- it evaluates to (conditions to mark as accepted to SimpleX operator, current conditions, and conditions to add) usageConditionsToAdd :: Bool -> UTCTime -> [UsageConditions] -> (Maybe UsageConditions, UsageConditions, [UsageConditions]) usageConditionsToAdd = usageConditionsToAdd' previousConditionsCommit usageConditionsCommit @@ -268,74 +270,59 @@ updatedServerOperators presetOps storedOps = Nothing -> ASO SDBNew presetOp -- This function should be used inside DB transaction to update servers. --- It assumes that the list of operators was amended using updatedServerOperators, --- that [ServerOperator] has the same operators as [PresetOperatorServers], --- and that they all have serverOperatorId set. -updatedUserServers :: forall p. NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) -updatedUserServers _presetOps randomSrvs = \case +updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) +updatedUserServers p presetOps randomSrvs = \case [] -> L.map (AUS SDBNew) randomSrvs srvs -> - L.map userChanges allPresetServers + L.map (userServer storedSrvs) presetSrvs `L.appendList` map (AUS SDBStored) (filter customServer srvs) + where + storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs where - customServer UserServer {preset, server = ProtoServerWithAuth srv _} = - not preset && all (`S.notMember` allPresetHosts) (host srv) - allPresetServers :: NonEmpty (NewUserServer p) - allPresetServers = undefined - allPresetHosts :: Set TransportHost - allPresetHosts = undefined - userChanges :: NewUserServer p -> AUserServer p -- apply changes from stored servers - userChanges = undefined + customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) + presetSrvs :: NonEmpty (NewUserServer p) + presetSrvs = foldMap1 (operatorServers p) presetOps + presetHosts :: Set TransportHost + presetHosts = foldMap1 (S.fromList . L.toList . srvHost) presetSrvs + userServer :: Map (ProtoServerWithAuth p) (UserServer p) -> NewUserServer p -> AUserServer p + userServer storedSrvs srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) -randomPresetServers :: NonEmpty PresetOperator -> IO (NonEmpty (NewUserServer p)) -randomPresetServers = undefined - --- randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p]) --- randomServers p ChatConfig {defaultServers} = do --- let srvs = operatorServers p defaultServers --- (enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs --- toUse = cfgServersToUse p defaultServers --- if length enbldSrvs <= toUse --- then pure (srvs, []) --- else do --- (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs --- let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable --- srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs --- pure (fromMaybe srvs $ L.nonEmpty srvs', srvs') --- where --- server' ServerCfg {server = ProtoServerWithAuth srv _} = srv +srvHost :: UserServer' s p -> NonEmpty TransportHost +srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv useServers :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) useServers opDomains = L.map agentServer where agentServer :: UserServer' s p -> ServerCfg p - agentServer UserServer {server = server@(ProtoServerWithAuth ProtocolServer {host} _), enabled} = - case snd <$> find (\(d, _) -> any (matchingHost d) host) opDomains of - Just ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles} -> + agentServer srv@UserServer {server, enabled} = + case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of + Just (_, ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles}) -> ServerCfg {server, operator = Just opId, enabled = opEnabled && enabled, roles} Nothing -> ServerCfg {server, operator = Nothing, enabled, roles = allRoles} - where - matchingHost d = \case - THDomainName h -> d `T.isSuffixOf` T.pack h - _ -> False + +matchingHost :: Text -> TransportHost -> Bool +matchingHost d = \case + THDomainName h -> d `T.isSuffixOf` T.pack h + _ -> False operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)] operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] groupByOperator :: [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> IO [UserOperatorServers] groupByOperator ops smpSrvs xftpSrvs = do - ss <- mapM (\op -> newIORef $ UserOperatorServers (Just op) [] []) ops + ss <- mapM (\op -> (serverDomains op,) <$> newIORef (UserOperatorServers (Just op) [] [])) ops custom <- newIORef $ UserOperatorServers Nothing [] [] - domains <- foldM addOpDomains M.empty ss - mapM_ (addServer ss custom domains) smpSrvs - mapM_ (addServer ss custom domains) xftpSrvs - mapM readIORef ss + mapM_ (addServer ss custom addSMP) (reverse smpSrvs) + mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs) + mapM (readIORef . snd) ss where - addOpDomains :: Map Text (IORef UserOperatorServers) -> IORef UserOperatorServers -> IO (Map Text (IORef UserOperatorServers)) - addOpDomains _domains _s = undefined - addServer :: [IORef UserOperatorServers] -> IORef UserOperatorServers -> Map Text (IORef UserOperatorServers) -> UserServer p -> IO () - addServer _ss _custom _domains = undefined + addServer :: [([Text], IORef UserOperatorServers)] -> IORef UserOperatorServers -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () + addServer ss custom add srv = + let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss + in atomicModifyIORef'_ v $ add srv + addSMP srv s@UserOperatorServers {smpServers} = s {smpServers = srv : smpServers} + addXFTP srv s@UserOperatorServers {xftpServers} = s {xftpServers = srv : xftpServers} data UserServersError = USEStorageMissing diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index d1eb6bf8ed..9b64a61e31 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -106,7 +106,7 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) @@ -530,19 +530,19 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) -getUpdateUserServers :: forall p. ProtocolTypeI p => DB.Connection -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p)) -getUpdateUserServers db presetOps randomSrvs user = do +getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p)) +getUpdateUserServers db p presetOps randomSrvs user = do ts <- getCurrentTime srvs <- getProtocolServers db user - let srvs' = updatedUserServers presetOps randomSrvs srvs + let srvs' = updatedUserServers p presetOps randomSrvs srvs mapM (upsertServer ts) srvs' where upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of - DBNewEntity -> insertProtocolServer db user ts s - DBEntityId _ -> updateServer s ts $> s - updateServer :: UserServer p -> UTCTime -> IO () - updateServer UserServer {serverId, server, preset, tested, enabled} ts = + DBNewEntity -> insertProtocolServer db p user ts s + DBEntityId _ -> updateServer ts s $> s + updateServer :: UTCTime -> UserServer p -> IO () + updateServer ts UserServer {serverId, server, preset, tested, enabled} = DB.execute db [sql| @@ -551,7 +551,7 @@ getUpdateUserServers db presetOps randomSrvs user = do preset = ?, tested = ?, enabled = ?, updated_at WHERE smp_server_id = ? |] - (serverColumns server :. (preset, tested, enabled, ts, serverId)) + (serverColumns p server :. (preset, tested, enabled, ts, serverId)) getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [UserServer p] getProtocolServers db User {userId} = @@ -574,12 +574,12 @@ getProtocolServers db User {userId} = -- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] -- overwriteOperatorsAndServers db user@User {userId} operators_ servers = do -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [UserServer p] -> ExceptT StoreError IO () -overwriteProtocolServers db User {userId} servers = +overwriteProtocolServers :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> [UserServer p] -> ExceptT StoreError IO () +overwriteProtocolServers db p User {userId} servers = -- liftIO $ mapM_ (updateServerOperators_ db) operators_ checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol) + DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, decodeLatin1 $ strEncode p) forM_ servers $ \UserServer {serverId, server, preset, tested, enabled} -> do DB.execute db @@ -588,13 +588,11 @@ overwriteProtocolServers db User {userId} servers = (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - (Only serverId :. serverColumns server :. (preset, tested, enabled, userId, currentTs, currentTs)) + (Only serverId :. serverColumns p server :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () - where - protocol = decodeLatin1 $ strEncode $ protocolTypeI @p -insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) -insertProtocolServer db User {userId} ts srv@UserServer {server, preset, tested, enabled} = do +insertProtocolServer :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> UTCTime -> NewUserServer p -> IO (UserServer p) +insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, tested, enabled} = do DB.execute db [sql| @@ -602,13 +600,13 @@ insertProtocolServer db User {userId} ts srv@UserServer {server, preset, tested, (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - (serverColumns server :. (preset, tested, enabled, userId, ts, ts)) + (serverColumns p server :. (preset, tested, enabled, userId, ts, ts)) sId <- insertedRowId db pure (srv :: NewUserServer p) {serverId = DBEntityId sId} -serverColumns :: forall p. ProtocolTypeI p => ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) -serverColumns (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = - let protocol = decodeLatin1 $ strEncode $ protocolTypeI @p +serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) +serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = + let protocol = decodeLatin1 $ strEncode p auth = safeDecodeUtf8 . unBasicAuth <$> auth_ in (protocol, host, port, keyHash, auth) @@ -694,7 +692,7 @@ getServerOperators_ db = db [sql| SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, role_storage, role_proxy, + server_domains, enabled, role_storage, role_proxy FROM server_operators |] where @@ -807,8 +805,8 @@ setUserServers db User {userId} userServers = do forM_ userServers $ do \UserOperatorServers {operator, smpServers, xftpServers} -> do forM_ operator $ \op -> liftIO $ updateOperator currentTs op - overwriteServers currentTs operator smpServers - overwriteServers currentTs operator xftpServers + overwriteServers SPSMP currentTs operator smpServers + overwriteServers SPXFTP currentTs operator xftpServers where updateOperator :: UTCTime -> ServerOperator -> IO () updateOperator currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = @@ -820,8 +818,8 @@ setUserServers db User {userId} userServers = do WHERE server_operator_id = ? |] (enabled, storage, proxy, operatorId, currentTs) - overwriteServers :: forall p. ProtocolTypeI p => UTCTime -> Maybe ServerOperator -> [UserServer p] -> ExceptT StoreError IO () - overwriteServers currentTs serverOperator servers = + overwriteServers :: ProtocolTypeI p => SProtocolType p -> UTCTime -> Maybe ServerOperator -> [UserServer p] -> ExceptT StoreError IO () + overwriteServers p currentTs serverOperator servers = checkConstraint SEUniqueID . ExceptT $ do case serverOperator of Nothing -> @@ -836,11 +834,11 @@ setUserServers db User {userId} userServers = do (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) |] - (Only serverId :. serverColumns server :. (tested, enabled, userId, currentTs, currentTs)) + (Only serverId :. serverColumns p server :. (tested, enabled, userId, currentTs, currentTs)) -- take preset from operator pure $ Right () where - protocol = decodeLatin1 $ strEncode $ protocolTypeI @p + protocol = decodeLatin1 $ strEncode p createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 8971e8d22d..7d5dc67d24 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -25,7 +25,7 @@ import Database.SQLite.Simple (Only (..)) import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..), DefaultAgentServers (..)) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -332,8 +332,8 @@ testRetryConnectingClientTimeout tmp = do { quotaExceededTimeout = 1, messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} }, - defaultServers = - let def@DefaultAgentServers {netCfg} = defaultServers testCfg + presetServers = + let def@PresetServers {netCfg} = presetServers testCfg in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} } opts' = diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index e0b1939c9e..63e46ea88c 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -7,8 +7,9 @@ module RandomServers where import Control.Monad (replicateM) import qualified Data.List.NonEmpty as L -import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers) +import Simplex.Chat (defaultChatConfig, randomPresetServers) import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Operators (operatorServers, operatorServersToUse) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec @@ -24,30 +25,32 @@ deriving instance Eq (ServerCfg p) testRandomSMPServers :: IO () testRandomSMPServers = do - [srvs1, srvs2, srvs3] <- - replicateM 3 $ - checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig - (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures + pure () + -- [srvs1, srvs2, srvs3] <- + -- replicateM 3 $ + -- checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig + -- (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures testRandomXFTPServers :: IO () testRandomXFTPServers = do - [srvs1, srvs2, srvs3] <- - replicateM 3 $ - checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig - (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True + pure () + -- [srvs1, srvs2, srvs3] <- + -- replicateM 3 $ + -- checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig + -- (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True -checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p] -checkEnabled p n allUsed (srvs, _) = do - let def = defaultServers defaultChatConfig - cfgSrvs = L.sortWith server' $ cfgServers p def - toUse = cfgServersToUse p def - srvs == cfgSrvs `shouldBe` allUsed - L.map enable srvs `shouldBe` L.map enable cfgSrvs - let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs - toUse `shouldBe` n - length enbldSrvs `shouldBe` n - pure enbldSrvs - where - server' ServerCfg {server = ProtoServerWithAuth srv _} = srv - enable :: forall p. ServerCfg p -> ServerCfg p - enable srv = (srv :: ServerCfg p) {enabled = False} +-- checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p] +-- checkEnabled p n allUsed (srvs, _) = do +-- let def = defaultServers defaultChatConfig +-- cfgSrvs = L.sortWith server' $ cfgServers p def +-- toUse = cfgServersToUse p def +-- srvs == cfgSrvs `shouldBe` allUsed +-- L.map enable srvs `shouldBe` L.map enable cfgSrvs +-- let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs +-- toUse `shouldBe` n +-- length enbldSrvs `shouldBe` n +-- pure enbldSrvs +-- where +-- server' ServerCfg {server = ProtoServerWithAuth srv _} = srv +-- enable :: forall p. ServerCfg p -> ServerCfg p +-- enable srv = (srv :: ServerCfg p) {enabled = False} From 84f7f901ead7156d8249fc19ec62ba04662ed80a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 10 Nov 2024 12:57:57 +0000 Subject: [PATCH 15/22] remove imports --- src/Simplex/Chat.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 85f039a6e8..f32714fe82 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -45,7 +45,7 @@ import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4) -import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) +import Data.List.NonEmpty (NonEmpty (..), toList, (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -100,7 +100,7 @@ import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), OperatorId, ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -301,6 +301,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} + -- TODO simpleNetCfg? ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} @@ -1847,7 +1848,6 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - cfg <- asks config newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (`getProtocolServers` newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do @@ -2580,7 +2580,6 @@ processChatCommand' vr = \case pure $ CRAgentSubsTotal user subsTotal hasSession GetAgentServersSummary userId -> withUserId userId $ \user -> do agentServersSummary <- lift $ withAgent' getAgentServersSummary - cfg <- asks config (users, smpServers, xftpServers) <- withStore' $ \db -> (,,) <$> getUsers db <*> getServers db user SPSMP <*> getServers db user SPXFTP let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers From af144c6208a23b678d5316e5c34a29b9640d8c8d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Sun, 10 Nov 2024 22:58:23 +0000 Subject: [PATCH 16/22] fix --- src/Simplex/Chat.hs | 122 +++++++++++++++++++---------- src/Simplex/Chat/Controller.hs | 8 +- src/Simplex/Chat/Mobile.hs | 3 +- src/Simplex/Chat/Operators.hs | 4 +- src/Simplex/Chat/Options.hs | 10 +-- src/Simplex/Chat/Store/Profiles.hs | 2 +- tests/ChatClient.hs | 27 ++++++- tests/ChatTests/Direct.hs | 12 +-- tests/ChatTests/Groups.hs | 8 +- tests/ChatTests/Profiles.hs | 9 ++- 10 files changed, 135 insertions(+), 70 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index f32714fe82..57a9768f06 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -197,6 +197,7 @@ defaultChatConfig = ntf = _defaultNtfServers, netCfg = defaultNetworkConfig }, + optionsServers = OptionsServers {smpServers = [], xftpServers = []}, tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, @@ -301,16 +302,17 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} - -- TODO simpleNetCfg? - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {optionsServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do 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, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} + PresetServers {netCfg} = presetServers + presetServers' = (presetServers :: PresetServers) {netCfg = updateNetworkConfig netCfg simpleNetCfg} + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', optionsServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user currentRemoteHost <- newTVarIO Nothing - servers <- withTransaction chatStore agentServers + servers <- withTransaction chatStore $ agentServers config smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom @@ -383,24 +385,22 @@ newChatController contactMergeEnabled } where - PresetServers {operators = presetOps, ntf, netCfg} = presetServers - agentServers :: DB.Connection -> IO InitialAgentServers - agentServers db = do + agentServers :: ChatConfig -> DB.Connection -> IO InitialAgentServers + agentServers config@ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} db = do users <- getUsers db opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) - smp' <- getUserServers SPSMP users opDomains smpServers - xftp' <- getUserServers SPXFTP users opDomains xftpServers + smp' <- getUserServers SPSMP users opDomains + xftp' <- getUserServers SPXFTP users opDomains pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> [ProtoServerWithAuth p] -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers p users opDomains = maybe get srvCfgs . L.nonEmpty + getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) + getUserServers p users opDomains = maybe get srvCfgs (L.nonEmpty $ optsServers config p) where get = do randomSrvs <- randomPresetServers p presetOps fmap M.fromList $ forM users $ \u -> - (aUserId u,) . useServers opDomains <$> getUpdateUserServers db p presetOps randomSrvs u - srvCfgs ss = pure $ M.fromList $ map (\u -> (aUserId u, L.map srvCfg ss)) users - srvCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} + (aUserId u,) . serverCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u + srvCfgs ss = pure $ M.fromList $ map (\u -> (aUserId u, L.map serverCfg ss)) users updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -443,6 +443,31 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} +useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [UserServer p] +useServers cfg p = \case + [] -> map userServer $ optsServers cfg p + srvs -> srvs + +-- TODO serverId? +userServer :: ProtoServerWithAuth p -> UserServer p +userServer server = UserServer {serverId = DBEntityId 0, server, preset = True, tested = Nothing, enabled = True} + +newUserServer :: ProtoServerWithAuth p -> NewUserServer p +newUserServer server = UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled = True} + +serverCfg :: ProtoServerWithAuth p -> ServerCfg p +serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} + +userProtoServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] +userProtoServers cfg p = \case + [] -> map protoServer $ optsServers cfg p + srvs -> map (\UserServer {server} -> protoServer server) srvs + +optsServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ProtoServerWithAuth p] +optsServers ChatConfig {optionsServers = OptionsServers {smpServers, xftpServers}} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> IO (NonEmpty (NewUserServer p)) randomPresetServers p = fmap fold1 . mapM opSrvs where @@ -603,8 +628,8 @@ processChatCommand' vr = \case p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser opDomains <- operatorDomains . fst <$> withFastStore getServerOperators - (smp, smpServers_) <- chooseServers SPSMP opDomains - (xftp, xftpServers_) <- chooseServers SPXFTP opDomains + (smp, smpServers) <- chooseServers SPSMP opDomains + (xftp, xftpServers) <- chooseServers SPXFTP opDomains users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ @@ -615,8 +640,8 @@ processChatCommand' vr = \case createPresetContactCards user `catchChatError` \_ -> pure () withFastStore $ \db -> do createNoteFolder db user - liftIO $ mapM_ (mapM_ (insertProtocolServer db SPSMP user ts)) smpServers_ - liftIO $ mapM_ (mapM_ (insertProtocolServer db SPXFTP user ts)) xftpServers_ + liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) smpServers + liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) xftpServers atomically . writeTVar u $ Just user pure $ CRActiveUser user where @@ -625,15 +650,19 @@ processChatCommand' vr = \case withFastStore $ \db -> do createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile - chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [(Text, ServerOperator)] -> CM (NonEmpty (ServerCfg p), Maybe (NonEmpty (NewUserServer p))) + chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [(Text, ServerOperator)] -> CM (NonEmpty (ServerCfg p), NonEmpty (NewUserServer p)) chooseServers p opDomains = do - PresetServers {operators = presetOps} <- asks $ presetServers . config - randomSrvs <- liftIO $ randomPresetServers p presetOps - chatReadVar currentUser >>= \case - Nothing -> pure (useServers opDomains randomSrvs, Just randomSrvs) - Just user -> do - srvs <- withFastStore' $ \db -> getUpdateUserServers db p presetOps randomSrvs user - pure (useServers opDomains srvs, Nothing) + cfg <- asks config + case L.nonEmpty $ optsServers cfg p of + Just srvs -> pure (L.map serverCfg srvs, L.map newUserServer srvs) + Nothing -> do + PresetServers {operators = presetOps} <- asks $ presetServers . config + randomSrvs <- liftIO $ randomPresetServers p presetOps + chatReadVar currentUser >>= \case + Nothing -> pure (serverCfgs opDomains randomSrvs, randomSrvs) + Just user -> do + srvs <- withFastStore' $ \db -> getUpdateUserServers db p presetOps randomSrvs user + pure (serverCfgs opDomains srvs, L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs) coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1556,15 +1585,17 @@ processChatCommand' vr = \case APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do liftIO $ setServerOperators db operatorsEnabled uncurry CRServerOperators <$> getServerOperators db - APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do - (operators, _) <- getServerOperators db - liftIO $ do - smpServers <- getServers db user SPSMP - xftpServers <- getServers db user SPXFTP - CRUserServers user <$> groupByOperator operators smpServers xftpServers + APIGetUserServers userId -> withUserId userId $ \user -> do + cfg <- asks config + withFastStore $ \db -> do + (operators, _) <- getServerOperators db + liftIO $ do + smpServers <- getServers db user cfg SPSMP + xftpServers <- getServers db user cfg SPXFTP + CRUserServers user <$> groupByOperator operators smpServers xftpServers where - getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [UserServer p] - getServers db user _p = getProtocolServers db user + getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> ChatConfig -> SProtocolType p -> IO [UserServer p] + getServers db user cfg p = useServers cfg p <$> getProtocolServers db user APISetUserServers userId userServers -> withUserId userId $ \user -> do let errors = validateUserServers userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) @@ -1848,7 +1879,10 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (`getProtocolServers` newUser) + cfg <- asks config + liftIO $ putStrLn $ "smpServer " <> show smpServer + newUserServers <- userProtoServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + liftIO $ putStrLn $ "newUserServers " <> show newUserServers pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2580,13 +2614,16 @@ processChatCommand' vr = \case pure $ CRAgentSubsTotal user subsTotal hasSession GetAgentServersSummary userId -> withUserId userId $ \user -> do agentServersSummary <- lift $ withAgent' getAgentServersSummary - (users, smpServers, xftpServers) <- - withStore' $ \db -> (,,) <$> getUsers db <*> getServers db user SPSMP <*> getServers db user SPXFTP - let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers - pure $ CRAgentServersSummary user presentedServersSummary + cfg <- asks config + withStore' $ \db -> do + users <- getUsers db + smpServers <- getServers db user cfg SPSMP + xftpServers <- getServers db user cfg SPXFTP + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers + pure $ CRAgentServersSummary user presentedServersSummary where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] - getServers db user _p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db user + getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> ChatConfig -> SProtocolType p -> IO [ProtocolServer p] + getServers db user cfg p = userProtoServers cfg p <$> getProtocolServers db user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3704,7 +3741,8 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (`getProtocolServers` user) + cfg <- asks config + knownSrvs <- userProtoServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index d974881753..1c28c304a6 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -84,7 +84,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, QueueId, XFTPServerWithAuth, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -133,6 +133,7 @@ data ChatConfig = ChatConfig chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, presetServers :: PresetServers, + optionsServers :: OptionsServers, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -154,6 +155,11 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } +data OptionsServers = OptionsServers + { smpServers :: [SMPServerWithAuth], + xftpServers :: [XFTPServerWithAuth] + } + -- The hooks can be used to extend or customize chat core in mobile or CLI clients. data ChatHooks = ChatHooks { -- preCmdHook can be used to process or modify the commands before they are processed. diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 57b0ee6c17..f6566c5a6d 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -189,8 +189,7 @@ mobileChatOpts dbFilePrefix = CoreChatOpts { dbFilePrefix, dbKey = "", -- for API database is already opened, and the key in options is not used - smpServers = [], - xftpServers = [], + optionsServers = OptionsServers [] [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index eb85752909..6e197f7af6 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -290,8 +290,8 @@ updatedUserServers p presetOps randomSrvs = \case srvHost :: UserServer' s p -> NonEmpty TransportHost srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv -useServers :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) -useServers opDomains = L.map agentServer +serverCfgs :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) +serverCfgs opDomains = L.map agentServer where agentServer :: UserServer' s p -> ServerCfg p agentServer srv@UserServer {server, enabled} = diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 16ffe6e28f..cb54f14e0e 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -27,12 +27,12 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), OptionsServers (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth) import System.FilePath (combine) @@ -56,8 +56,7 @@ data ChatOpts = ChatOpts data CoreChatOpts = CoreChatOpts { dbFilePrefix :: String, dbKey :: ScrubbedBytes, - smpServers :: [SMPServerWithAuth], - xftpServers :: [XFTPServerWithAuth], + optionsServers :: OptionsServers, simpleNetCfg :: SimpleNetCfg, logLevel :: ChatLogLevel, logConnections :: Bool, @@ -244,8 +243,7 @@ coreChatOptsP appDir defaultDbFileName = do CoreChatOpts { dbFilePrefix, dbKey, - smpServers, - xftpServers, + optionsServers = OptionsServers {smpServers, xftpServers}, simpleNetCfg = SimpleNetCfg { socksProxy, diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 9b64a61e31..b2f265bc0d 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -548,7 +548,7 @@ getUpdateUserServers db p presetOps randomSrvs user = do [sql| UPDATE protocol_servers SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, - preset = ?, tested = ?, enabled = ?, updated_at + preset = ?, tested = ?, enabled = ?, updated_at = ? WHERE smp_server_id = ? |] (serverColumns p server :. (preset, tested, enabled, ts, serverId)) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index d435af186e..cfe8cf60f4 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -21,13 +21,15 @@ import Control.Monad.Reader import Data.ByteArray (ScrubbedBytes) import Data.Functor (($>)) import Data.List (dropWhileEnd, find) +import qualified Data.List.NonEmpty as L import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), OptionsServers (..), PresetServers (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options +import Simplex.Chat.Operators (PresetOperator (..), presetServer) import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) import Simplex.Chat.Store import Simplex.Chat.Store.Profiles @@ -94,8 +96,8 @@ testCoreOpts = { dbFilePrefix = "./simplex_v1", dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", - smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], - xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + -- optionsServers = testOptsServers, + optionsServers = OptionsServers [] [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, @@ -107,6 +109,13 @@ testCoreOpts = yesToUpMigrations = False } +testOptsServers :: OptionsServers +testOptsServers = + OptionsServers + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"] + } + getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = testCoreOpts {dbKey}} @@ -149,6 +158,18 @@ testCfg :: ChatConfig testCfg = defaultChatConfig { agentConfig = testAgentCfg, + presetServers = + (presetServers defaultChatConfig) + { operators = + [ PresetOperator + { operator = operatorSimpleXChat, + smp = L.map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + useSMP = 1, + xftp = L.map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + useXFTP = 1 + } + ] + }, showReceipts = False, testView = True, tbqSize = 16 diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7d5dc67d24..7b34f19bc2 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -25,7 +25,7 @@ import Database.SQLite.Simple (Only (..)) import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..), PresetServers (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -79,10 +79,10 @@ chatDirectTests = do it "own invitation link" testPlanInvitationLinkOwn it "connecting via invitation link" testPlanInvitationLinkConnecting describe "SMP servers" $ do - it "get and set SMP servers" testGetSetSMPServers + xit "get and set SMP servers" testGetSetSMPServers it "test SMP server connection" testTestSMPServerConnection describe "XFTP servers" $ do - it "get and set XFTP servers" testGetSetXFTPServers + xit "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer describe "async connection handshake" $ do describe "connect when initiating client goes offline" $ do @@ -116,7 +116,7 @@ chatDirectTests = do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses - it "create user with same servers" testCreateUserSameServers + xit "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration @@ -271,7 +271,7 @@ testRetryConnecting tmp = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile te testOpts { coreOptions = testCoreOpts - { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} } } @@ -340,7 +340,7 @@ testRetryConnectingClientTimeout tmp = do testOpts { coreOptions = testCoreOpts - { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} } } diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index f1a36c8722..979f696d15 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1,8 +1,10 @@ +{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Groups where @@ -15,7 +17,7 @@ import qualified Data.ByteString.Char8 as B import Data.List (intercalate, isInfixOf) import qualified Data.Text as T import Database.SQLite.Simple (Only (..)) -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -6502,7 +6504,7 @@ testGroupMemberInactive tmp = do opts' = testOpts { coreOptions = - testCoreOpts - { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + (testCoreOpts :: CoreChatOpts) + { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} } } diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 06ed9aa5bc..d6ee04baa5 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -2,6 +2,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PostfixOperators #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module ChatTests.Profiles where @@ -14,7 +15,7 @@ import Control.Monad.Except import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..)) import Simplex.Chat.Options import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Types (ConnStatus (..), Profile (..)) @@ -75,7 +76,7 @@ chatProfileTests = do it "change user for pending connection" testChangePCCUser it "change from incognito profile connects as new user" testChangePCCUserFromIncognito it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito - it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv + xit "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv describe "preferences" $ do it "set contact preferences" testSetContactPrefs it "feature offers" testFeatureOffers @@ -313,8 +314,8 @@ testRetryAcceptingViaContactLink tmp = testChatCfgOpts2 cfg' opts' aliceProfile opts' = testOpts { coreOptions = - testCoreOpts - { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + (testCoreOpts :: CoreChatOpts) + { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} } } From bd4745775d6c1ca97ad3d25ff5f3b43df795a75d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 11 Nov 2024 11:34:02 +0000 Subject: [PATCH 17/22] update --- src/Simplex/Chat.hs | 74 +++++++++++------------------- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Operators.hs | 9 ---- src/Simplex/Chat/Store/Profiles.hs | 67 ++++++++++++++------------- src/Simplex/Chat/Store/Shared.hs | 1 + 5 files changed, 65 insertions(+), 90 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 57a9768f06..cc70abf864 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -386,7 +386,7 @@ newChatController } where agentServers :: ChatConfig -> DB.Connection -> IO InitialAgentServers - agentServers config@ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} db = do + agentServers ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} db = do users <- getUsers db opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) smp' <- getUserServers SPSMP users opDomains @@ -394,13 +394,10 @@ newChatController pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers p users opDomains = maybe get srvCfgs (L.nonEmpty $ optsServers config p) - where - get = do - randomSrvs <- randomPresetServers p presetOps - fmap M.fromList $ forM users $ \u -> - (aUserId u,) . serverCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u - srvCfgs ss = pure $ M.fromList $ map (\u -> (aUserId u, L.map serverCfg ss)) users + getUserServers p users opDomains = do + randomSrvs <- randomPresetServers p presetOps + fmap M.fromList $ forM users $ \u -> + (aUserId u,) . serverCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -443,23 +440,11 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} -useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [UserServer p] -useServers cfg p = \case - [] -> map userServer $ optsServers cfg p - srvs -> srvs - --- TODO serverId? -userServer :: ProtoServerWithAuth p -> UserServer p -userServer server = UserServer {serverId = DBEntityId 0, server, preset = True, tested = Nothing, enabled = True} - -newUserServer :: ProtoServerWithAuth p -> NewUserServer p -newUserServer server = UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled = True} - serverCfg :: ProtoServerWithAuth p -> ServerCfg p serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} -userProtoServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] -userProtoServers cfg p = \case +useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] +useServers cfg p = \case [] -> map protoServer $ optsServers cfg p srvs -> map (\UserServer {server} -> protoServer server) srvs @@ -663,6 +648,8 @@ processChatCommand' vr = \case Just user -> do srvs <- withFastStore' $ \db -> getUpdateUserServers db p presetOps randomSrvs user pure (serverCfgs opDomains srvs, L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs) + newUserServer :: ProtoServerWithAuth p -> NewUserServer p + newUserServer server = UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled = True} coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1585,17 +1572,12 @@ processChatCommand' vr = \case APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do liftIO $ setServerOperators db operatorsEnabled uncurry CRServerOperators <$> getServerOperators db - APIGetUserServers userId -> withUserId userId $ \user -> do - cfg <- asks config - withFastStore $ \db -> do - (operators, _) <- getServerOperators db - liftIO $ do - smpServers <- getServers db user cfg SPSMP - xftpServers <- getServers db user cfg SPXFTP - CRUserServers user <$> groupByOperator operators smpServers xftpServers - where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> ChatConfig -> SProtocolType p -> IO [UserServer p] - getServers db user cfg p = useServers cfg p <$> getProtocolServers db user + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do + (operators, _) <- getServerOperators db + liftIO $ do + smpServers <- getProtocolServers @'PSMP db user + xftpServers <- getProtocolServers @'PXFTP db user + CRUserServers user <$> groupByOperator operators smpServers xftpServers APISetUserServers userId userServers -> withUserId userId $ \user -> do let errors = validateUserServers userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) @@ -1617,16 +1599,14 @@ processChatCommand' vr = \case conditionsText = usageConditionsText, acceptedConditions } - APISetConditionsNotified conditionsId -> do + APISetConditionsNotified condId -> do currentTs <- liftIO getCurrentTime - withFastStore' $ \db -> setConditionsNotified db conditionsId currentTs + withFastStore' $ \db -> setConditionsNotified db condId currentTs ok_ - -- TODO switch to IDs - APIAcceptConditions conditionsId operators -> withFastStore $ \db -> do + APIAcceptConditions condId opIds -> withFastStore $ \db -> do currentTs <- liftIO getCurrentTime - operators' <- L.toList <$> acceptConditions db conditionsId operators currentTs - currentConds <- getCurrentUsageConditions db - pure $ CRServerOperators operators' $ usageConditionsAction operators' currentConds currentTs + acceptConditions db condId opIds currentTs + uncurry CRServerOperators <$> getServerOperators db APISetChatItemTTL userId newTTL_ -> withUserId userId $ \user -> checkStoreNotChanged $ withChatLock "setChatItemTTL" $ do @@ -1880,9 +1860,7 @@ processChatCommand' vr = \case let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q cfg <- asks config - liftIO $ putStrLn $ "smpServer " <> show smpServer - newUserServers <- userProtoServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) - liftIO $ putStrLn $ "newUserServers " <> show newUserServers + newUserServers <- useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2623,7 +2601,7 @@ processChatCommand' vr = \case pure $ CRAgentServersSummary user presentedServersSummary where getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> ChatConfig -> SProtocolType p -> IO [ProtocolServer p] - getServers db user cfg p = userProtoServers cfg p <$> getProtocolServers db user + getServers db user cfg p = useServers cfg p <$> getProtocolServers db user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3742,7 +3720,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do cfg <- asks config - knownSrvs <- userProtoServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) + knownSrvs <- useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -8222,12 +8200,12 @@ chatCommandP = -- "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, "/_operators " *> (APISetServerOperators <$> jsonP), - "/_user_servers " *> (APIGetUserServers <$> A.decimal), - "/_user_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), + "/_servers " *> (APIGetUserServers <$> A.decimal), + "/_servers " *> (APISetUserServers <$> A.decimal <* A.space <*> jsonP), "/_validate_servers " *> (APIValidateServers <$> jsonP), "/_conditions" $> APIGetUsageConditions, "/_conditions_notified " *> (APISetConditionsNotified <$> A.decimal), - "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <* A.space <*> jsonP), + "/_accept_conditions " *> (APIAcceptConditions <$> A.decimal <*> _strP), "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 1c28c304a6..f41ed26e98 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -356,13 +356,13 @@ data ChatCommand APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators - | APISetServerOperators (NonEmpty OperatorEnabled) + | APISetServerOperators (NonEmpty ServerOperator) | APIGetUserServers UserId | APISetUserServers UserId (NonEmpty UserOperatorServers) | APIValidateServers (NonEmpty UserOperatorServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 - | APIAcceptConditions Int64 (NonEmpty ServerOperator) -- TODO replace with IDs + | APIAcceptConditions Int64 (NonEmpty Int64) | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 6e197f7af6..256654bea8 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -178,13 +178,6 @@ conditionsAccepted ServerOperator {conditionsAcceptance} = case conditionsAccept CAAccepted {} -> True _ -> False -data OperatorEnabled = OperatorEnabled - { operatorId' :: OperatorId, - enabled' :: Bool, - roles' :: ServerRoles - } - deriving (Show) - data UserOperatorServers = UserOperatorServers { operator :: Maybe ServerOperator, smpServers :: [UserServer 'PSMP], @@ -373,8 +366,6 @@ instance ToJSON ServerOperator where instance FromJSON ServerOperator where parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator') -$(JQ.deriveJSON defaultJSON ''OperatorEnabled) - $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) instance ProtocolTypeI p => ToJSON (UserServer p) where diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index b2f265bc0d..434a247e1e 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -620,13 +620,13 @@ getServerOperators db = do operators <- mapM getConds =<< getServerOperators_ db pure (operators, usageConditionsAction operators currentConds now) -setServerOperators :: DB.Connection -> NonEmpty OperatorEnabled -> IO () +setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO () setServerOperators db = - mapM_ $ \OperatorEnabled {operatorId', enabled', roles' = ServerRoles {storage, proxy}} -> + mapM_ $ \ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> DB.execute db "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" - (enabled', storage, proxy, operatorId') + (enabled, storage, proxy, operatorId) getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] getUpdateServerOperators db presetOps newUser = do @@ -685,28 +685,29 @@ getUpdateServerOperators db presetOps newUser = do acceptConditions_ db op conditionsCommit Nothing $> op {conditionsAcceptance = CAAccepted Nothing} +serverOperatorQuery :: Query +serverOperatorQuery = + [sql| + SELECT server_operator_id, server_operator_tag, trade_name, legal_name, + server_domains, enabled, role_storage, role_proxy + FROM server_operators + |] + getServerOperators_ :: DB.Connection -> IO [ServerOperator] -getServerOperators_ db = - map toOperator - <$> DB.query_ - db - [sql| - SELECT server_operator_id, server_operator_tag, trade_name, legal_name, - server_domains, enabled, role_storage, role_proxy - FROM server_operators - |] - where - toOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = - ServerOperator - { operatorId, - operatorTag, - tradeName, - legalName, - serverDomains = T.splitOn "," domains, - conditionsAcceptance = CARequired Nothing, - enabled, - roles = ServerRoles {storage, proxy} - } +getServerOperators_ db = map toServerOperator <$> DB.query_ db serverOperatorQuery + +toServerOperator :: (DBEntityId, Maybe OperatorTag, Text, Maybe Text, Text, Bool, Bool, Bool) -> ServerOperator +toServerOperator (operatorId, operatorTag, tradeName, legalName, domains, enabled, storage, proxy) = + ServerOperator + { operatorId, + operatorTag, + tradeName, + legalName, + serverDomains = T.splitOn "," domains, + conditionsAcceptance = CARequired Nothing, + enabled, + roles = ServerRoles {storage, proxy} + } getOperatorConditions_ :: DB.Connection -> ServerOperator -> UsageConditions -> Maybe UsageConditions -> UTCTime -> IO ConditionsAcceptance getOperatorConditions_ db ServerOperator {operatorId} UsageConditions {conditionsCommit = currentCommit, createdAt, notifiedAt} latestAcceptedConds_ now = do @@ -766,15 +767,19 @@ getLatestAcceptedConditions db = |] setConditionsNotified :: DB.Connection -> Int64 -> UTCTime -> IO () -setConditionsNotified db conditionsId notifiedAt = - DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, conditionsId) +setConditionsNotified db condId notifiedAt = + DB.execute db "UPDATE usage_conditions SET notified_at = ? WHERE usage_conditions_id = ?" (notifiedAt, condId) -acceptConditions :: DB.Connection -> Int64 -> NonEmpty ServerOperator -> UTCTime -> ExceptT StoreError IO (NonEmpty ServerOperator) -acceptConditions db conditionsId operators acceptedAt = do - UsageConditions {conditionsCommit} <- getUsageConditionsById_ db conditionsId +acceptConditions :: DB.Connection -> Int64 -> NonEmpty Int64 -> UTCTime -> ExceptT StoreError IO () +acceptConditions db condId opIds acceptedAt = do + UsageConditions {conditionsCommit} <- getUsageConditionsById_ db condId + operators <- mapM getServerOperator_ opIds let ts = Just acceptedAt - liftIO $ forM operators $ \op -> - acceptConditions_ db op conditionsCommit ts $> op {conditionsAcceptance = CAAccepted ts} + liftIO $ forM_ operators $ \op -> acceptConditions_ db op conditionsCommit ts + where + getServerOperator_ opId = + ExceptT $ firstRow toServerOperator (SEOperatorNotFound opId) $ + DB.query db (serverOperatorQuery <> " WHERE operator_id = ?") (Only opId) acceptConditions_ :: DB.Connection -> ServerOperator -> Text -> Maybe UTCTime -> IO () acceptConditions_ db ServerOperator {operatorId, operatorTag} conditionsCommit acceptedAt = diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 083079e2ea..fcd9896917 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -127,6 +127,7 @@ data StoreError | SERemoteCtrlNotFound {remoteCtrlId :: RemoteCtrlId} | SERemoteCtrlDuplicateCA | SEProhibitedDeleteUser {userId :: UserId, contactId :: ContactId} + | SEOperatorNotFound {serverOperatorId :: Int64} | SEUsageConditionsNotFound deriving (Show, Exception) From d0a7e14a96f88fbb814baee8b414c340f1a560ea Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 11 Nov 2024 15:15:00 +0000 Subject: [PATCH 18/22] make preset servers lists potentially empty in some operators, as long as the combined list is not empty --- src/Simplex/Chat.hs | 139 ++++++++++++++++++--------------- src/Simplex/Chat/Controller.hs | 10 +-- src/Simplex/Chat/Mobile.hs | 3 +- src/Simplex/Chat/Operators.hs | 54 +++++++------ src/Simplex/Chat/Options.hs | 10 ++- src/Simplex/Chat/Terminal.hs | 6 +- tests/ChatClient.hs | 20 ++--- tests/ChatTests/Direct.hs | 6 +- tests/ChatTests/Groups.hs | 6 +- tests/ChatTests/Profiles.hs | 6 +- 10 files changed, 135 insertions(+), 125 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cc70abf864..bcdef12fe2 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -40,12 +40,11 @@ import Data.Constraint (Dict (..)) import Data.Either (fromRight, lefts, partitionEithers, rights) import Data.Fixed (div') import Data.Foldable (foldr') -import Data.Foldable1 (fold1) import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn, zipWith4) -import Data.List.NonEmpty (NonEmpty (..), toList, (<|)) +import Data.List.NonEmpty (NonEmpty (..), (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M @@ -180,14 +179,14 @@ defaultChatConfig = PresetServers { operators = [ PresetOperator - { operator = operatorSimpleXChat, + { operator = Just operatorSimpleXChat, smp = simplexChatSMPServers, useSMP = 4, - xftp = L.map (presetServer True) defaultXFTPServers, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, useXFTP = 3 }, PresetOperator - { operator = operatorXYZ, + { operator = Just operatorXYZ, smp = xyzSMPServers, useSMP = 3, xftp = xyzXFTPServers, @@ -197,7 +196,6 @@ defaultChatConfig = ntf = _defaultNtfServers, netCfg = defaultNetworkConfig }, - optionsServers = OptionsServers {smpServers = [], xftpServers = []}, tbqSize = 1024, fileChunkSize = 15780, -- do not change xftpDescrPartSize = 14000, @@ -219,9 +217,9 @@ defaultChatConfig = chatHooks = defaultChatHooks } -simplexChatSMPServers :: NonEmpty (NewUserServer 'PSMP) +simplexChatSMPServers :: [NewUserServer 'PSMP] simplexChatSMPServers = - L.map + map (presetServer True) [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", @@ -235,16 +233,16 @@ simplexChatSMPServers = "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" ] - <> L.map + <> map (presetServer False) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ] -xyzSMPServers :: NonEmpty (NewUserServer 'PSMP) +xyzSMPServers :: [NewUserServer 'PSMP] xyzSMPServers = - L.map + map (presetServer True) [ "smp://abcd@smp1.xyz.com", "smp://abcd@smp2.xyz.com", @@ -254,9 +252,9 @@ xyzSMPServers = "smp://abcd@smp6.xyz.com" ] -xyzXFTPServers :: NonEmpty (NewUserServer 'PXFTP) +xyzXFTPServers :: [NewUserServer 'PXFTP] xyzXFTPServers = - L.map + map (presetServer True) [ "xftp://abcd@xftp1.xyz.com", "xftp://abcd@xftp2.xyz.com", @@ -302,17 +300,18 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, presetServers, inlineFiles, deviceNameForRemote, confirmMigrations} - ChatOpts {coreOptions = CoreChatOpts {optionsServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations - PresetServers {netCfg} = presetServers - presetServers' = (presetServers :: PresetServers) {netCfg = updateNetworkConfig netCfg simpleNetCfg} - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', optionsServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, presetServers = presetServers', inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user + randomSMP <- randomPresetServers SPSMP presetServers' + randomXFTP <- randomPresetServers SPXFTP presetServers' + let randomServers = RandomServers {smpServers = randomSMP, xftpServers = randomXFTP} currentRemoteHost <- newTVarIO Nothing - servers <- withTransaction chatStore $ agentServers config + servers <- withTransaction chatStore $ \db -> agentServers db config randomServers smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom @@ -348,6 +347,7 @@ newChatController ChatController { firstTime, currentUser, + randomServers, currentRemoteHost, smpAgent, agentAsync, @@ -385,8 +385,28 @@ newChatController contactMergeEnabled } where - agentServers :: ChatConfig -> DB.Connection -> IO InitialAgentServers - agentServers ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} db = do + presetServers' :: PresetServers + presetServers' = presetServers {operators = operators', netCfg = netCfg'} + where + PresetServers {operators, netCfg} = presetServers + netCfg' = updateNetworkConfig netCfg simpleNetCfg + operators' = case (smpServers, xftpServers) of + ([], []) -> operators + (smpSrvs, []) -> L.map removeSMP operators <> [custom smpSrvs []] + ([], xftpSrvs) -> L.map removeXFTP operators <> [custom [] xftpSrvs] + (smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs] + removeSMP op = (op :: PresetOperator) {smp = []} + removeXFTP op = (op :: PresetOperator) {xftp = []} + custom smpSrvs xftpSrvs = + PresetOperator + { operator = Nothing, + smp = map (presetServer True) smpSrvs, + useSMP = 0, + xftp = map (presetServer True) xftpSrvs, + useXFTP = 0 + } + agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers + agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} randomServers = do users <- getUsers db opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) smp' <- getUserServers SPSMP users opDomains @@ -395,9 +415,9 @@ newChatController where getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) getUserServers p users opDomains = do - randomSrvs <- randomPresetServers p presetOps + let randomSrvs = rndServers p randomServers fmap M.fromList $ forM users $ \u -> - (aUserId u,) . serverCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u + (aUserId u,) . agentServerCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -443,31 +463,33 @@ withFileLock name = withEntityLock name . CLFile serverCfg :: ProtoServerWithAuth p -> ServerCfg p serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} -useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] -useServers cfg p = \case - [] -> map protoServer $ optsServers cfg p - srvs -> map (\UserServer {server} -> protoServer server) srvs +-- useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] +-- useServers cfg p = \case +-- [] -> map protoServer $ optsServers cfg p +-- srvs -> map (\UserServer {server} -> protoServer server) srvs -optsServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ProtoServerWithAuth p] -optsServers ChatConfig {optionsServers = OptionsServers {smpServers, xftpServers}} = \case +rndServers :: UserProtocol p => SProtocolType p -> RandomServers -> NonEmpty (NewUserServer p) +rndServers p RandomServers {smpServers, xftpServers} = case p of SPSMP -> smpServers SPXFTP -> xftpServers -randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> IO (NonEmpty (NewUserServer p)) -randomPresetServers p = fmap fold1 . mapM opSrvs +randomPresetServers :: forall p. UserProtocol p => SProtocolType p -> PresetServers -> IO (NonEmpty (NewUserServer p)) +randomPresetServers p PresetServers {operators} = toJust . L.nonEmpty . concat =<< mapM opSrvs operators where - opSrvs :: PresetOperator -> IO (NonEmpty (NewUserServer p)) + toJust = \case + Just a -> pure a + Nothing -> E.throwIO $ userError "no preset servers" + opSrvs :: PresetOperator -> IO [NewUserServer p] opSrvs op = do let srvs = operatorServers p op - (enbldSrvs, dsbldSrvs) = L.partition (\UserServer {enabled} -> enabled) srvs toUse = operatorServersToUse p op - if length enbldSrvs <= toUse + (enbldSrvs, dsbldSrvs) = partition (\UserServer {enabled} -> enabled) srvs + if toUse <= 0 || toUse >= length enbldSrvs then pure srvs else do (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs let dsbldSrvs' = map (\srv -> (srv :: NewUserServer p) {enabled = False}) srvsToDisable - srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs - pure $ fromMaybe srvs $ L.nonEmpty srvs' + pure $ sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs server' UserServer {server = ProtoServerWithAuth srv _} = srv -- enableSndFiles has no effect when mainApp is True @@ -612,13 +634,15 @@ processChatCommand' vr = \case forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser - opDomains <- operatorDomains . fst <$> withFastStore getServerOperators - (smp, smpServers) <- chooseServers SPSMP opDomains - (xftp, xftpServers) <- chooseServers SPXFTP opDomains + smpServers <- chooseServers SPSMP + xftpServers <- chooseServers SPXFTP users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} + opDomains <- operatorDomains . fst <$> withFastStore getServerOperators + let smp = agentServerCfgs opDomains smpServers + xftp = agentServerCfgs opDomains xftpServers auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts @@ -635,21 +659,13 @@ processChatCommand' vr = \case withFastStore $ \db -> do createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile - chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [(Text, ServerOperator)] -> CM (NonEmpty (ServerCfg p), NonEmpty (NewUserServer p)) - chooseServers p opDomains = do - cfg <- asks config - case L.nonEmpty $ optsServers cfg p of - Just srvs -> pure (L.map serverCfg srvs, L.map newUserServer srvs) - Nothing -> do - PresetServers {operators = presetOps} <- asks $ presetServers . config - randomSrvs <- liftIO $ randomPresetServers p presetOps - chatReadVar currentUser >>= \case - Nothing -> pure (serverCfgs opDomains randomSrvs, randomSrvs) - Just user -> do - srvs <- withFastStore' $ \db -> getUpdateUserServers db p presetOps randomSrvs user - pure (serverCfgs opDomains srvs, L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs) - newUserServer :: ProtoServerWithAuth p -> NewUserServer p - newUserServer server = UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled = True} + chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (NewUserServer p)) + chooseServers p = + chatReadVar currentUser + $>>= (fmap L.nonEmpty . withFastStore' . flip getProtocolServers) + >>= \case + Nothing -> rndServers p <$> asks randomServers + Just srvs -> pure $ L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1859,8 +1875,7 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - cfg <- asks config - newUserServers <- useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (`getProtocolServers` newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2592,16 +2607,15 @@ processChatCommand' vr = \case pure $ CRAgentSubsTotal user subsTotal hasSession GetAgentServersSummary userId -> withUserId userId $ \user -> do agentServersSummary <- lift $ withAgent' getAgentServersSummary - cfg <- asks config withStore' $ \db -> do users <- getUsers db - smpServers <- getServers db user cfg SPSMP - xftpServers <- getServers db user cfg SPXFTP + smpServers <- getServers db user SPSMP + xftpServers <- getServers db user SPXFTP let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers pure $ CRAgentServersSummary user presentedServersSummary where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> ChatConfig -> SProtocolType p -> IO [ProtocolServer p] - getServers db user cfg p = useServers cfg p <$> getProtocolServers db user + getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] + getServers db user _p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3719,8 +3733,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - cfg <- asks config - knownSrvs <- useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) + knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (`getProtocolServers` user) pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -5025,7 +5038,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (Just fileDescrText, Just msgId) -> do partSize <- asks $ xftpDescrPartSize . config let parts = splitFileDescr partSize fileDescrText - pure . toList $ L.map (XMsgFileDescr msgId) parts + pure . L.toList $ L.map (XMsgFileDescr msgId) parts _ -> pure [] let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents GroupMember {memberId} = sender diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index f41ed26e98..2c062f757d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -84,7 +84,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, QueueId, XFTPServerWithAuth, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, MsgId, NMsgMeta (..), NtfServer, ProtocolType (..), QueueId, SMPMsgMeta (..), SubscriptionMode (..), XFTPServer) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost) @@ -133,7 +133,6 @@ data ChatConfig = ChatConfig chatVRange :: VersionRangeChat, confirmMigrations :: MigrationConfirmation, presetServers :: PresetServers, - optionsServers :: OptionsServers, tbqSize :: Natural, fileChunkSize :: Integer, xftpDescrPartSize :: Int, @@ -155,9 +154,9 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } -data OptionsServers = OptionsServers - { smpServers :: [SMPServerWithAuth], - xftpServers :: [XFTPServerWithAuth] +data RandomServers = RandomServers + { smpServers :: NonEmpty (NewUserServer 'PSMP), + xftpServers :: NonEmpty (NewUserServer 'PXFTP) } -- The hooks can be used to extend or customize chat core in mobile or CLI clients. @@ -206,6 +205,7 @@ data ChatDatabase = ChatDatabase {chatStore :: SQLiteStore, agentStore :: SQLite data ChatController = ChatController { currentUser :: TVar (Maybe User), + randomServers :: RandomServers, currentRemoteHost :: TVar (Maybe RemoteHostId), firstTime :: Bool, smpAgent :: AgentClient, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index f6566c5a6d..57b0ee6c17 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -189,7 +189,8 @@ mobileChatOpts dbFilePrefix = CoreChatOpts { dbFilePrefix, dbKey = "", -- for API database is already opened, and the key in options is not used - optionsServers = OptionsServers [] [], + smpServers = [], + xftpServers = [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 256654bea8..59b079bcfc 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -22,7 +22,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import Data.FileEmbed -import Data.Foldable1 (foldMap1) +import Data.Foldable (foldMap') import Data.IORef import Data.Int (Int64) import Data.List (find, foldl') @@ -42,7 +42,7 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) import Simplex.Chat.Operators.Conditions import Simplex.Chat.Types.Util (textParseJSON) -import Simplex.Messaging.Agent.Env.SQLite (OperatorId, ServerCfg (..), ServerRoles (..), allRoles) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) @@ -201,14 +201,14 @@ data UserServer' s p = UserServer deriving (Show) data PresetOperator = PresetOperator - { operator :: NewServerOperator, - smp :: NonEmpty (NewUserServer 'PSMP), + { operator :: Maybe NewServerOperator, + smp :: [NewUserServer 'PSMP], useSMP :: Int, - xftp :: NonEmpty (NewUserServer 'PXFTP), + xftp :: [NewUserServer 'PXFTP], useXFTP :: Int } -operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> NonEmpty (NewUserServer p) +operatorServers :: UserProtocol p => SProtocolType p -> PresetOperator -> [NewUserServer p] operatorServers p PresetOperator {smp, xftp} = case p of SPSMP -> smp SPXFTP -> xftp @@ -255,36 +255,38 @@ updatedServerOperators presetOps storedOps = <> map (ASO SDBStored) (filter (isNothing . operatorTag) storedOps) where -- TODO remove domains of preset operators from custom - addPreset PresetOperator {operator = presetOp} = (storedOp' :) - where - storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of - Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> - ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles} - Nothing -> ASO SDBNew presetOp + addPreset PresetOperator {operator} = case operator of + Nothing -> id + Just presetOp -> (storedOp' :) + where + storedOp' = case find ((operatorTag presetOp ==) . operatorTag) storedOps of + Just ServerOperator {operatorId, conditionsAcceptance, enabled, roles} -> + ASO SDBStored presetOp {operatorId, conditionsAcceptance, enabled, roles} + Nothing -> ASO SDBNew presetOp -- This function should be used inside DB transaction to update servers. updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) -updatedUserServers p presetOps randomSrvs = \case - [] -> L.map (AUS SDBNew) randomSrvs - srvs -> - L.map (userServer storedSrvs) presetSrvs - `L.appendList` map (AUS SDBStored) (filter customServer srvs) - where - storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs +updatedUserServers _ _ randomSrvs [] = L.map (AUS SDBNew) randomSrvs +updatedUserServers p presetOps randomSrvs srvs = + fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedServers) where + updatedServers = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs) + storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) + storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs + customServer :: UserServer p -> Bool customServer srv = not (preset srv) && all (`S.notMember` presetHosts) (srvHost srv) - presetSrvs :: NonEmpty (NewUserServer p) - presetSrvs = foldMap1 (operatorServers p) presetOps + presetSrvs :: [NewUserServer p] + presetSrvs = concatMap (operatorServers p) presetOps presetHosts :: Set TransportHost - presetHosts = foldMap1 (S.fromList . L.toList . srvHost) presetSrvs - userServer :: Map (ProtoServerWithAuth p) (UserServer p) -> NewUserServer p -> AUserServer p - userServer storedSrvs srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) + presetHosts = foldMap' (S.fromList . L.toList . srvHost) presetSrvs + userServer :: NewUserServer p -> AUserServer p + userServer srv@UserServer {server} = maybe (AUS SDBNew srv) (AUS SDBStored) (M.lookup server storedSrvs) srvHost :: UserServer' s p -> NonEmpty TransportHost srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv -serverCfgs :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) -serverCfgs opDomains = L.map agentServer +agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) +agentServerCfgs opDomains = L.map agentServer where agentServer :: UserServer' s p -> ServerCfg p agentServer srv@UserServer {server, enabled} = diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index cb54f14e0e..16ffe6e28f 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -27,12 +27,12 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), OptionsServers (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth) import System.FilePath (combine) @@ -56,7 +56,8 @@ data ChatOpts = ChatOpts data CoreChatOpts = CoreChatOpts { dbFilePrefix :: String, dbKey :: ScrubbedBytes, - optionsServers :: OptionsServers, + smpServers :: [SMPServerWithAuth], + xftpServers :: [XFTPServerWithAuth], simpleNetCfg :: SimpleNetCfg, logLevel :: ChatLogLevel, logConnections :: Bool, @@ -243,7 +244,8 @@ coreChatOptsP appDir defaultDbFileName = do CoreChatOpts { dbFilePrefix, dbKey, - optionsServers = OptionsServers {smpServers, xftpServers}, + smpServers, + xftpServers, simpleNetCfg = SimpleNetCfg { socksProxy, diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 361e61b953..aa6babfcbd 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -34,16 +34,16 @@ terminalChatConfig = PresetServers { operators = [ PresetOperator - { operator = operatorSimpleXChat, + { operator = Just operatorSimpleXChat, smp = - L.map + map (presetServer True) [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" ], useSMP = 3, - xftp = L.map (presetServer True) defaultXFTPServers, + xftp = map (presetServer True) $ L.toList defaultXFTPServers, useXFTP = 3 } ], diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index cfe8cf60f4..ab47951214 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -21,12 +21,11 @@ import Control.Monad.Reader import Data.ByteArray (ScrubbedBytes) import Data.Functor (($>)) import Data.List (dropWhileEnd, find) -import qualified Data.List.NonEmpty as L import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), OptionsServers (..), PresetServers (..), defaultSimpleNetCfg) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), PresetServers (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Operators (PresetOperator (..), presetServer) @@ -96,8 +95,8 @@ testCoreOpts = { dbFilePrefix = "./simplex_v1", dbKey = "", -- dbKey = "this is a pass-phrase to encrypt the database", - -- optionsServers = testOptsServers, - optionsServers = OptionsServers [] [], + smpServers = [], + xftpServers = [], simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, @@ -109,13 +108,6 @@ testCoreOpts = yesToUpMigrations = False } -testOptsServers :: OptionsServers -testOptsServers = - OptionsServers - { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], - xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"] - } - getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts getTestOpts maintenance dbKey = testOpts {maintenance, coreOptions = testCoreOpts {dbKey}} @@ -162,10 +154,10 @@ testCfg = (presetServers defaultChatConfig) { operators = [ PresetOperator - { operator = operatorSimpleXChat, - smp = L.map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], + { operator = Just operatorSimpleXChat, + smp = map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], useSMP = 1, - xftp = L.map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], + xftp = map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], useXFTP = 1 } ] diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7b34f19bc2..39e0599150 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -25,7 +25,7 @@ import Database.SQLite.Simple (Only (..)) import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS import Simplex.Chat.Call -import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..), PresetServers (..)) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -271,7 +271,7 @@ testRetryConnecting tmp = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile te testOpts { coreOptions = testCoreOpts - { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] } } @@ -340,7 +340,7 @@ testRetryConnectingClientTimeout tmp = do testOpts { coreOptions = testCoreOpts - { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] } } diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 979f696d15..5f69b00fba 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -17,7 +17,7 @@ import qualified Data.ByteString.Char8 as B import Data.List (intercalate, isInfixOf) import qualified Data.Text as T import Database.SQLite.Simple (Only (..)) -import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..)) +import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Messages (ChatItemId) import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) @@ -6504,7 +6504,7 @@ testGroupMemberInactive tmp = do opts' = testOpts { coreOptions = - (testCoreOpts :: CoreChatOpts) - { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] } } diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index d6ee04baa5..d98a818db4 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -15,7 +15,7 @@ import Control.Monad.Except import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), OptionsServers (..)) +import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Options import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Types (ConnStatus (..), Profile (..)) @@ -314,8 +314,8 @@ testRetryAcceptingViaContactLink tmp = testChatCfgOpts2 cfg' opts' aliceProfile opts' = testOpts { coreOptions = - (testCoreOpts :: CoreChatOpts) - { optionsServers = testOptsServers {smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"]} + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] } } From da65474452366e3d42e17fd7648196ea011808e3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Mon, 11 Nov 2024 23:24:11 +0000 Subject: [PATCH 19/22] CLI API in progress, validateUserServers --- package.yaml | 1 + simplex-chat.cabal | 7 ++ src/Simplex/Chat.hs | 97 +++++++++--------- src/Simplex/Chat/Controller.hs | 15 ++- src/Simplex/Chat/Operators.hs | 159 ++++++++++++++++++++--------- src/Simplex/Chat/Store/Profiles.hs | 121 ++++++++++------------ tests/ChatTests/Direct.hs | 6 +- tests/ChatTests/Profiles.hs | 2 +- tests/RandomServers.hs | 70 +++++++------ 9 files changed, 273 insertions(+), 205 deletions(-) diff --git a/package.yaml b/package.yaml index 2fc50a3532..7cf20c46e5 100644 --- a/package.yaml +++ b/package.yaml @@ -39,6 +39,7 @@ dependencies: - optparse-applicative >= 0.15 && < 0.17 - random >= 1.1 && < 1.3 - record-hasfield == 1.0.* + - scientific ==0.3.7.* - simple-logger == 0.1.* - simplexmq >= 5.0 - socks == 0.6.* diff --git a/simplex-chat.cabal b/simplex-chat.cabal index c7d603457c..9f50bf7bd5 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -227,6 +227,7 @@ library , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplexmq >=5.0 , socks ==0.6.* @@ -291,6 +292,7 @@ executable simplex-bot , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -356,6 +358,7 @@ executable simplex-bot-advanced , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -424,6 +427,7 @@ executable simplex-broadcast-bot , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -490,6 +494,7 @@ executable simplex-chat , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -562,6 +567,7 @@ executable simplex-directory-service , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , simple-logger ==0.1.* , simplex-chat , simplexmq >=5.0 @@ -663,6 +669,7 @@ test-suite simplex-chat-test , optparse-applicative >=0.15 && <0.17 , random >=1.1 && <1.3 , record-hasfield ==1.0.* + , scientific ==0.3.7.* , silently ==1.2.* , simple-logger ==0.1.* , simplex-chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index bcdef12fe2..cc9aa4431c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -463,10 +463,10 @@ withFileLock name = withEntityLock name . CLFile serverCfg :: ProtoServerWithAuth p -> ServerCfg p serverCfg server = ServerCfg {server, operator = Nothing, enabled = True, roles = allRoles} --- useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [UserServer p] -> [ProtocolServer p] --- useServers cfg p = \case --- [] -> map protoServer $ optsServers cfg p --- srvs -> map (\UserServer {server} -> protoServer server) srvs +useServers :: forall p. UserProtocol p => SProtocolType p -> RandomServers -> [UserServer p] -> NonEmpty (NewUserServer p) +useServers p rs servers = case L.nonEmpty servers of + Nothing -> rndServers p rs + Just srvs -> L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs rndServers :: UserProtocol p => SProtocolType p -> RandomServers -> NonEmpty (NewUserServer p) rndServers p RandomServers {smpServers, xftpServers} = case p of @@ -660,12 +660,10 @@ processChatCommand' vr = \case createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (NewUserServer p)) - chooseServers p = - chatReadVar currentUser - $>>= (fmap L.nonEmpty . withFastStore' . flip getProtocolServers) - >>= \case - Nothing -> rndServers p <$> asks randomServers - Just srvs -> pure $ L.map (\srv -> (srv :: UserServer p) {serverId = DBNewEntity}) srvs + chooseServers p = do + rs <- asks randomServers + srvs <- chatReadVar currentUser >>= mapM (\user -> withFastStore' $ \db -> getProtocolServers db p user) + pure $ useServers p rs $ fromMaybe [] srvs coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1563,23 +1561,25 @@ processChatCommand' vr = \case msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs pure $ CRConnNtfMessages ntfMsgs - -- APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do - -- cfg@ChatConfig {defaultServers} <- asks config - -- srvs <- withFastStore' (`getProtocolServers` user) - -- (operators, _) <- withFastStore $ \db -> getServerOperators db - -- let servers = AUPS $ UserProtoServers p (useServers cfg p srvs) (cfgServers p defaultServers) - -- pure $ CRUserProtoServers {user, servers, operators} - -- GetUserProtoServers aProtocol -> withUser $ \User {userId} -> - -- processChatCommand $ APIGetUserProtoServers userId aProtocol - -- APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) - -- | null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do - -- withFastStore $ \db -> overwriteProtocolServers db user servers - -- cfg <- asks config - -- lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers - -- ok user - -- | otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled" - -- SetUserProtoServers serversConfig -> withUser $ \User {userId} -> - -- processChatCommand $ APISetUserProtoServers userId serversConfig + GetUserProtoServers (AProtocolType p) -> withUser $ \user@User {userId} -> withServerProtocol p $ do + (operators, smpServers, xftpServers) <- withFastStore (`getUserServers` user) + userServers <- liftIO $ groupByOperator $ case p of + SPSMP -> (operators, smpServers, []) + SPXFTP -> (operators, [], xftpServers) + pure $ CRUserServers user userServers + SetUserProtoServers (AProtocolType p) servers -> withUser $ \user@User {userId} -> withServerProtocol p $ do + userServers <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) + -- disable operators servers and repace (or add) custom servers, or restore random defaults if empty list + case L.nonEmpty userServers of + Just srvs -> processChatCommand $ APISetUserServers userId $ L.map updated srvs + where + updated UserOperatorServers {operator, smpServers, xftpServers} = + UpdatedUserOperatorServers + { operator, + smpServers = map (AUS SDBStored) smpServers, + xftpServers = map (AUS SDBStored) xftpServers + } + Nothing -> throwChatError $ CECommandError "no servers" APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> @@ -1588,21 +1588,22 @@ processChatCommand' vr = \case APISetServerOperators operatorsEnabled -> withFastStore $ \db -> do liftIO $ setServerOperators db operatorsEnabled uncurry CRServerOperators <$> getServerOperators db - APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> do - (operators, _) <- getServerOperators db - liftIO $ do - smpServers <- getProtocolServers @'PSMP db user - xftpServers <- getProtocolServers @'PXFTP db user - CRUserServers user <$> groupByOperator operators smpServers xftpServers + APIGetUserServers userId -> withUserId userId $ \user -> withFastStore $ \db -> + CRUserServers user <$> (liftIO . groupByOperator =<< getUserServers db user) APISetUserServers userId userServers -> withUserId userId $ \user -> do let errors = validateUserServers userServers unless (null errors) $ throwChatError (CECommandError $ "user servers validation error(s): " <> show errors) - withFastStore $ \db -> setUserServers db user userServers - -- TODO set protocol servers for agent + (operators, smpServers, xftpServers) <- withFastStore $ \db -> do + setUserServers db user userServers + getUserServers db user + let opDomains = operatorDomains operators + rs <- asks randomServers + lift $ withAgent' $ \a -> do + let auId = aUserId user + setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPSMP rs smpServers + setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPXFTP rs xftpServers ok_ - APIValidateServers userServers -> do - let errors = validateUserServers userServers - pure $ CRUserServersValidation errors + APIValidateServers userServers -> pure $ CRUserServersValidation $ validateUserServers userServers APIGetUsageConditions -> do (usageConditions, acceptedConditions) <- withFastStore $ \db -> do usageConditions <- getCurrentUsageConditions db @@ -1875,7 +1876,7 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (`getProtocolServers` newUser) + newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (\db -> getProtocolServers db SPSMP newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2615,7 +2616,7 @@ processChatCommand' vr = \case pure $ CRAgentServersSummary user presentedServersSummary where getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] - getServers db user _p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db user + getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails @@ -3733,7 +3734,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (`getProtocolServers` user) + knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (\db -> getProtocolServers db SPXFTP user) pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -8203,14 +8204,12 @@ chatCommandP = "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), - -- "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), - -- "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - -- "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), - -- "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), - -- "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), - -- "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), - -- "/smp" $> GetUserProtoServers (AProtocolType SPSMP), - -- "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), + "/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP), + "/smp default" $> SetUserProtoServers (AProtocolType SPSMP) [], + "/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP), + "/xftp default" $> SetUserProtoServers (AProtocolType SPXFTP) [], + "/smp" $> GetUserProtoServers (AProtocolType SPSMP), + "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, "/_operators " *> (APISetServerOperators <$> jsonP), "/_servers " *> (APIGetUserServers <$> A.decimal), diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 2c062f757d..92462c173a 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -349,17 +349,15 @@ data ChatCommand | APIGetGroupLink GroupId | APICreateMemberContact GroupId GroupMemberId | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} - | -- | APIGetUserProtoServers UserId AProtocolType - -- | GetUserProtoServers AProtocolType - -- | APISetUserProtoServers UserId AProtoServersConfig - -- | SetUserProtoServers AProtoServersConfig - APITestProtoServer UserId AProtoServerWithAuth + | GetUserProtoServers AProtocolType + | SetUserProtoServers AProtocolType [AProtoServerWithAuth] + | APITestProtoServer UserId AProtoServerWithAuth | TestProtoServer AProtoServerWithAuth | APIGetServerOperators | APISetServerOperators (NonEmpty ServerOperator) | APIGetUserServers UserId - | APISetUserServers UserId (NonEmpty UserOperatorServers) - | APIValidateServers (NonEmpty UserOperatorServers) -- response is CRUserServersValidation + | APISetUserServers UserId (NonEmpty UpdatedUserOperatorServers) + | APIValidateServers (NonEmpty UpdatedUserOperatorServers) -- response is CRUserServersValidation | APIGetUsageConditions | APISetConditionsNotified Int64 | APIAcceptConditions Int64 (NonEmpty Int64) @@ -588,8 +586,7 @@ data ChatResponse | CRChatItemInfo {user :: User, chatItem :: AChatItem, chatItemInfo :: ChatItemInfo} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | -- | CRUserProtoServers {user :: User, servers :: AUserProtoServers, operators :: [ServerOperator]} - CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} + | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRServerOperators {operators :: [ServerOperator], conditionsAction :: Maybe UsageConditionsAction} | CRUserServers {user :: User, userServers :: [UserOperatorServers]} | CRUserServersValidation {serverErrors :: [UserServersError]} diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 59b079bcfc..bad6c250b4 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -13,10 +13,12 @@ {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Operators where +import Control.Applicative ((<|>)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE @@ -30,13 +32,15 @@ import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe, isNothing, mapMaybe) +import Data.Scientific (floatingOrInteger) import Data.Set (Set) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) +import Data.Type.Equality import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) @@ -45,9 +49,9 @@ import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..), allRoles) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) +import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8, (<$?>)) usageConditionsCommit :: Text usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" @@ -67,6 +71,19 @@ data SDBStored (s :: DBStored) where SDBStored :: SDBStored 'DBStored SDBNew :: SDBStored 'DBNew +deriving instance Show (SDBStored s) + +class DBStoredI s where sdbStored :: SDBStored s + +instance DBStoredI 'DBStored where sdbStored = SDBStored + +instance DBStoredI 'DBNew where sdbStored = SDBNew + +instance TestEquality SDBStored where + testEquality SDBStored SDBStored = Just Refl + testEquality SDBNew SDBNew = Just Refl + testEquality _ _ = Nothing + data DBEntityId' (s :: DBStored) where DBEntityId :: Int64 -> DBEntityId' 'DBStored DBNewEntity :: DBEntityId' 'DBNew @@ -77,7 +94,7 @@ type DBEntityId = DBEntityId' 'DBStored type DBNewEntity = DBEntityId' 'DBNew -data ADBEntityId = forall s. AEI (SDBStored s) (DBEntityId' s) +data ADBEntityId = forall s. DBStoredI s => AEI (SDBStored s) (DBEntityId' s) pattern ADBEntityId :: Int64 -> ADBEntityId pattern ADBEntityId i = AEI SDBStored (DBEntityId i) @@ -161,6 +178,8 @@ type NewServerOperator = ServerOperator' 'DBNew data AServerOperator = forall s. ASO (SDBStored s) (ServerOperator' s) +deriving instance Show AServerOperator + data ServerOperator' s = ServerOperator { operatorId :: DBEntityId' s, operatorTag :: Maybe OperatorTag, @@ -185,18 +204,33 @@ data UserOperatorServers = UserOperatorServers } deriving (Show) +data UpdatedUserOperatorServers = UpdatedUserOperatorServers + { operator :: Maybe ServerOperator, + smpServers :: [AUserServer 'PSMP], + xftpServers :: [AUserServer 'PXFTP] + } + deriving (Show) + +updatedServers :: UserProtocol p => UpdatedUserOperatorServers -> SProtocolType p -> [AUserServer p] +updatedServers UpdatedUserOperatorServers {smpServers, xftpServers} = \case + SPSMP -> smpServers + SPXFTP -> xftpServers + type UserServer p = UserServer' 'DBStored p type NewUserServer p = UserServer' 'DBNew p data AUserServer p = forall s. AUS (SDBStored s) (UserServer' s p) +deriving instance Show (AUserServer p) + data UserServer' s p = UserServer { serverId :: DBEntityId' s, server :: ProtoServerWithAuth p, preset :: Bool, tested :: Maybe Bool, - enabled :: Bool + enabled :: Bool, + deleted :: Bool } deriving (Show) @@ -220,7 +254,7 @@ operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p presetServer enabled server = - UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled} + UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled, deleted = False} -- This function should be used inside DB transaction to update conditions in the database -- it evaluates to (conditions to mark as accepted to SimpleX operator, current conditions, and conditions to add) @@ -268,9 +302,9 @@ updatedServerOperators presetOps storedOps = updatedUserServers :: forall p. UserProtocol p => SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> [UserServer p] -> NonEmpty (AUserServer p) updatedUserServers _ _ randomSrvs [] = L.map (AUS SDBNew) randomSrvs updatedUserServers p presetOps randomSrvs srvs = - fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedServers) + fromMaybe (L.map (AUS SDBNew) randomSrvs) (L.nonEmpty updatedSrvs) where - updatedServers = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs) + updatedSrvs = map userServer presetSrvs <> map (AUS SDBStored) (filter customServer srvs) storedSrvs :: Map (ProtoServerWithAuth p) (UserServer p) storedSrvs = foldl' (\ss srv@UserServer {server} -> M.insert server srv ss) M.empty srvs customServer :: UserServer p -> Bool @@ -304,8 +338,8 @@ matchingHost d = \case operatorDomains :: [ServerOperator] -> [(Text, ServerOperator)] operatorDomains = foldr (\op ds -> foldr (\d -> ((d, op) :)) ds (serverDomains op)) [] -groupByOperator :: [ServerOperator] -> [UserServer 'PSMP] -> [UserServer 'PXFTP] -> IO [UserOperatorServers] -groupByOperator ops smpSrvs xftpSrvs = do +groupByOperator :: ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> IO [UserOperatorServers] +groupByOperator (ops, smpSrvs, xftpSrvs) = do ss <- mapM (\op -> (serverDomains op,) <$> newIORef (UserOperatorServers (Just op) [] [])) ops custom <- newIORef $ UserOperatorServers Nothing [] [] mapM_ (addServer ss custom addSMP) (reverse smpSrvs) @@ -316,67 +350,98 @@ groupByOperator ops smpSrvs xftpSrvs = do addServer ss custom add srv = let v = maybe custom snd $ find (\(ds, _) -> any (\d -> any (matchingHost d) (srvHost srv)) ds) ss in atomicModifyIORef'_ v $ add srv - addSMP srv s@UserOperatorServers {smpServers} = s {smpServers = srv : smpServers} - addXFTP srv s@UserOperatorServers {xftpServers} = s {xftpServers = srv : xftpServers} + addSMP srv s@UserOperatorServers {smpServers} = (s :: UserOperatorServers) {smpServers = srv : smpServers} + addXFTP srv s@UserOperatorServers {xftpServers} = (s :: UserOperatorServers) {xftpServers = srv : xftpServers} data UserServersError - = USEStorageMissing - | USEProxyMissing - | USEDuplicateSMP {server :: AProtoServerWithAuth} - | USEDuplicateXFTP {server :: AProtoServerWithAuth} + = USEStorageMissing {protocol :: AProtocolType} + | USEProxyMissing {protocol :: AProtocolType} + | USEDuplicateServer {protocol :: AProtocolType, duplicateServer :: AProtoServerWithAuth, duplicateHost :: TransportHost} deriving (Show) -validateUserServers :: NonEmpty UserOperatorServers -> [UserServersError] -validateUserServers userServers = - let storageMissing_ = if any (canUseForRole storage) userServers then [] else [USEStorageMissing] - proxyMissing_ = if any (canUseForRole proxy) userServers then [] else [USEProxyMissing] - allSMPServers = map (\UserServer {server} -> server) $ concatMap (\UserOperatorServers {smpServers} -> smpServers) userServers - duplicateSMPServers = findDuplicatesByHost allSMPServers - duplicateSMPErrors = map (USEDuplicateSMP . AProtoServerWithAuth SPSMP) duplicateSMPServers - - allXFTPServers = map (\UserServer {server} -> server) $ concatMap (\UserOperatorServers {xftpServers} -> xftpServers) userServers - duplicateXFTPServers = findDuplicatesByHost allXFTPServers - duplicateXFTPErrors = map (USEDuplicateXFTP . AProtoServerWithAuth SPXFTP) duplicateXFTPServers - in storageMissing_ <> proxyMissing_ <> duplicateSMPErrors <> duplicateXFTPErrors +validateUserServers :: NonEmpty UpdatedUserOperatorServers -> [UserServersError] +validateUserServers uss = + missingRolesErr SPSMP storage USEStorageMissing + <> missingRolesErr SPSMP proxy USEProxyMissing + <> missingRolesErr SPXFTP storage USEStorageMissing + <> duplicatServerErrs SPSMP + <> duplicatServerErrs SPXFTP where - canUseForRole :: (ServerRoles -> Bool) -> UserOperatorServers -> Bool - canUseForRole roleSel UserOperatorServers {operator, smpServers, xftpServers} = case operator of - Just ServerOperator {roles} -> roleSel roles - Nothing -> not (null smpServers) && not (null xftpServers) - findDuplicatesByHost :: [ProtoServerWithAuth p] -> [ProtoServerWithAuth p] - findDuplicatesByHost servers = - let allHosts = concatMap (L.toList . host . protoServer) servers - hostCounts = M.fromListWith (+) [(host, 1 :: Int) | host <- allHosts] - duplicateHosts = M.keys $ M.filter (> 1) hostCounts - in filter (\srv -> any (`elem` duplicateHosts) (L.toList $ host . protoServer $ srv)) servers + missingRolesErr :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> (AProtocolType -> UserServersError) -> [UserServersError] + missingRolesErr p roleSel err = [err (AProtocolType p) | hasRole] + where + hasRole = + any (\(AUS _ UserServer {deleted, enabled}) -> enabled && not deleted) $ + concatMap (`updatedServers` p) $ filter roleEnabled (L.toList uss) + roleEnabled UpdatedUserOperatorServers {operator} = + maybe True (\ServerOperator {enabled, roles} -> enabled && roleSel roles) operator + duplicatServerErrs :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [UserServersError] + duplicatServerErrs p = mapMaybe duplicateErr_ srvs + where + srvs = + filter (\(AUS _ UserServer {deleted}) -> not deleted) $ + concatMap (`updatedServers` p) (L.toList uss) + duplicateErr_ (AUS _ srv@UserServer {server}) = + USEDuplicateServer (AProtocolType p) (AProtoServerWithAuth p server) + <$> find (`S.member` duplicateHosts) (srvHost srv) + duplicateHosts = snd $ foldl' (\acc (AUS _ srv) -> foldl' addHost acc $ srvHost srv) (S.empty, S.empty) srvs + addHost (hs, dups) h + | h `S.member` hs = (hs, S.insert h dups) + | otherwise = (S.insert h hs, dups) -instance ToJSON DBEntityId where - toEncoding (DBEntityId i) = toEncoding i - toJSON (DBEntityId i) = toJSON i +instance ToJSON ADBEntityId where + toEncoding (AEI _ dbId) = toEncoding dbId + toJSON (AEI _ dbId) = toJSON dbId -instance FromJSON DBEntityId where - parseJSON v = DBEntityId <$> parseJSON v +instance ToJSON (DBEntityId' s) where + toEncoding = \case + DBEntityId i -> toEncoding i + DBNewEntity -> JE.null_ + toJSON = \case + DBEntityId i -> toJSON i + DBNewEntity -> J.Null + +instance FromJSON ADBEntityId where + parseJSON (J.Null) = pure $ AEI SDBNew DBNewEntity + parseJSON (J.Number n) = case floatingOrInteger n of + Left (_ :: Double) -> fail "bad ADBEntityId" + Right i -> pure $ AEI SDBStored (DBEntityId $ fromInteger i) + parseJSON _ = fail "bad ADBEntityId" + +instance DBStoredI s => FromJSON (DBEntityId' s) where + parseJSON v = (\(AEI _ dbId) -> checkDBStored dbId) <$?> parseJSON v + +checkDBStored :: forall t s s'. (DBStoredI s, DBStoredI s') => t s' -> Either String (t s) +checkDBStored x = case testEquality (sdbStored @s) (sdbStored @s') of + Just Refl -> Right x + Nothing -> Left "bad DBStored" $(JQ.deriveJSON defaultJSON ''UsageConditions) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CA") ''ConditionsAcceptance) -instance ToJSON ServerOperator where +instance ToJSON (ServerOperator' s) where toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerOperator') toJSON = $(JQ.mkToJSON defaultJSON ''ServerOperator') -instance FromJSON ServerOperator where +instance DBStoredI s => FromJSON (ServerOperator' s) where parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerOperator') $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "UCA") ''UsageConditionsAction) -instance ProtocolTypeI p => ToJSON (UserServer p) where +instance ProtocolTypeI p => ToJSON (UserServer' s p) where toEncoding = $(JQ.mkToEncoding defaultJSON ''UserServer') toJSON = $(JQ.mkToJSON defaultJSON ''UserServer') -instance ProtocolTypeI p => FromJSON (UserServer p) where +instance (DBStoredI s, ProtocolTypeI p) => FromJSON (UserServer' s p) where parseJSON = $(JQ.mkParseJSON defaultJSON ''UserServer') +instance ProtocolTypeI p => FromJSON (AUserServer p) where + parseJSON v = (AUS SDBStored <$> parseJSON v) <|> (AUS SDBNew <$> parseJSON v) + $(JQ.deriveJSON defaultJSON ''UserOperatorServers) +instance FromJSON UpdatedUserOperatorServers where + parseJSON = $(JQ.mkParseJSON defaultJSON ''UpdatedUserOperatorServers) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "USE") ''UserServersError) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 434a247e1e..23cb391ecb 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} @@ -55,6 +56,7 @@ module Simplex.Chat.Store.Profiles insertProtocolServer, getUpdateServerOperators, getServerOperators, + getUserServers, setServerOperators, getCurrentUsageConditions, getLatestAcceptedConditions, @@ -106,7 +108,7 @@ import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode, UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) @@ -533,28 +535,17 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p)) getUpdateUserServers db p presetOps randomSrvs user = do ts <- getCurrentTime - srvs <- getProtocolServers db user + srvs <- getProtocolServers db p user let srvs' = updatedUserServers p presetOps randomSrvs srvs mapM (upsertServer ts) srvs' where upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) upsertServer ts (AUS _ s@UserServer {serverId}) = case serverId of DBNewEntity -> insertProtocolServer db p user ts s - DBEntityId _ -> updateServer ts s $> s - updateServer :: UTCTime -> UserServer p -> IO () - updateServer ts UserServer {serverId, server, preset, tested, enabled} = - DB.execute - db - [sql| - UPDATE protocol_servers - SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, - preset = ?, tested = ?, enabled = ?, updated_at = ? - WHERE smp_server_id = ? - |] - (serverColumns p server :. (preset, tested, enabled, ts, serverId)) + DBEntityId _ -> updateProtocolServer db p ts s $> s -getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [UserServer p] -getProtocolServers db User {userId} = +getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> SProtocolType p -> User -> IO [UserServer p] +getProtocolServers db p User {userId} = map toUserServer <$> DB.query db @@ -563,13 +554,12 @@ getProtocolServers db User {userId} = FROM protocol_servers WHERE user_id = ? AND protocol = ? |] - (userId, decodeLatin1 $ strEncode protocol) + (userId, decodeLatin1 $ strEncode p) where - protocol = protocolTypeI @p toUserServer :: (DBEntityId, NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> UserServer p toUserServer (serverId, host, port, keyHash, auth_, preset, tested, enabled) = - let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) - in UserServer {serverId, server, preset, tested, enabled} + let server = ProtoServerWithAuth (ProtocolServer p host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + in UserServer {serverId, server, preset, tested, enabled, deleted = False} -- TODO remove -- overwriteOperatorsAndServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> Maybe [ServerOperator] -> [ServerCfg p] -> ExceptT StoreError IO [ServerCfg p] @@ -604,6 +594,18 @@ insertProtocolServer db p User {userId} ts srv@UserServer {server, preset, teste sId <- insertedRowId db pure (srv :: NewUserServer p) {serverId = DBEntityId sId} +updateProtocolServer :: ProtocolTypeI p => DB.Connection -> SProtocolType p -> UTCTime -> UserServer p -> IO () +updateProtocolServer db p ts UserServer {serverId, server, preset, tested, enabled} = + DB.execute + db + [sql| + UPDATE protocol_servers + SET protocol = ?, host = ?, port = ?, key_hash = ?, basic_auth = ?, + preset = ?, tested = ?, enabled = ?, updated_at = ? + WHERE smp_server_id = ? + |] + (serverColumns p server :. (preset, tested, enabled, ts, serverId)) + serverColumns :: ProtocolTypeI p => SProtocolType p -> ProtoServerWithAuth p -> (Text, NonEmpty TransportHost, String, C.KeyHash, Maybe Text) serverColumns p (ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) = let protocol = decodeLatin1 $ strEncode p @@ -620,13 +622,28 @@ getServerOperators db = do operators <- mapM getConds =<< getServerOperators_ db pure (operators, usageConditionsAction operators currentConds now) +getUserServers :: DB.Connection -> User -> ExceptT StoreError IO ([ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) +getUserServers db user = + (,,) + <$> (fst <$> getServerOperators db) + <*> liftIO (getProtocolServers db SPSMP user) + <*> liftIO (getProtocolServers db SPXFTP user) + setServerOperators :: DB.Connection -> NonEmpty ServerOperator -> IO () -setServerOperators db = - mapM_ $ \ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} -> - DB.execute - db - "UPDATE server_operators SET enabled = ?, role_storage = ?, role_proxy = ? WHERE server_operator_id = ?" - (enabled, storage, proxy, operatorId) +setServerOperators db ops = do + currentTs <- getCurrentTime + mapM_ (updateServerOperator db currentTs) ops + +updateServerOperator :: DB.Connection -> UTCTime -> ServerOperator -> IO () +updateServerOperator db currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = + DB.execute + db + [sql| + UPDATE server_operators + SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? + WHERE server_operator_id = ? + |] + (enabled, storage, proxy, operatorId, currentTs) getUpdateServerOperators :: DB.Connection -> NonEmpty PresetOperator -> Bool -> IO [ServerOperator] getUpdateServerOperators db presetOps newUser = do @@ -804,46 +821,20 @@ getUsageConditionsById_ db conditionsId = |] (Only conditionsId) -setUserServers :: DB.Connection -> User -> NonEmpty UserOperatorServers -> ExceptT StoreError IO () -setUserServers db User {userId} userServers = do - currentTs <- liftIO getCurrentTime - forM_ userServers $ do - \UserOperatorServers {operator, smpServers, xftpServers} -> do - forM_ operator $ \op -> liftIO $ updateOperator currentTs op - overwriteServers SPSMP currentTs operator smpServers - overwriteServers SPXFTP currentTs operator xftpServers +setUserServers :: DB.Connection -> User -> NonEmpty UpdatedUserOperatorServers -> ExceptT StoreError IO () +setUserServers db user@User {userId} userServers = checkConstraint SEUniqueID $ liftIO $ do + ts <- getCurrentTime + forM_ userServers $ \UpdatedUserOperatorServers {operator, smpServers, xftpServers} -> do + mapM_ (updateServerOperator db ts) operator + mapM_ (upsertOrDelete SPSMP ts) smpServers + mapM_ (upsertOrDelete SPXFTP ts) xftpServers where - updateOperator :: UTCTime -> ServerOperator -> IO () - updateOperator currentTs ServerOperator {operatorId, enabled, roles = ServerRoles {storage, proxy}} = - DB.execute - db - [sql| - UPDATE server_operators - SET enabled = ?, role_storage = ?, role_proxy = ?, updated_at = ? - WHERE server_operator_id = ? - |] - (enabled, storage, proxy, operatorId, currentTs) - overwriteServers :: ProtocolTypeI p => SProtocolType p -> UTCTime -> Maybe ServerOperator -> [UserServer p] -> ExceptT StoreError IO () - overwriteServers p currentTs serverOperator servers = - checkConstraint SEUniqueID . ExceptT $ do - case serverOperator of - Nothing -> - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id IS NULL AND protocol = ?" (userId, protocol) - Just ServerOperator {operatorId} -> - DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND server_operator_id = ? AND protocol = ?" (userId, operatorId, protocol) - forM_ servers $ \UserServer {serverId, server, tested, enabled} -> do - DB.execute - db - [sql| - INSERT INTO protocol_servers - (server_id, protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) - |] - (Only serverId :. serverColumns p server :. (tested, enabled, userId, currentTs, currentTs)) - -- take preset from operator - pure $ Right () - where - protocol = decodeLatin1 $ strEncode p + upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> UTCTime -> AUserServer p -> IO () + upsertOrDelete p ts (AUS _ s@UserServer {serverId, deleted}) = case serverId of + DBNewEntity -> void $ insertProtocolServer db p user ts s + DBEntityId srvId + | deleted -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False) + | otherwise -> updateProtocolServer db p ts s createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 39e0599150..7d5dc67d24 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -79,10 +79,10 @@ chatDirectTests = do it "own invitation link" testPlanInvitationLinkOwn it "connecting via invitation link" testPlanInvitationLinkConnecting describe "SMP servers" $ do - xit "get and set SMP servers" testGetSetSMPServers + it "get and set SMP servers" testGetSetSMPServers it "test SMP server connection" testTestSMPServerConnection describe "XFTP servers" $ do - xit "get and set XFTP servers" testGetSetXFTPServers + it "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer describe "async connection handshake" $ do describe "connect when initiating client goes offline" $ do @@ -116,7 +116,7 @@ chatDirectTests = do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses - xit "create user with same servers" testCreateUserSameServers + it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL it "chat items expire after restart for all users according to per user configuration" testUsersRestartCIExpiration diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index d98a818db4..d5dff9fde6 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -76,7 +76,7 @@ chatProfileTests = do it "change user for pending connection" testChangePCCUser it "change from incognito profile connects as new user" testChangePCCUserFromIncognito it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito - xit "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv + it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv describe "preferences" $ do it "set contact preferences" testSetContactPrefs it "feature offers" testFeatureOffers diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs index 63e46ea88c..8b0b94dbd5 100644 --- a/tests/RandomServers.hs +++ b/tests/RandomServers.hs @@ -1,56 +1,64 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wno-orphans #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module RandomServers where import Control.Monad (replicateM) +import Data.Foldable (foldMap') +import Data.List (sortOn) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L +import Data.Monoid (Sum (..)) import Simplex.Chat (defaultChatConfig, randomPresetServers) -import Simplex.Chat.Controller (ChatConfig (..)) -import Simplex.Chat.Operators (operatorServers, operatorServersToUse) -import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..), ServerRoles (..)) +import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) +import Simplex.Chat.Operators (DBEntityId' (..), NewUserServer, UserServer' (..), operatorServers, operatorServersToUse) +import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) import Test.Hspec randomServersTests :: Spec randomServersTests = describe "choosig random servers" $ do - it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers - it "should keep all 6 XFTP servers" testRandomXFTPServers + it "should choose 4 + 3 random SMP servers and keep the rest disabled" testRandomSMPServers + it "should choose 3 + 3 random XFTP servers and keep the rest disabled" testRandomXFTPServers deriving instance Eq ServerRoles -deriving instance Eq (ServerCfg p) +deriving instance Eq (DBEntityId' s) + +deriving instance Eq (UserServer' s p) testRandomSMPServers :: IO () testRandomSMPServers = do - pure () - -- [srvs1, srvs2, srvs3] <- - -- replicateM 3 $ - -- checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig - -- (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPSMP 7 False =<< randomPresetServers SPSMP (presetServers defaultChatConfig) + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures testRandomXFTPServers :: IO () testRandomXFTPServers = do - pure () - -- [srvs1, srvs2, srvs3] <- - -- replicateM 3 $ - -- checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig - -- (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPXFTP 6 False =<< randomPresetServers SPXFTP (presetServers defaultChatConfig) + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures --- checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p] --- checkEnabled p n allUsed (srvs, _) = do --- let def = defaultServers defaultChatConfig --- cfgSrvs = L.sortWith server' $ cfgServers p def --- toUse = cfgServersToUse p def --- srvs == cfgSrvs `shouldBe` allUsed --- L.map enable srvs `shouldBe` L.map enable cfgSrvs --- let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs --- toUse `shouldBe` n --- length enbldSrvs `shouldBe` n --- pure enbldSrvs --- where --- server' ServerCfg {server = ProtoServerWithAuth srv _} = srv --- enable :: forall p. ServerCfg p -> ServerCfg p --- enable srv = (srv :: ServerCfg p) {enabled = False} +checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> NonEmpty (NewUserServer p) -> IO [NewUserServer p] +checkEnabled p n allUsed srvs = do + let srvs' = sortOn server' $ L.toList srvs + PresetServers {operators = presetOps} = presetServers defaultChatConfig + presetSrvs = sortOn server' $ concatMap (operatorServers p) presetOps + Sum toUse = foldMap' (Sum . operatorServersToUse p) presetOps + srvs' == presetSrvs `shouldBe` allUsed + map enable srvs' `shouldBe` map enable presetSrvs + let enbldSrvs = filter (\UserServer {enabled} -> enabled) srvs' + toUse `shouldBe` n + length enbldSrvs `shouldBe` n + pure enbldSrvs + where + server' UserServer {server = ProtoServerWithAuth srv _} = srv + enable :: forall p. NewUserServer p -> NewUserServer p + enable srv = (srv :: NewUserServer p) {enabled = False} From 0ec88fd560572ede849a06493047ab9c1254c6ef Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 12 Nov 2024 20:41:20 +0000 Subject: [PATCH 20/22] make servers of disabled operators "unknown", consider only enabled servers when switching profile links --- src/Simplex/Chat.hs | 92 +++++++++++++++---------- src/Simplex/Chat/Controller.hs | 1 + src/Simplex/Chat/Operators.hs | 77 +++++++++------------ src/Simplex/Chat/Store/Profiles.hs | 8 ++- src/Simplex/Chat/View.hs | 106 ++++++++++++++++++++++------- tests/ChatClient.hs | 2 +- tests/ChatTests/Direct.hs | 71 ++++++++++++++----- tests/ChatTests/Profiles.hs | 11 ++- 8 files changed, 239 insertions(+), 129 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index cc9aa4431c..6a6ac4fb0a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -55,6 +55,7 @@ import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) +import Data.Type.Equality import qualified Data.UUID as UUID import qualified Data.UUID.V4 as V4 import Data.Word (Word32) @@ -392,32 +393,34 @@ newChatController netCfg' = updateNetworkConfig netCfg simpleNetCfg operators' = case (smpServers, xftpServers) of ([], []) -> operators - (smpSrvs, []) -> L.map removeSMP operators <> [custom smpSrvs []] - ([], xftpSrvs) -> L.map removeXFTP operators <> [custom [] xftpSrvs] + (smpSrvs, []) -> L.map disableSMP operators <> [custom smpSrvs []] + ([], xftpSrvs) -> L.map disableXFTP operators <> [custom [] xftpSrvs] (smpSrvs, xftpSrvs) -> [custom smpSrvs xftpSrvs] - removeSMP op = (op :: PresetOperator) {smp = []} - removeXFTP op = (op :: PresetOperator) {xftp = []} + disableSMP op@PresetOperator {smp} = (op :: PresetOperator) {smp = map disableSrv smp} + disableXFTP op@PresetOperator {xftp} = (op :: PresetOperator) {xftp = map disableSrv xftp} + disableSrv :: forall p. NewUserServer p -> NewUserServer p + disableSrv srv = (srv :: NewUserServer p) {enabled = False} custom smpSrvs xftpSrvs = PresetOperator { operator = Nothing, - smp = map (presetServer True) smpSrvs, + smp = map newUserServer smpSrvs, useSMP = 0, - xftp = map (presetServer True) xftpSrvs, + xftp = map newUserServer xftpSrvs, useXFTP = 0 } agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} randomServers = do users <- getUsers db opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) - smp' <- getUserServers SPSMP users opDomains - xftp' <- getUserServers SPXFTP users opDomains + smp' <- getServers SPSMP users opDomains + xftp' <- getServers SPXFTP users opDomains pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) - getUserServers p users opDomains = do + getServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) + getServers p users opDomains = do let randomSrvs = rndServers p randomServers fmap M.fromList $ forM users $ \u -> - (aUserId u,) . agentServerCfgs opDomains <$> getUpdateUserServers db p presetOps randomSrvs u + (aUserId u,) . agentServerCfgs opDomains randomSrvs <$> getUpdateUserServers db p presetOps randomSrvs u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = @@ -641,16 +644,17 @@ processChatCommand' vr = \case when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} opDomains <- operatorDomains . fst <$> withFastStore getServerOperators - let smp = agentServerCfgs opDomains smpServers - xftp = agentServerCfgs opDomains xftpServers + rs <- asks randomServers + let smp = agentServerCfgs opDomains (rndServers SPSMP rs) smpServers + xftp = agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts createPresetContactCards user `catchChatError` \_ -> pure () withFastStore $ \db -> do createNoteFolder db user - liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) smpServers - liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) xftpServers + liftIO $ mapM_ (insertProtocolServer db SPSMP user ts) $ useServers SPSMP rs smpServers + liftIO $ mapM_ (insertProtocolServer db SPXFTP user ts) $ useServers SPXFTP rs xftpServers atomically . writeTVar u $ Just user pure $ CRActiveUser user where @@ -659,11 +663,10 @@ processChatCommand' vr = \case withFastStore $ \db -> do createContact db user simplexStatusContactProfile createContact db user simplexTeamContactProfile - chooseServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (NewUserServer p)) + chooseServers :: forall p. ProtocolTypeI p => SProtocolType p -> CM [UserServer p] chooseServers p = do - rs <- asks randomServers srvs <- chatReadVar currentUser >>= mapM (\user -> withFastStore' $ \db -> getProtocolServers db p user) - pure $ useServers p rs $ fromMaybe [] srvs + pure $ fromMaybe [] srvs coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 ListUsers -> CRUsersList <$> withFastStore' getUsersInfo @@ -1561,25 +1564,36 @@ processChatCommand' vr = \case msgs <- lift $ withAgent' $ \a -> getConnectionMessages a acIds let ntfMsgs = L.map (\msg -> receivedMsgInfo <$> msg) msgs pure $ CRConnNtfMessages ntfMsgs - GetUserProtoServers (AProtocolType p) -> withUser $ \user@User {userId} -> withServerProtocol p $ do + GetUserProtoServers (AProtocolType p) -> withUser $ \user -> withServerProtocol p $ do (operators, smpServers, xftpServers) <- withFastStore (`getUserServers` user) userServers <- liftIO $ groupByOperator $ case p of SPSMP -> (operators, smpServers, []) SPXFTP -> (operators, [], xftpServers) pure $ CRUserServers user userServers - SetUserProtoServers (AProtocolType p) servers -> withUser $ \user@User {userId} -> withServerProtocol p $ do - userServers <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) - -- disable operators servers and repace (or add) custom servers, or restore random defaults if empty list - case L.nonEmpty userServers of - Just srvs -> processChatCommand $ APISetUserServers userId $ L.map updated srvs - where - updated UserOperatorServers {operator, smpServers, xftpServers} = - UpdatedUserOperatorServers - { operator, - smpServers = map (AUS SDBStored) smpServers, - xftpServers = map (AUS SDBStored) xftpServers - } + SetUserProtoServers (AProtocolType (p :: SProtocolType p)) srvs -> withUser $ \user@User {userId} -> withServerProtocol p $ do + srvs' <- mapM aUserServer srvs + userServers_ <- liftIO . groupByOperator =<< withFastStore (`getUserServers` user) + case L.nonEmpty userServers_ of Nothing -> throwChatError $ CECommandError "no servers" + Just userServers -> case srvs of + [] -> throwChatError $ CECommandError "no servers" + _ -> processChatCommand $ APISetUserServers userId $ L.map updatedSrvs userServers + where + -- disable preset and replace custom servers (groupByOperator always adds custom) + updatedSrvs UserOperatorServers {operator, smpServers, xftpServers} = case p of + SPSMP -> u (updateSrvs smpServers, map (AUS SDBStored) xftpServers) + SPXFTP -> u (map (AUS SDBStored) smpServers, updateSrvs xftpServers) + where + u = uncurry $ UpdatedUserOperatorServers operator + updateSrvs :: [UserServer p] -> [AUserServer p] + updateSrvs pSrvs = map disableSrv pSrvs <> maybe srvs' (const []) operator + disableSrv srv@UserServer {preset} = + AUS SDBStored $ if preset then srv {enabled = False} else srv {deleted = True} + where + aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) + aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of + Just Refl -> pure $ AUS SDBNew $ newUserServer srv + Nothing -> throwChatError $ CECommandError $ "incorrect server protocol: " <> B.unpack (strEncode srv) APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> @@ -1600,8 +1614,8 @@ processChatCommand' vr = \case rs <- asks randomServers lift $ withAgent' $ \a -> do let auId = aUserId user - setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPSMP rs smpServers - setProtocolServers a auId $ agentServerCfgs opDomains $ useServers SPXFTP rs xftpServers + setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPSMP rs) smpServers + setProtocolServers a auId $ agentServerCfgs opDomains (rndServers SPXFTP rs) xftpServers ok_ APIValidateServers userServers -> pure $ CRUserServersValidation $ validateUserServers userServers APIGetUsageConditions -> do @@ -1876,7 +1890,13 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - newUserServers <- map (\UserServer {server} -> protoServer server) <$> withFastStore' (\db -> getProtocolServers db SPSMP newUser) + rs <- asks randomServers + newUserServers <- withFastStore $ \db -> do + opDomains <- operatorDomains . fst <$> getServerOperators db + L.map (\ServerCfg {server} -> protoServer server) + . agentServerCfgs opDomains (rndServers SPSMP rs) + . filter (\UserServer {enabled} -> enabled) + <$> liftIO (getProtocolServers db SPSMP newUser) pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -2615,7 +2635,7 @@ processChatCommand' vr = \case let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers pure $ CRAgentServersSummary user presentedServersSummary where - getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] + getServers :: ProtocolTypeI p => DB.Connection -> User -> SProtocolType p -> IO [ProtocolServer p] getServers db user p = map (\UserServer {server} -> protoServer server) <$> getProtocolServers db p user ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary @@ -8205,9 +8225,7 @@ chatCommandP = "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), "/smp " *> (SetUserProtoServers (AProtocolType SPSMP) . map (AProtoServerWithAuth SPSMP) <$> protocolServersP), - "/smp default" $> SetUserProtoServers (AProtocolType SPSMP) [], "/xftp " *> (SetUserProtoServers (AProtocolType SPXFTP) . map (AProtoServerWithAuth SPXFTP) <$> protocolServersP), - "/xftp default" $> SetUserProtoServers (AProtocolType SPXFTP) [], "/smp" $> GetUserProtoServers (AProtocolType SPSMP), "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_operators" $> APIGetServerOperators, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 92462c173a..e4fcb59758 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -158,6 +158,7 @@ data RandomServers = RandomServers { smpServers :: NonEmpty (NewUserServer 'PSMP), xftpServers :: NonEmpty (NewUserServer 'PXFTP) } + deriving (Show) -- The hooks can be used to extend or customize chat core in mobile or CLI clients. data ChatHooks = ChatHooks diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index bad6c250b4..81541074c8 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -8,7 +8,6 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} @@ -40,7 +39,6 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime, nominalDay) -import Data.Type.Equality import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Language.Haskell.TH.Syntax (lift) @@ -51,7 +49,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI, SProtocolType (..), UserProtocol) import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (atomicModifyIORef'_, safeDecodeUtf8) usageConditionsCommit :: Text usageConditionsCommit = "165143a1112308c035ac00ed669b96b60599aa1c" @@ -79,11 +77,6 @@ instance DBStoredI 'DBStored where sdbStored = SDBStored instance DBStoredI 'DBNew where sdbStored = SDBNew -instance TestEquality SDBStored where - testEquality SDBStored SDBStored = Just Refl - testEquality SDBNew SDBNew = Just Refl - testEquality _ _ = Nothing - data DBEntityId' (s :: DBStored) where DBEntityId :: Int64 -> DBEntityId' 'DBStored DBNewEntity :: DBEntityId' 'DBNew @@ -94,14 +87,6 @@ type DBEntityId = DBEntityId' 'DBStored type DBNewEntity = DBEntityId' 'DBNew -data ADBEntityId = forall s. DBStoredI s => AEI (SDBStored s) (DBEntityId' s) - -pattern ADBEntityId :: Int64 -> ADBEntityId -pattern ADBEntityId i = AEI SDBStored (DBEntityId i) - -pattern ADBNewEntity :: ADBEntityId -pattern ADBNewEntity = AEI SDBNew DBNewEntity - data OperatorTag = OTSimplex | OTXyz deriving (Eq, Ord, Show) @@ -253,8 +238,14 @@ operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of SPXFTP -> useXFTP presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p -presetServer enabled server = - UserServer {serverId = DBNewEntity, server, preset = True, tested = Nothing, enabled, deleted = False} +presetServer = newUserServer_ True + +newUserServer :: ProtoServerWithAuth p -> NewUserServer p +newUserServer = newUserServer_ False True + +newUserServer_ :: Bool -> Bool -> ProtoServerWithAuth p -> NewUserServer p +newUserServer_ preset enabled server = + UserServer {serverId = DBNewEntity, server, preset, tested = Nothing, enabled, deleted = False} -- This function should be used inside DB transaction to update conditions in the database -- it evaluates to (conditions to mark as accepted to SimpleX operator, current conditions, and conditions to add) @@ -319,16 +310,21 @@ updatedUserServers p presetOps randomSrvs srvs = srvHost :: UserServer' s p -> NonEmpty TransportHost srvHost UserServer {server = ProtoServerWithAuth srv _} = host srv -agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (UserServer' s p) -> NonEmpty (ServerCfg p) -agentServerCfgs opDomains = L.map agentServer +agentServerCfgs :: [(Text, ServerOperator)] -> NonEmpty (NewUserServer p) -> [UserServer' s p] -> NonEmpty (ServerCfg p) +agentServerCfgs opDomains randomSrvs = + fromMaybe fallbackSrvs . L.nonEmpty . mapMaybe enabledOpAgentServer where - agentServer :: UserServer' s p -> ServerCfg p + fallbackSrvs = L.map (snd . agentServer) randomSrvs + enabledOpAgentServer srv = + let (opEnabled, srvCfg) = agentServer srv + in if opEnabled then Just srvCfg else Nothing + agentServer :: UserServer' s p -> (Bool, ServerCfg p) agentServer srv@UserServer {server, enabled} = case find (\(d, _) -> any (matchingHost d) (srvHost srv)) opDomains of Just (_, ServerOperator {operatorId = DBEntityId opId, enabled = opEnabled, roles}) -> - ServerCfg {server, operator = Just opId, enabled = opEnabled && enabled, roles} + (opEnabled, ServerCfg {server, enabled, operator = Just opId, roles}) Nothing -> - ServerCfg {server, operator = Nothing, enabled, roles = allRoles} + (True, ServerCfg {server, enabled, operator = Nothing, roles = allRoles}) matchingHost :: Text -> TransportHost -> Bool matchingHost d = \case @@ -344,7 +340,9 @@ groupByOperator (ops, smpSrvs, xftpSrvs) = do custom <- newIORef $ UserOperatorServers Nothing [] [] mapM_ (addServer ss custom addSMP) (reverse smpSrvs) mapM_ (addServer ss custom addXFTP) (reverse xftpSrvs) - mapM (readIORef . snd) ss + opSrvs <- mapM (readIORef . snd) ss + customSrvs <- readIORef custom + pure $ opSrvs <> [customSrvs] where addServer :: [([Text], IORef UserOperatorServers)] -> IORef UserOperatorServers -> (UserServer p -> UserOperatorServers -> UserOperatorServers) -> UserServer p -> IO () addServer ss custom add srv = @@ -368,7 +366,7 @@ validateUserServers uss = <> duplicatServerErrs SPXFTP where missingRolesErr :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> (ServerRoles -> Bool) -> (AProtocolType -> UserServersError) -> [UserServersError] - missingRolesErr p roleSel err = [err (AProtocolType p) | hasRole] + missingRolesErr p roleSel err = [err (AProtocolType p) | not hasRole] where hasRole = any (\(AUS _ UserServer {deleted, enabled}) -> enabled && not deleted) $ @@ -384,15 +382,12 @@ validateUserServers uss = duplicateErr_ (AUS _ srv@UserServer {server}) = USEDuplicateServer (AProtocolType p) (AProtoServerWithAuth p server) <$> find (`S.member` duplicateHosts) (srvHost srv) - duplicateHosts = snd $ foldl' (\acc (AUS _ srv) -> foldl' addHost acc $ srvHost srv) (S.empty, S.empty) srvs + duplicateHosts = snd $ foldl' addHost (S.empty, S.empty) allHosts + allHosts = concatMap (\(AUS _ srv) -> L.toList $ srvHost srv) srvs addHost (hs, dups) h | h `S.member` hs = (hs, S.insert h dups) | otherwise = (S.insert h hs, dups) -instance ToJSON ADBEntityId where - toEncoding (AEI _ dbId) = toEncoding dbId - toJSON (AEI _ dbId) = toJSON dbId - instance ToJSON (DBEntityId' s) where toEncoding = \case DBEntityId i -> toEncoding i @@ -401,20 +396,16 @@ instance ToJSON (DBEntityId' s) where DBEntityId i -> toJSON i DBNewEntity -> J.Null -instance FromJSON ADBEntityId where - parseJSON (J.Null) = pure $ AEI SDBNew DBNewEntity - parseJSON (J.Number n) = case floatingOrInteger n of - Left (_ :: Double) -> fail "bad ADBEntityId" - Right i -> pure $ AEI SDBStored (DBEntityId $ fromInteger i) - parseJSON _ = fail "bad ADBEntityId" - instance DBStoredI s => FromJSON (DBEntityId' s) where - parseJSON v = (\(AEI _ dbId) -> checkDBStored dbId) <$?> parseJSON v - -checkDBStored :: forall t s s'. (DBStoredI s, DBStoredI s') => t s' -> Either String (t s) -checkDBStored x = case testEquality (sdbStored @s) (sdbStored @s') of - Just Refl -> Right x - Nothing -> Left "bad DBStored" + parseJSON v = case (v, sdbStored @s) of + (J.Null, SDBNew) -> pure DBNewEntity + (J.Number n, SDBStored) -> case floatingOrInteger n of + Left (_ :: Double) -> fail "bad DBEntityId" + Right i -> pure $ DBEntityId (fromInteger i) + _ -> fail "bad DBEntityId" + omittedField = case sdbStored @s of + SDBStored -> Nothing + SDBNew -> Just DBNewEntity $(JQ.deriveJSON defaultJSON ''UsageConditions) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 23cb391ecb..39bd4bb985 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -532,11 +532,11 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) -getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO (NonEmpty (UserServer p)) +getUpdateUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => DB.Connection -> SProtocolType p -> NonEmpty PresetOperator -> NonEmpty (NewUserServer p) -> User -> IO [UserServer p] getUpdateUserServers db p presetOps randomSrvs user = do ts <- getCurrentTime srvs <- getProtocolServers db p user - let srvs' = updatedUserServers p presetOps randomSrvs srvs + let srvs' = L.toList $ updatedUserServers p presetOps randomSrvs srvs mapM (upsertServer ts) srvs' where upsertServer :: UTCTime -> AUserServer p -> IO (UserServer p) @@ -831,7 +831,9 @@ setUserServers db user@User {userId} userServers = checkConstraint SEUniqueID $ where upsertOrDelete :: ProtocolTypeI p => SProtocolType p -> UTCTime -> AUserServer p -> IO () upsertOrDelete p ts (AUS _ s@UserServer {serverId, deleted}) = case serverId of - DBNewEntity -> void $ insertProtocolServer db p user ts s + DBNewEntity + | deleted -> pure () + | otherwise -> void $ insertProtocolServer db p user ts s DBEntityId srvId | deleted -> DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND smp_server_id = ? AND preset = ?" (userId, srvId, False) | otherwise -> updateProtocolServer db p ts s diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index a14ba1317a..6296fd8b4a 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -25,6 +25,7 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) +import Data.String import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) @@ -42,6 +43,7 @@ import Simplex.Chat.Help import Simplex.Chat.Markdown import Simplex.Chat.Messages hiding (NewChatItem (..)) import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Operators import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange) import Simplex.Chat.Remote.Types @@ -53,7 +55,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) -import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) +import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerRoles (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) @@ -95,10 +97,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChats chats -> viewChats ts tz chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] CRApiParsedMarkdown ft -> [viewJSON ft] - -- CRUserProtoServers u userServers operators -> ttyUser u $ viewUserServers userServers operators testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure - CRServerOperators {} -> [] - CRUserServers {} -> [] + CRServerOperators ops ca -> viewServerOperators ops ca + CRUserServers u uss -> ttyUser u $ concatMap viewUserServers uss <> (if testView then [] else serversUserHelp) CRUserServersValidation _ -> [] CRUsageConditions {} -> [] CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl @@ -1213,27 +1214,31 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho "profile is " <> if isJust viewPwdHash then "hidden" else "visible" ] --- viewUserServers :: AUserProtoServers -> [ServerOperator] -> Bool -> [StyledString] --- viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, presetServers}) operators testView = --- customServers --- <> if testView --- then [] --- else --- [ "", --- "use " <> highlight (srvCmd <> " test ") <> " to test " <> pName <> " server connection", --- "use " <> highlight (srvCmd <> " ") <> " to configure " <> pName <> " servers", --- "use " <> highlight (srvCmd <> " default") <> " to remove configured " <> pName <> " servers and use presets" --- ] --- <> case p of --- SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"] --- SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"] --- where --- srvCmd = "/" <> strEncode p --- pName = protocolName p --- customServers = --- if null protoServers --- then ("no " <> pName <> " servers saved, using presets: ") : viewServers operators presetServers --- else viewServers operators protoServers +viewUserServers :: UserOperatorServers -> [StyledString] +viewUserServers (UserOperatorServers _ [] []) = [] +viewUserServers UserOperatorServers {operator, smpServers, xftpServers} = + [plain $ maybe "Your servers" shortViewOperator operator] + <> viewServers SPSMP smpServers + <> viewServers SPXFTP xftpServers + where + viewServers :: ProtocolTypeI p => SProtocolType p -> [UserServer p] -> [StyledString] + viewServers _ [] = [] + viewServers p srvs = [" " <> protocolName p <> " servers"] <> map (plain . (" " <> ) . viewServer) srvs + where + viewServer UserServer {server, preset, tested, enabled} = safeDecodeUtf8 (strEncode server) <> serverInfo + where + serverInfo = if null serverInfo_ then "" else parens $ T.intercalate ", " serverInfo_ + serverInfo_ = ["preset" | preset] <> testedInfo <> ["disabled" | not enabled] + testedInfo = maybe [] (\t -> ["test: " <> if t then "passed" else "failed"]) tested + +serversUserHelp :: [StyledString] +serversUserHelp = + [ "", + "use " <> highlight' "/smp test " <> " to test SMP server connection", + "use " <> highlight' "/smp " <> " to configure SMP servers", + "or the same commands starting from /xftp for XFTP servers", + "chat options " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") and " <> highlight' "--xftp-servers" <> " have precedence over preset servers for new user profiles" + ] protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1254,6 +1259,53 @@ viewServerTestResult (AProtoServerWithAuth p _) = \case where pName = protocolName p +viewServerOperators :: [ServerOperator] -> Maybe UsageConditionsAction -> [StyledString] +viewServerOperators ops ca = map (plain . viewOperator) ops <> maybe [] viewConditionsAction ca + +viewOperator :: ServerOperator' s -> Text +viewOperator op@ServerOperator {tradeName, legalName, serverDomains, conditionsAcceptance} = + viewOpIdTag op + <> tradeName + <> maybe "" parens legalName + <> (", domains: " <> T.intercalate ", " serverDomains) + <> (", conditions: " <> viewOpConditions conditionsAcceptance) + <> (", " <> viewOpEnabled op) + +shortViewOperator :: ServerOperator -> Text +shortViewOperator op@ServerOperator {operatorId = DBEntityId opId, tradeName} = + tshow opId <> ". " <> tradeName <> parens (viewOpEnabled op) + +viewOpIdTag :: ServerOperator' s -> Text +viewOpIdTag ServerOperator {operatorId, operatorTag} = case operatorId of + DBEntityId i -> tshow i <> " - " <> tag + DBNewEntity -> tag + where + tag = maybe "" textEncode operatorTag <> ". " + +viewOpConditions :: ConditionsAcceptance -> Text +viewOpConditions = \case + CAAccepted ts -> viewCond "accepted" ts + CARequired ts -> viewCond "required" ts + where + viewCond w ts = w <> maybe "" (parens . tshow) ts + +viewOpEnabled :: ServerOperator' s -> Text +viewOpEnabled ServerOperator {enabled, roles = ServerRoles {storage, proxy}} + | enabled && storage && proxy = "enabled" + | enabled && storage = "enabled storage" + | enabled && proxy = "enabled proxy" + | otherwise = "disabled" + +viewConditionsAction :: UsageConditionsAction -> [StyledString] +viewConditionsAction = \case + UCAReview {operators, deadline, showNotice} | showNotice -> case deadline of + Just ts -> [plain $ "New conditions will be accepted at " <> tshow ts <> " for " <> ops] + Nothing -> [plain $ "New conditions have to be accepted for " <> ops] + where + ops = T.intercalate ", " $ map legalName_ operators + legalName_ ServerOperator {tradeName, legalName} = fromMaybe tradeName legalName + _ -> [] + viewChatItemTTL :: Maybe Int64 -> [StyledString] viewChatItemTTL = \case Nothing -> ["old messages are not being deleted"] @@ -1933,7 +1985,9 @@ viewVersionInfo logLevel CoreVersionInfo {version, simplexmqVersion, simplexmqCo then [versionString version, updateStr, "simplexmq: " <> simplexmqVersion <> parens simplexmqCommit] else [versionString version, updateStr] where - parens s = " (" <> s <> ")" + +parens :: (IsString a, Semigroup a) => a -> a +parens s = " (" <> s <> ")" viewRemoteHosts :: [RemoteHostInfo] -> [StyledString] viewRemoteHosts = \case diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index ab47951214..b3d8166f9f 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -154,7 +154,7 @@ testCfg = (presetServers defaultChatConfig) { operators = [ PresetOperator - { operator = Just operatorSimpleXChat, + { operator = Nothing, smp = map (presetServer True) ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], useSMP = 1, xftp = map (presetServer True) ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7d5dc67d24..36771760d3 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -1109,17 +1109,32 @@ testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ \alice _ -> do - alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://1234-w==@smp1.example.im" alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") - alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + -- alice #$> ("/smp", id, "smp://1234-w==:password@smp1.example.im") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://1234-w==:password@smp1.example.im" alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" - alice #$> ("/smp default", id, "ok") - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" testTestSMPServerConnection :: HasCallStack => FilePath -> IO () testTestSMPServerConnection = @@ -1140,17 +1155,31 @@ testGetSetXFTPServers :: HasCallStack => FilePath -> IO () testGetSetXFTPServers = testChat2 aliceProfile bobProfile $ \alice _ -> withXFTPServer $ do - alice #$> ("/_servers 1 xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + alice ##> "/_servers 1" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset)" alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://1234-w==@xftp1.example.im" alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok") - alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im") + alice ##> "/xftp" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://1234-w==:password@xftp1.example.im" alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" - alice #$> ("/xftp default", id, "ok") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" testTestXFTPServer :: HasCallStack => FilePath -> IO () testTestXFTPServer = @@ -1768,11 +1797,17 @@ testCreateUserSameServers = where checkCustomServers alice = do alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://2345-w==@smp2.example.im" + alice <## " smp://3456-w==@smp3.example.im:5224" alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" + alice <## "Your servers" + alice <## " XFTP servers" + alice <## " xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002 (preset, disabled)" + alice <## " xftp://2345-w==@xftp2.example.im" + alice <## " xftp://3456-w==@xftp3.example.im:5224" testDeleteUser :: HasCallStack => FilePath -> IO () testDeleteUser = diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index d5dff9fde6..1d390e1236 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1734,7 +1734,16 @@ testChangePCCUserDiffSrv tmp = do -- Create new user with different servers alice ##> "/create user alisa" showActiveUser alice "alisa" - alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003", id, "ok") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset)" + alice #$> ("/smp smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003", id, "ok") + alice ##> "/smp" + alice <## "Your servers" + alice <## " SMP servers" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001 (preset, disabled)" + alice <## " smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@127.0.0.1:7003" alice ##> "/user alice" showActiveUser alice "alice (Alice)" -- Change connection to newly created user and use the newly created connection From b19dffad4d52b8eecd6a773fcb424f705653c4de Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Tue, 12 Nov 2024 21:18:13 +0000 Subject: [PATCH 21/22] exclude disabled operators when receiving files --- src/Simplex/Chat.hs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 6a6ac4fb0a..a856a76eb7 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1890,13 +1890,9 @@ processChatCommand' vr = \case canKeepLink (CRInvitationUri crData _) newUser = do let ConnReqUriData {crSmpQueues = q :| _} = crData SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q - rs <- asks randomServers - newUserServers <- withFastStore $ \db -> do - opDomains <- operatorDomains . fst <$> getServerOperators db - L.map (\ServerCfg {server} -> protoServer server) - . agentServerCfgs opDomains (rndServers SPSMP rs) - . filter (\UserServer {enabled} -> enabled) - <$> liftIO (getProtocolServers db SPSMP newUser) + newUserServers <- + map protoServer' . filter (\ServerCfg {enabled} -> enabled) + <$> getKnownAgentServers SPSMP newUser pure $ smpServer `elem` newUserServers updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) @@ -3754,7 +3750,7 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] getUnknownSrvs srvs = do - knownSrvs <- map (\UserServer {server} -> protoServer server) <$> withStore' (\db -> getProtocolServers db SPXFTP user) + knownSrvs <- map protoServer' <$> getKnownAgentServers SPXFTP user pure $ filter (`notElem` knownSrvs) srvs ipProtectedForSrvs :: [XFTPServer] -> CM Bool ipProtectedForSrvs srvs = do @@ -3768,6 +3764,17 @@ receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} toView $ CRChatItemUpdated user aci throwChatError $ CEFileNotApproved fileId unknownSrvs +getKnownAgentServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> User -> CM [ServerCfg p] +getKnownAgentServers p user = do + rs <- asks randomServers + withStore $ \db -> do + opDomains <- operatorDomains . fst <$> getServerOperators db + srvs <- liftIO $ getProtocolServers db p user + pure $ L.toList $ agentServerCfgs opDomains (rndServers p rs) srvs + +protoServer' :: ServerCfg p -> ProtocolServer p +protoServer' ServerCfg {server} = protoServer server + getNetworkConfig :: CM' NetworkConfig getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig From 1683b7109f3ee99cd0150d486aaac96f9a0ddd24 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 13 Nov 2024 21:56:35 +0000 Subject: [PATCH 22/22] fix TH in ghc 8.10.7 --- package.yaml | 2 +- simplex-chat.cabal | 14 +++++++------- src/Simplex/Chat.hs | 6 +++--- src/Simplex/Chat/Operators.hs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.yaml b/package.yaml index 7cf20c46e5..4a95d52044 100644 --- a/package.yaml +++ b/package.yaml @@ -74,7 +74,7 @@ when: - bytestring == 0.10.* - process >= 1.6 && < 1.6.18 - template-haskell == 2.16.* - - text >= 1.2.3.0 && < 1.3 + - text >= 1.2.4.0 && < 1.3 library: source-dirs: src diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 9f50bf7bd5..640fc2ddbf 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -254,7 +254,7 @@ library bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-bot main-is: Main.hs @@ -320,7 +320,7 @@ executable simplex-bot bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-bot-advanced main-is: Main.hs @@ -386,7 +386,7 @@ executable simplex-bot-advanced bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-broadcast-bot main-is: Main.hs @@ -455,7 +455,7 @@ executable simplex-broadcast-bot bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-chat main-is: Main.hs @@ -523,7 +523,7 @@ executable simplex-chat bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 executable simplex-directory-service main-is: Main.hs @@ -595,7 +595,7 @@ executable simplex-directory-service bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 test-suite simplex-chat-test type: exitcode-stdio-1.0 @@ -698,7 +698,7 @@ test-suite simplex-chat-test bytestring ==0.10.* , process >=1.6 && <1.6.18 , template-haskell ==2.16.* - , text >=1.2.3.0 && <1.3 + , text >=1.2.4.0 && <1.3 if impl(ghc >= 9.6.2) build-depends: hspec ==2.11.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a856a76eb7..a46ad9a60c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -409,7 +409,7 @@ newChatController useXFTP = 0 } agentServers :: DB.Connection -> ChatConfig -> RandomServers -> IO InitialAgentServers - agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} randomServers = do + agentServers db ChatConfig {presetServers = PresetServers {operators = presetOps, ntf, netCfg}} rs = do users <- getUsers db opDomains <- operatorDomains <$> getUpdateServerOperators db presetOps (null users) smp' <- getServers SPSMP users opDomains @@ -418,9 +418,9 @@ newChatController where getServers :: forall p. (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> [User] -> [(Text, ServerOperator)] -> IO (Map UserId (NonEmpty (ServerCfg p))) getServers p users opDomains = do - let randomSrvs = rndServers p randomServers + let rs' = rndServers p rs fmap M.fromList $ forM users $ \u -> - (aUserId u,) . agentServerCfgs opDomains randomSrvs <$> getUpdateUserServers db p presetOps randomSrvs u + (aUserId u,) . agentServerCfgs opDomains rs' <$> getUpdateUserServers db p presetOps rs' u updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPort, tcpTimeout_, logTLSErrors} = diff --git a/src/Simplex/Chat/Operators.hs b/src/Simplex/Chat/Operators.hs index 81541074c8..55b62ad3ab 100644 --- a/src/Simplex/Chat/Operators.hs +++ b/src/Simplex/Chat/Operators.hs @@ -60,7 +60,7 @@ previousConditionsCommit = "edf99fcd1d7d38d2501d19608b94c084cf00f2ac" usageConditionsText :: Text usageConditionsText = $( let s = $(embedFile =<< makeRelativeToProject "PRIVACY.md") - in [|stripFrontMatter (safeDecodeUtf8 $(lift s))|] + in [|stripFrontMatter $(lift (safeDecodeUtf8 s))|] ) data DBStored = DBStored | DBNew