From 1a3f0bed47a708a84915b6220227aeff58d6e8bf Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 5 Apr 2023 21:59:12 +0100 Subject: [PATCH] core: update servers API to include XFTP servers, ios: generalize UI to manage servers (#2140) * core: update servers API to include XFTP servers, ios: generalize UI to manage servers * add test * update migrations to pass tests * fix readme * update simplexmq --- apps/ios/Shared/Model/ChatModel.swift | 2 - apps/ios/Shared/Model/SimpleXAPI.swift | 31 +++-- .../UserSettings/NetworkAndServers.swift | 2 +- ...verView.swift => ProtocolServerView.swift} | 15 ++- ...rsView.swift => ProtocolServersView.swift} | 78 +++++++----- ...PServer.swift => ScanProtocolServer.swift} | 10 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 24 ++-- apps/ios/SimpleXChat/APITypes.swift | 6 + cabal.project | 2 +- package.yaml | 1 + scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 7 + src/Simplex/Chat.hs | 120 ++++++++++-------- src/Simplex/Chat/Controller.hs | 56 +++++--- .../M20230118_recreate_smp_servers.hs | 2 +- .../Migrations/M20230402_protocol_servers.hs | 20 +++ src/Simplex/Chat/Migrations/chat_schema.sql | 5 +- src/Simplex/Chat/Mobile.hs | 8 +- src/Simplex/Chat/Options.hs | 22 ++-- src/Simplex/Chat/Store.hs | 52 ++++---- src/Simplex/Chat/Types.hs | 20 +-- src/Simplex/Chat/View.hs | 56 +++++--- stack.yaml | 2 +- tests/ChatClient.hs | 2 +- tests/ChatTests/Direct.hs | 36 +++++- 25 files changed, 347 insertions(+), 234 deletions(-) rename apps/ios/Shared/Views/UserSettings/{SMPServerView.swift => ProtocolServerView.swift} (92%) rename apps/ios/Shared/Views/UserSettings/{SMPServersView.swift => ProtocolServersView.swift} (77%) rename apps/ios/Shared/Views/UserSettings/{ScanSMPServer.swift => ScanProtocolServer.swift} (84%) create mode 100644 src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 6f33e4264..382f96bd5 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -33,8 +33,6 @@ final class ChatModel: ObservableObject { // items in the terminal view @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: UserContactLink? - @Published var userSMPServers: [ServerCfg]? - @Published var presetSMPServers: [String]? @Published var chatItemTTL: ChatItemTTL = .none @Published var appOpenUrl: URL? @Published var deviceToken: DeviceToken? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7f81bc510..e4291fd53 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -391,22 +391,28 @@ func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } -func getUserSMPServers() throws -> ([ServerCfg], [String]) { +func getUserProtocolServers(_ p: ServerProtocol) throws -> ([ServerCfg], [String]) { + if case .smp = p { + return try getUserSMPServers() + } + throw RuntimeError("not supported") +} + +private func getUserSMPServers() throws -> ([ServerCfg], [String]) { let userId = try currentUserId("getUserSMPServers") - return try userSMPServersResponse(chatSendCmdSync(.apiGetUserSMPServers(userId: userId))) -} - -func getUserSMPServersAsync() async throws -> ([ServerCfg], [String]) { - let userId = try currentUserId("getUserSMPServersAsync") - return try userSMPServersResponse(await chatSendCmd(.apiGetUserSMPServers(userId: userId))) -} - -private func userSMPServersResponse(_ r: ChatResponse) throws -> ([ServerCfg], [String]) { + let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId)) if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) } throw r } -func setUserSMPServers(smpServers: [ServerCfg]) async throws { +func setUserProtocolServers(_ p: ServerProtocol, servers: [ServerCfg]) async throws { + if case .smp = p { + return try await setUserSMPServers(smpServers: servers) + } + throw RuntimeError("not supported") +} + +private func setUserSMPServers(smpServers: [ServerCfg]) async throws { let userId = try currentUserId("setUserSMPServers") try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers)) } @@ -1098,7 +1104,6 @@ func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws { func getUserChatData() throws { let m = ChatModel.shared m.userAddress = try apiGetUserAddress() - (m.userSMPServers, m.presetSMPServers) = try getUserSMPServers() m.chatItemTTL = try getChatItemTTL() let chats = try apiGetChats() m.chats = chats.map { Chat.init($0) } @@ -1106,13 +1111,11 @@ func getUserChatData() throws { private func getUserChatDataAsync() async throws { let userAddress = try await apiGetUserAddressAsync() - let servers = try await getUserSMPServersAsync() let chatItemTTL = try await getChatItemTTLAsync() let chats = try await apiGetChatsAsync() await MainActor.run { let m = ChatModel.shared m.userAddress = userAddress - (m.userSMPServers, m.presetSMPServers) = servers m.chatItemTTL = chatItemTTL m.chats = chats.map { Chat.init($0) } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index 17bc4121c..e18761ca5 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -37,7 +37,7 @@ struct NetworkAndServers: View { List { Section { NavigationLink { - SMPServersView() + ProtocolServersView(serverProtocol: .smp) .navigationTitle("Your SMP servers") } label: { Text("SMP servers") diff --git a/apps/ios/Shared/Views/UserSettings/SMPServerView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift similarity index 92% rename from apps/ios/Shared/Views/UserSettings/SMPServerView.swift rename to apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift index 454e1e718..8e2e35613 100644 --- a/apps/ios/Shared/Views/UserSettings/SMPServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift @@ -9,14 +9,17 @@ import SwiftUI import SimpleXChat -struct SMPServerView: View { +struct ProtocolServerView: View { @Environment(\.dismiss) var dismiss: DismissAction + let serverProtocol: ServerProtocol @Binding var server: ServerCfg @State var serverToEdit: ServerCfg @State private var showTestFailure = false @State private var testing = false @State private var testFailure: SMPTestFailure? + var proto: String { serverProtocol.rawValue.uppercased() } + var body: some View { ZStack { if server.preset { @@ -28,7 +31,7 @@ struct SMPServerView: View { ProgressView().scaleEffect(2) } } - .modifier(BackButton(label: "Your SMP servers") { + .modifier(BackButton(label: "Your \(proto) servers") { server = serverToEdit dismiss() }) @@ -169,8 +172,12 @@ func serverHostname(_ srv: String) -> String { parseServerAddress(srv)?.hostnames.first ?? srv } -struct SMPServerView_Previews: PreviewProvider { +struct ProtocolServerView_Previews: PreviewProvider { static var previews: some View { - SMPServerView(server: Binding.constant(ServerCfg.sampleData.custom), serverToEdit: ServerCfg.sampleData.custom) + ProtocolServerView( + serverProtocol: .smp, + server: Binding.constant(ServerCfg.sampleData.custom), + serverToEdit: ServerCfg.sampleData.custom + ) } } diff --git a/apps/ios/Shared/Views/UserSettings/SMPServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift similarity index 77% rename from apps/ios/Shared/Views/UserSettings/SMPServersView.swift rename to apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift index ab6b95477..60e53e695 100644 --- a/apps/ios/Shared/Views/UserSettings/SMPServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift @@ -1,5 +1,5 @@ // -// SMPServersView.swift +// ProtocolServersView.swift // SimpleX (iOS) // // Created by Evgeny on 15/11/2022. @@ -11,28 +11,33 @@ import SimpleXChat private let howToUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md")! -struct SMPServersView: View { +struct ProtocolServersView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var m: ChatModel @Environment(\.editMode) private var editMode - @State private var servers = ChatModel.shared.userSMPServers ?? [] + let serverProtocol: ServerProtocol + @State private var currServers: [ServerCfg] = [] + @State private var presetServers: [String] = [] + @State private var servers: [ServerCfg] = [] @State private var selectedServer: String? = nil @State private var showAddServer = false @State private var showScanSMPServer = false @State private var testing = false - @State private var alert: SMPServerAlert? = nil + @State private var alert: ServerAlert? = nil @State private var showSaveDialog = false + var proto: String { serverProtocol.rawValue.uppercased() } + var body: some View { ZStack { - smpServersView() + protocolServersView() if testing { ProgressView().scaleEffect(2) } } } - enum SMPServerAlert: Identifiable { + enum ServerAlert: Identifiable { case testsFailed(failures: [String: SMPTestFailure]) case error(title: LocalizedStringKey, error: LocalizedStringKey = "") @@ -44,11 +49,11 @@ struct SMPServersView: View { } } - private func smpServersView() -> some View { + private func protocolServersView() -> some View { List { Section { ForEach($servers) { srv in - smpServerView(srv) + protocolServerView(srv) } .onMove { indexSet, offset in servers.move(fromOffsets: indexSet, toOffset: offset) @@ -60,18 +65,18 @@ struct SMPServersView: View { showAddServer = true } } header: { - Text("SMP servers") + Text("\(proto) servers") } footer: { Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") .lineLimit(10) } Section { - Button("Reset") { servers = m.userSMPServers ?? [] } - .disabled(servers == m.userSMPServers || testing) + Button("Reset") { servers = currServers } + .disabled(servers == currServers || testing) Button("Test servers", action: testServers) .disabled(testing || allServersDisabled) - Button("Save servers", action: saveSMPServers) + Button("Save servers", action: saveServers) .disabled(saveDisabled) howToButton() } @@ -87,7 +92,7 @@ struct SMPServersView: View { .disabled(hasAllPresets()) } .sheet(isPresented: $showScanSMPServer) { - ScanSMPServer(servers: $servers) + ScanProtocolServer(servers: $servers) } .modifier(BackButton { if saveDisabled { @@ -98,7 +103,7 @@ struct SMPServersView: View { }) .confirmationDialog("Save servers?", isPresented: $showSaveDialog) { Button("Save") { - saveSMPServers() + saveServers() dismiss() } Button("Exit without saving") { dismiss() } @@ -119,11 +124,22 @@ struct SMPServersView: View { ) } } + .onAppear { + do { + (currServers, presetServers) = try getUserProtocolServers(serverProtocol) + servers = currServers + } catch let error { + alert = .error( + title: "Error loading \(proto) servers", + error: "Error: \(responseError(error))" + ) + } + } } private var saveDisabled: Bool { servers.isEmpty || - servers == m.userSMPServers || + servers == currServers || testing || !servers.allSatisfy { srv in if let address = parseServerAddress(srv.server) { @@ -138,12 +154,16 @@ struct SMPServersView: View { servers.allSatisfy { !$0.enabled } } - private func smpServerView(_ server: Binding) -> some View { + private func protocolServerView(_ server: Binding) -> some View { let srv = server.wrappedValue return NavigationLink(tag: srv.id, selection: $selectedServer) { - SMPServerView(server: server, serverToEdit: srv) - .navigationBarTitle(srv.preset ? "Preset server" : "Your server") - .navigationBarTitleDisplayMode(.large) + ProtocolServerView( + serverProtocol: .smp, + server: server, + serverToEdit: srv + ) + .navigationBarTitle(srv.preset ? "Preset server" : "Your server") + .navigationBarTitleDisplayMode(.large) } label: { let address = parseServerAddress(srv.server) HStack { @@ -201,11 +221,11 @@ struct SMPServersView: View { } private func hasAllPresets() -> Bool { - m.presetSMPServers?.allSatisfy { hasPreset($0) } ?? true + presetServers.allSatisfy { hasPreset($0) } } private func addAllPresets() { - for srv in m.presetSMPServers ?? [] { + for srv in presetServers { if !hasPreset(srv) { servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true)) } @@ -250,21 +270,21 @@ struct SMPServersView: View { return fs } - func saveSMPServers() { + func saveServers() { Task { do { - try await setUserSMPServers(smpServers: servers) + try await setUserProtocolServers(serverProtocol, servers: servers) await MainActor.run { - m.userSMPServers = servers + currServers = servers editMode?.wrappedValue = .inactive } } catch let error { let err = responseError(error) - logger.error("saveSMPServers setUserSMPServers error: \(err)") + logger.error("saveServers setUserProtocolServers error: \(err)") await MainActor.run { alert = .error( - title: "Error saving SMP servers", - error: "Make sure SMP server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))." + title: "Error saving \(proto) servers", + error: "Make sure \(proto) server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))." ) } } @@ -272,8 +292,8 @@ struct SMPServersView: View { } } -struct SMPServersView_Previews: PreviewProvider { +struct ProtocolServersView_Previews: PreviewProvider { static var previews: some View { - SMPServersView() + ProtocolServersView(serverProtocol: .smp) } } diff --git a/apps/ios/Shared/Views/UserSettings/ScanSMPServer.swift b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift similarity index 84% rename from apps/ios/Shared/Views/UserSettings/ScanSMPServer.swift rename to apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift index 37f41b2d5..c0ad4e3e1 100644 --- a/apps/ios/Shared/Views/UserSettings/ScanSMPServer.swift +++ b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift @@ -1,5 +1,5 @@ // -// ScanSMPServer.swift +// ScanProtocolServer.swift // SimpleX (iOS) // // Created by Evgeny on 19/11/2022. @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat import CodeScanner -struct ScanSMPServer: View { +struct ScanProtocolServer: View { @Environment(\.dismiss) var dismiss: DismissAction @Binding var servers: [ServerCfg] @State private var showAddressError = false @@ -47,14 +47,14 @@ struct ScanSMPServer: View { showAddressError = true } case let .failure(e): - logger.error("ScanSMPServer.processQRCode QR code error: \(e.localizedDescription)") + logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)") dismiss() } } } -struct ScanSMPServer_Previews: PreviewProvider { +struct ScanProtocolServer_Previews: PreviewProvider { static var previews: some View { - ScanSMPServer(servers: Binding.constant([])) + ScanProtocolServer(servers: Binding.constant([])) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 50aa95f6d..da11d8732 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -63,10 +63,10 @@ 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; }; 5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; }; 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; }; - 5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* SMPServersView.swift */; }; - 5C93293129239BED0090FFF9 /* SMPServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* SMPServerView.swift */; }; + 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; }; + 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; }; 5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */; }; - 5C9329412929248A0090FFF9 /* ScanSMPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9329402929248A0090FFF9 /* ScanSMPServer.swift */; }; + 5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */; }; 5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; }; 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; }; 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; }; @@ -307,10 +307,10 @@ 5C8B41CA29AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; - 5C93292E29239A170090FFF9 /* SMPServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServersView.swift; sourceTree = ""; }; - 5C93293029239BED0090FFF9 /* SMPServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServerView.swift; sourceTree = ""; }; + 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = ""; }; + 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServerView.swift; sourceTree = ""; }; 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecPlay.swift; sourceTree = ""; }; - 5C9329402929248A0090FFF9 /* ScanSMPServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanSMPServer.swift; sourceTree = ""; }; + 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanProtocolServer.swift; sourceTree = ""; }; 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = ""; }; 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = ""; }; 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = ""; }; @@ -685,9 +685,9 @@ 5CB924E027A867BA00ACCCDD /* UserProfile.swift */, 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, - 5C93292E29239A170090FFF9 /* SMPServersView.swift */, - 5C93293029239BED0090FFF9 /* SMPServerView.swift */, - 5C9329402929248A0090FFF9 /* ScanSMPServer.swift */, + 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, + 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, + 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, 5CB2084E28DA4B4800D024EC /* RTCServers.swift */, 5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */, 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */, @@ -1022,7 +1022,7 @@ 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */, 5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */, 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */, - 5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */, + 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, @@ -1032,7 +1032,7 @@ 5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */, 644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */, 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */, - 5C93293129239BED0090FFF9 /* SMPServerView.swift in Sources */, + 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */, 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, @@ -1122,7 +1122,7 @@ 6440CA00288857A10062C672 /* CIEventView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, - 5C9329412929248A0090FFF9 /* ScanSMPServer.swift in Sources */, + 5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, 5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */, 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 22eea39a7..1a7340730 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -760,6 +760,12 @@ struct SMPServersConfig: Encodable { var smpServers: [ServerCfg] } +public enum ServerProtocol: String { + case smp + case xftp + case ntf +} + public struct ServerCfg: Identifiable, Equatable, Codable { public var server: String public var preset: Bool diff --git a/cabal.project b/cabal.project index 2f308200c..d8726aee8 100644 --- a/cabal.project +++ b/cabal.project @@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 511a97c5d080193a84c4db1ddbc503d2eab3441f + tag: 9f8db135537a121dc1210e3dff96b4a480666c6f source-repository-package type: git diff --git a/package.yaml b/package.yaml index 6b6ba5e7e..bcf6150b6 100644 --- a/package.yaml +++ b/package.yaml @@ -20,6 +20,7 @@ dependencies: - base64-bytestring >= 1.0 && < 1.3 - bytestring == 0.10.* - composition == 1.0.* + - constraints >= 0.12 && < 0.14 - containers == 0.6.* - cryptonite >= 0.27 && < 0.30 - directory == 1.3.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 622e47032..1d8977a11 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."511a97c5d080193a84c4db1ddbc503d2eab3441f" = "0bpjyksrqq1s19r47m39q9dj7z5r8b1s1l28spv26503z0qi91fv"; + "https://github.com/simplex-chat/simplexmq.git"."9f8db135537a121dc1210e3dff96b4a480666c6f" = "0qlqzs916kpddmpk9f79h8i10is0yn83l7zc35sd7b9vw4gj8fhy"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b3b903e8130a7172b8dfa18b67bcc59620fc0ca0" = "040bkjf0i4p3n0q9z73735c7hgdlckajnvajfdycwzsmhiph9rnq"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index ee22cde69..a974126c6 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -89,6 +89,7 @@ library Simplex.Chat.Migrations.M20230318_file_description Simplex.Chat.Migrations.M20230321_agent_file_deleted Simplex.Chat.Migrations.M20230328_files_protocol + Simplex.Chat.Migrations.M20230402_protocol_servers Simplex.Chat.Mobile Simplex.Chat.Mobile.WebRTC Simplex.Chat.Options @@ -117,6 +118,7 @@ library , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , direct-sqlcipher ==2.3.* @@ -164,6 +166,7 @@ executable simplex-bot , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , direct-sqlcipher ==2.3.* @@ -212,6 +215,7 @@ executable simplex-bot-advanced , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , direct-sqlcipher ==2.3.* @@ -261,6 +265,7 @@ executable simplex-broadcast-bot , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , direct-sqlcipher ==2.3.* @@ -310,6 +315,7 @@ executable simplex-chat , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , direct-sqlcipher ==2.3.* @@ -372,6 +378,7 @@ test-suite simplex-chat-test , base64-bytestring >=1.0 && <1.3 , bytestring ==0.10.* , composition ==1.0.* + , constraints >=0.12 && <0.14 , containers ==0.6.* , cryptonite >=0.27 && <0.30 , deepseq ==1.4.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index f9108ab3a..727926dcf 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -29,6 +29,7 @@ import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.Char (isSpace) +import Data.Constraint (Dict (..)) import Data.Either (fromRight, rights) import Data.Fixed (div') import Data.Functor (($>)) @@ -72,7 +73,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), UserProtocol, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxy) @@ -182,27 +183,32 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen agentServers :: ChatConfig -> IO InitialAgentServers agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do users <- withTransaction chatStore getUsers - smp' <- getUserServers users smp - xftp' <- getUserServers users xftp + smp' <- getUserServers users SPSMP + xftp' <- getUserServers users SPXFTP pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. ProtocolTypeI p => [User] -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p))) - getUserServers users srvSel = case users of - [] -> pure $ M.fromList [(1, srvSel defServers)] + getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p))) + getUserServers users protocol = case users of + [] -> pure $ M.fromList [(1, cfgServers protocol defServers)] _ -> M.fromList <$> initialServers where initialServers :: IO [(UserId, NonEmpty (ProtoServerWithAuth p))] initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) - userServers user' = activeAgentServers config srvSel <$> withTransaction chatStore (`getProtocolServers` user') + userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') -activeAgentServers :: ChatConfig -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) -activeAgentServers ChatConfig {defaultServers} srvSel = - fromMaybe (srvSel defaultServers) +activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) +activeAgentServers ChatConfig {defaultServers} p = + fromMaybe (cfgServers p defaultServers) . nonEmpty . map (\ServerCfg {server} -> server) . filter (\ServerCfg {enabled} -> enabled) +cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) +cfgServers = \case + SPSMP -> smp + SPXFTP -> xftp + startChatController :: forall m. ChatMonad' m => Bool -> Bool -> m (Async ()) startChatController subConns enableExpireCIs = do asks smpAgent >>= resumeAgentClient @@ -294,33 +300,37 @@ processChatCommand = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser p@Profile {displayName} sameServers -> do u <- asks currentUser - (smp, smpServers) <- chooseServers + (smp, smpServers) <- chooseServers SPSMP + (xftp, xftpServers) <- chooseServers SPXFTP auId <- withStore' getUsers >>= \case [] -> pure 1 users -> do when (any (\User {localDisplayName = n} -> n == displayName) users) $ throwChatError $ CEUserExists displayName - withAgent (`createUser` smp) + withAgent (\a -> createUser a smp xftp) user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True - unless (null smpServers) $ - withStore $ \db -> overwriteSMPServers db user smpServers + storeServers user smpServers + storeServers user xftpServers setActive ActiveNone atomically . writeTVar u $ Just user pure $ CRActiveUser user where - chooseServers :: m (NonEmpty SMPServerWithAuth, [ServerCfg 'PSMP]) - chooseServers + chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> m (NonEmpty (ProtoServerWithAuth p), [ServerCfg p]) + chooseServers protocol | sameServers = asks currentUser >>= readTVarIO >>= \case Nothing -> throwChatError CENoActiveUser Just user -> do - smpServers <- withStore' (`getSMPServers` user) + smpServers <- withStore' (`getProtocolServers` user) cfg <- asks config - pure (activeAgentServers cfg smp smpServers, smpServers) + pure (activeAgentServers cfg protocol smpServers, smpServers) | otherwise = do - DefaultAgentServers {smp} <- asks $ defaultServers . config - pure (smp, []) + defServers <- asks $ defaultServers . config + pure (cfgServers protocol defServers, []) + storeServers user servers = + unless (null servers) $ + withStore $ \db -> overwriteProtocolServers db user servers ListUsers -> CRUsersList <$> withStore' getUsersInfo APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do user' <- privateGetUser userId' @@ -902,30 +912,29 @@ processChatCommand = \case pure user_ $>>= \user -> withStore (\db -> Just <$> getConnectionEntity db user agentConnId) `catchError` (\e -> toView (CRChatError (Just user) e) $> Nothing) pure CRNtfMessages {user_, connEntity, msgTs = msgTs', ntfMessages} - APIGetUserSMPServers userId -> withUserId userId $ \user -> do - ChatConfig {defaultServers = DefaultAgentServers {smp = defaultSMPServers}} <- asks config - smpServers <- withStore' (`getSMPServers` user) - let smpServers' = fromMaybe (L.map toServerCfg defaultSMPServers) $ nonEmpty smpServers - pure $ CRUserSMPServers user smpServers' defaultSMPServers + APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do + ChatConfig {defaultServers} <- asks config + servers <- withStore' (`getProtocolServers` user) + let defServers = cfgServers p defaultServers + servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers + pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers where toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} - GetUserSMPServers -> withUser $ \User {userId} -> - processChatCommand $ APIGetUserSMPServers userId - APISetUserSMPServers userId (SMPServersConfig smpServers) -> withUserId userId $ \user -> withChatLock "setUserSMPServers" $ do - withStore $ \db -> overwriteSMPServers db user smpServers - cfg <- asks config - withAgent $ \a -> setSMPServers a (aUserId user) $ activeAgentServers cfg smp smpServers - ok user - SetUserSMPServers smpServersConfig -> withUser $ \User {userId} -> - processChatCommand $ APISetUserSMPServers userId smpServersConfig - APIGetUserServers _ -> pure $ chatCmdError Nothing "TODO" - GetUserServers -> pure $ chatCmdError Nothing "TODO" - APISetUserServers _ _ -> pure $ chatCmdError Nothing "TODO" - SetUserServers _ -> pure $ chatCmdError Nothing "TODO" - APITestSMPServer userId smpServer -> withUserId userId $ \user -> - CRSmpTestResult user <$> withAgent (\a -> testSMPServerConnection a (aUserId user) smpServer) - TestSMPServer smpServer -> withUser $ \User {userId} -> - processChatCommand $ APITestSMPServer userId smpServer + GetUserProtoServers aProtocol -> withUser $ \User {userId} -> + processChatCommand $ APIGetUserProtoServers userId aProtocol + APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ + withChatLock "setUserSMPServers" $ do + withStore $ \db -> overwriteProtocolServers db user servers + cfg <- asks config + withAgent $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers + ok user + SetUserProtoServers serversConfig -> withUser $ \User {userId} -> + processChatCommand $ APISetUserProtoServers userId serversConfig + APITestProtoServer userId srv@(AProtoServerWithAuth p server) -> withUserId userId $ \user -> + withServerProtocol p $ + CRServerTestResult user srv <$> withAgent (\a -> testProtocolServer a (aUserId user) server) + TestProtoServer srv -> withUser $ \User {userId} -> + processChatCommand $ APITestProtoServer userId srv APISetChatItemTTL userId newTTL_ -> withUser' $ \user -> do checkSameUser userId user checkStoreNotChanged $ @@ -1661,6 +1670,10 @@ processChatCommand = \case atomically $ TM.delete ctId calls ok user | otherwise -> throwChatError $ CECallContact contactId + withServerProtocol :: ProtocolTypeI p => SProtocolType p -> (UserProtocol p => m a) -> m a + withServerProtocol p action = case userProtocol p of + Just Dict -> action + _ -> throwChatError $ CEServerProtocol $ AProtocolType p forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse forwardFile chatName fileId sendCommand = withUser $ \user -> do withStore (\db -> getFileTransfer db user fileId) >>= \case @@ -4457,17 +4470,17 @@ chatCommandP = "/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal), "/_leave #" *> (APILeaveGroup <$> A.decimal), "/_members #" *> (APIListMembers <$> A.decimal), - -- /smp_servers is deprecated, use /smp and /_smp - "/smp_servers default" $> SetUserSMPServers (SMPServersConfig []), - "/smp_servers " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP), - "/smp_servers" $> GetUserSMPServers, - "/smp default" $> SetUserSMPServers (SMPServersConfig []), - "/_smp test " *> (APITestSMPServer <$> A.decimal <* A.space <*> strP), - "/smp test " *> (TestSMPServer <$> strP), - "/_smp " *> (APISetUserSMPServers <$> A.decimal <* A.space <*> jsonP), - "/smp " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP), - "/_smp " *> (APIGetUserSMPServers <$> A.decimal), - "/smp" $> GetUserSMPServers, + "/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP), + "/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP), + "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), + "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), + "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map toServerCfg <$> protocolServersP), + "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), + "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map toServerCfg <$> protocolServersP), + "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), + "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), + "/smp" $> GetUserProtoServers (AProtocolType SPSMP), + "/xftp" $> GetUserProtoServers (AProtocolType SPXFTP), "/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal), "/ttl " *> (SetChatItemTTL <$> ciTTL), "/_ttl " *> (APIGetChatItemTTL <$> A.decimal), @@ -4686,6 +4699,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) toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True} char_ = optional . A.char diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 94db939f2..56139fcf3 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -4,9 +4,11 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} @@ -45,7 +47,7 @@ import Simplex.Chat.Protocol import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink) import Simplex.Chat.Types import Simplex.Messaging.Agent (AgentClient) -import Simplex.Messaging.Agent.Client (AgentLocks, SMPTestFailure) +import Simplex.Messaging.Agent.Client (AgentLocks, ProtocolTestFailure) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol @@ -54,7 +56,7 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags, NtfServer, ProtocolType (..), QueueId, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) @@ -258,16 +260,12 @@ data ChatCommand | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId - | APIGetUserSMPServers UserId - | GetUserSMPServers - | APISetUserSMPServers UserId SMPServersConfig - | SetUserSMPServers SMPServersConfig - | APIGetUserServers UserId - | GetUserServers - | APISetUserServers UserId ServersConfig - | SetUserServers ServersConfig - | APITestSMPServer UserId SMPServerWithAuth - | TestSMPServer SMPServerWithAuth + | APIGetUserProtoServers UserId AProtocolType + | GetUserProtoServers AProtocolType + | APISetUserProtoServers UserId AProtoServersConfig + | SetUserProtoServers AProtoServersConfig + | APITestProtoServer UserId AProtoServerWithAuth + | TestProtoServer AProtoServerWithAuth | APISetChatItemTTL UserId (Maybe Int64) | SetChatItemTTL (Maybe Int64) | APIGetChatItemTTL UserId @@ -386,8 +384,8 @@ data ChatResponse | CRChatItems {user :: User, chatItems :: [AChatItem]} | CRChatItemId User (Maybe ChatItemId) | CRApiParsedMarkdown {formattedText :: Maybe MarkdownList} - | CRUserSMPServers {user :: User, smpServers :: NonEmpty (ServerCfg 'PSMP), presetSMPServers :: NonEmpty SMPServerWithAuth} - | CRSmpTestResult {user :: User, smpTestFailure :: Maybe SMPTestFailure} + | CRUserProtoServers {user :: User, servers :: AUserProtoServers} + | CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure} | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} @@ -566,11 +564,31 @@ instance ToJSON AgentQueueId where toJSON = strToJSON toEncoding = strToJEncoding -data SMPServersConfig = SMPServersConfig {smpServers :: [ServerCfg 'PSMP]} +data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]} deriving (Show, Generic, FromJSON) -data ServersConfig = ServersConfig {servers :: [AServerCfg]} - deriving (Show, Generic, FromJSON) +data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p) + +deriving instance Show AProtoServersConfig + +data UserProtoServers p = UserProtoServers + { serverProtocol :: SProtocolType p, + protoServers :: NonEmpty (ServerCfg p), + presetServers :: NonEmpty (ProtoServerWithAuth p) + } + deriving (Show, Generic) + +instance ProtocolTypeI p => ToJSON (UserProtoServers p) where + toJSON = J.genericToJSON J.defaultOptions + toEncoding = J.genericToEncoding J.defaultOptions + +data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p) + +instance ToJSON AUserProtoServers where + toJSON (AUPS s) = J.genericToJSON J.defaultOptions s + toEncoding (AUPS s) = J.genericToEncoding J.defaultOptions s + +deriving instance Show AUserProtoServers data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath} deriving (Show, Generic, FromJSON) @@ -679,7 +697,8 @@ data ParsedServerAddress = ParsedServerAddress instance ToJSON ParsedServerAddress where toEncoding = J.genericToEncoding J.defaultOptions data ServerAddress = ServerAddress - { hostnames :: NonEmpty String, + { protocol :: AProtocolType, + hostnames :: NonEmpty String, port :: String, keyHash :: String, basicAuth :: String @@ -794,6 +813,7 @@ data ChatErrorType | CEAgentVersion | CEAgentNoSubResult {agentConnId :: AgentConnId} | CECommandError {message :: String} + | CEServerProtocol {serverProtocol :: AProtocolType} | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} | CEInternalError {message :: String} diff --git a/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs b/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs index 3098c31d7..6253a3a37 100644 --- a/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs +++ b/src/Simplex/Chat/Migrations/M20230118_recreate_smp_servers.hs @@ -35,5 +35,5 @@ SELECT DROP TABLE smp_servers; ALTER TABLE new_smp_servers RENAME TO smp_servers; -CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id); +CREATE INDEX idx_smp_servers_user_id ON "smp_servers"(user_id); |] diff --git a/src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs b/src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs new file mode 100644 index 000000000..bffe7ac81 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230402_protocol_servers.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230402_protocol_servers where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230402_protocol_servers :: Query +m20230402_protocol_servers = + [sql| +ALTER TABLE smp_servers RENAME TO protocol_servers; +ALTER TABLE protocol_servers ADD COLUMN protocol TEXT NOT NULL DEFAULT 'smp'; +|] + +down_m20230402_protocol_servers :: Query +down_m20230402_protocol_servers = + [sql| +ALTER TABLE protocol_servers DROP COLUMN protocol; +ALTER TABLE protocol_servers RENAME TO smp_servers; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 4fc96eb5d..08e07bed2 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -547,7 +547,7 @@ CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks( CREATE INDEX idx_snd_files_group_member_id ON snd_files(group_member_id); CREATE INDEX idx_snd_files_connection_id ON snd_files(connection_id); CREATE INDEX idx_snd_files_file_id ON snd_files(file_id); -CREATE TABLE IF NOT EXISTS "smp_servers"( +CREATE TABLE IF NOT EXISTS "protocol_servers"( smp_server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, port TEXT NOT NULL, @@ -559,9 +559,10 @@ CREATE TABLE IF NOT EXISTS "smp_servers"( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), + protocol TEXT NOT NULL DEFAULT 'smp', UNIQUE(user_id, host, port) ); -CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id); +CREATE INDEX idx_smp_servers_user_id ON "protocol_servers"(user_id); CREATE INDEX idx_chat_items_item_deleted_by_group_member_id ON chat_items( item_deleted_by_group_member_id ); diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 321367136..bb6a03046 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -44,7 +44,7 @@ import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..), SMPServerWithAuth) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) import System.Timeout (timeout) @@ -194,11 +194,11 @@ chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownLi chatParseServer :: String -> JSONString chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack where - toServerAddress :: Either String SMPServerWithAuth -> ParsedServerAddress + toServerAddress :: Either String AProtoServerWithAuth -> ParsedServerAddress toServerAddress = \case - Right (ProtoServerWithAuth ProtocolServer {host, port, keyHash = C.KeyHash kh} auth) -> + Right (AProtoServerWithAuth protocol (ProtoServerWithAuth ProtocolServer {host, port, keyHash = C.KeyHash kh} auth)) -> let basicAuth = maybe "" (\(BasicAuth a) -> enc a) auth - in ParsedServerAddress (Just ServerAddress {hostnames = L.map enc host, port, keyHash = enc kh, basicAuth}) "" + in ParsedServerAddress (Just ServerAddress {protocol = AProtocolType protocol, hostnames = L.map enc host, port, keyHash = enc kh, basicAuth}) "" Left e -> ParsedServerAddress Nothing e enc :: StrEncoding a => a -> String enc = B.unpack . strEncode diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 20053a806..b9dc2e363 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -11,7 +11,7 @@ module Simplex.Chat.Options chatOptsP, coreChatOptsP, getChatOpts, - smpServersP, + protocolServersP, fullNetworkConfig, ) where @@ -25,7 +25,7 @@ import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, ver import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Protocol (SMPServerWithAuth, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (ProtocolTypeI, ProtoServerWithAuth, SMPServerWithAuth, XFTPServerWithAuth) import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy) import System.FilePath (combine) @@ -82,7 +82,7 @@ coreChatOptsP appDir defaultDbFileName = do ) smpServers <- option - parseSMPServers + parseProtocolServers ( long "server" <> short 's' <> metavar "SERVER" @@ -91,7 +91,7 @@ coreChatOptsP appDir defaultDbFileName = do ) xftpServers <- option - parseXFTPServers + parseProtocolServers ( long "xftp-server" <> metavar "SERVER" <> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)" @@ -243,11 +243,8 @@ fullNetworkConfig socksProxy tcpTimeout logTLSErrors = let tcpConnectTimeout = (tcpTimeout * 3) `div` 2 in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors} -parseSMPServers :: ReadM [SMPServerWithAuth] -parseSMPServers = eitherReader $ parseAll smpServersP . B.pack - -parseXFTPServers :: ReadM [XFTPServerWithAuth] -parseXFTPServers = eitherReader $ parseAll xftpServersP . B.pack +parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p] +parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack parseSocksProxy :: ReadM (Maybe SocksProxy) parseSocksProxy = eitherReader $ parseAll strP . B.pack @@ -258,11 +255,8 @@ parseServerPort = eitherReader $ parseAll serverPortP . B.pack serverPortP :: A.Parser (Maybe String) serverPortP = Just . B.unpack <$> A.takeWhile A.isDigit -smpServersP :: A.Parser [SMPServerWithAuth] -smpServersP = strP `A.sepBy1` A.char ';' - -xftpServersP :: A.Parser [XFTPServerWithAuth] -xftpServersP = strP `A.sepBy1` A.char ';' +protocolServersP :: ProtocolTypeI p => A.Parser [ProtoServerWithAuth p] +protocolServersP = strP `A.sepBy1` A.char ';' parseLogLevel :: ReadM ChatLogLevel parseLogLevel = eitherReader $ \case diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 84e5b8f8a..b2f277811 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -246,8 +246,6 @@ module Simplex.Chat.Store updateGroupChatItemsRead, getGroupUnreadTimedItems, setGroupChatItemDeleteAt, - getSMPServers, - overwriteSMPServers, getProtocolServers, overwriteProtocolServers, createCall, @@ -297,7 +295,7 @@ import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe, mapMaybe) import Data.Ord (Down (..)) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Data.Time.LocalTime (TimeZone, getCurrentTimeZone) @@ -365,6 +363,7 @@ import Simplex.Chat.Migrations.M20230317_hidden_profiles import Simplex.Chat.Migrations.M20230318_file_description import Simplex.Chat.Migrations.M20230321_agent_file_deleted import Simplex.Chat.Migrations.M20230328_files_protocol +import Simplex.Chat.Migrations.M20230402_protocol_servers import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) @@ -372,8 +371,9 @@ import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, Invitation import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, MigrationError, SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), pattern SMPServer) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..)) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8) import UnliftIO.STM @@ -435,7 +435,8 @@ schemaMigrations = ("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles), ("20230318_file_description", m20230318_file_description, Just down_m20230318_file_description), ("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted), - ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol) + ("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol), + ("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers) ] -- | The list of migrations in ascending order by date @@ -4849,49 +4850,42 @@ toGroupChatItemList tz currentTs userContactId (((Just itemId, Just itemTs, Just either (const []) (: []) $ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) toGroupChatItemList _ _ _ _ = [] -getSMPServers :: DB.Connection -> User -> IO [ServerCfg 'PSMP] -getSMPServers db User {userId} = +getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] +getProtocolServers db User {userId} = map toServerCfg <$> DB.query db [sql| SELECT host, port, key_hash, basic_auth, preset, tested, enabled - FROM smp_servers - WHERE user_id = ?; + FROM protocol_servers + WHERE user_id = ? AND protocol = ?; |] - (Only userId) + (userId, decodeLatin1 $ strEncode protocol) where - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg 'PSMP + 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) = - let server = ProtoServerWithAuth (SMPServer host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) in ServerCfg {server, preset, tested, enabled} -overwriteSMPServers :: DB.Connection -> User -> [ServerCfg 'PSMP] -> ExceptT StoreError IO () -overwriteSMPServers db User {userId} servers = +overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () +overwriteProtocolServers db User {userId} servers = checkConstraint SEUniqueID . ExceptT $ do currentTs <- getCurrentTime - DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId) + 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 DB.execute db [sql| - INSERT INTO smp_servers - (host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?,?,?) + INSERT INTO protocol_servers + (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - (host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, preset, tested, enabled, userId, currentTs, currentTs) + ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) pure $ Right () - -getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] -getProtocolServers db user = case protocolTypeI @p of - SPSMP -> getSMPServers db user - _ -> pure [] -- TODO read from the new table of all servers (alternatively, we could migrate data) - -overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () -overwriteProtocolServers db user servers = case protocolTypeI @p of - SPSMP -> overwriteSMPServers db user servers - _ -> pure () -- TODO write the new table of all servers + where + protocol = decodeLatin1 $ strEncode $ protocolTypeI @p createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 3c17570ca..23aa4f250 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -52,7 +52,7 @@ import Simplex.FileTransfer.Description (FileDigest) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtoServerWithAuth, ProtocolTypeI) +import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) class IsContact a where @@ -2100,21 +2100,3 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where instance ProtocolTypeI p => FromJSON (ServerCfg p) where parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} - -data AServerCfg = AServerCfg - { server :: AProtoServerWithAuth, - preset :: Bool, - tested :: Maybe Bool, - enabled :: Bool - } - -deriving instance Show AServerCfg - -deriving instance Generic AServerCfg - -instance ToJSON AServerCfg where - toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} - toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} - -instance FromJSON AServerCfg where - parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d1be04e1e..6a401f69d 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -18,10 +18,12 @@ import Data.Char (toUpper) import Data.Function (on) import Data.Int (Int64) import Data.List (groupBy, intercalate, intersperse, partition, sortOn) +import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Encoding (decodeLatin1) import Data.Time.Clock (DiffTime, UTCTime) import Data.Time.Format (defaultTimeLocale, formatTime) import Data.Time.LocalTime (ZonedTime (..), localDay, localTimeOfDay, timeOfDayToTime, utcToZonedTime) @@ -38,14 +40,15 @@ import Simplex.Chat.Protocol import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (..)) import Simplex.Chat.Styled import Simplex.Chat.Types -import Simplex.Messaging.Agent.Client (SMPTestFailure (..), SMPTestStep (..)) +import qualified Simplex.FileTransfer.Protocol as XFTP +import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtocolType, ProtocolServer (..), ProtocolTypeI) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (bshow, tshow) @@ -68,8 +71,8 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case CRChats chats -> viewChats ts chats CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] - CRUserSMPServers u smpServers _ -> ttyUser u $ viewSMPServers (L.toList smpServers) testView - CRSmpTestResult u testFailure -> ttyUser u $ viewSMPTestResult testFailure + CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView + CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile @@ -748,36 +751,46 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho ] -- TODO make more generic messages or split -viewSMPServers :: ProtocolTypeI p => [ServerCfg p] -> Bool -> [StyledString] -viewSMPServers servers testView = +viewUserServers :: AUserProtoServers -> Bool -> [StyledString] +viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers}) testView = if testView then [customServers] else [ customServers, "", - "use " <> highlight' "/smp test " <> " to test SMP server connection", - "use " <> highlight' "/smp set " <> " to switch to custom SMP servers", - "use " <> highlight' "/smp default" <> " to remove custom SMP servers and use default", - "(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)" + "use " <> highlight (srvCmd <> " test ") <> " to test " <> pName <> " server connection", + "use " <> highlight (srvCmd <> " set ") <> " to switch to custom " <> pName <> " servers", + "use " <> highlight (srvCmd <> " default") <> " to remove custom " <> pName <> " servers and use default" ] + <> 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 servers + if null protoServers then "no custom SMP servers saved" - else viewServers servers + else viewServers protoServers -viewSMPTestResult :: Maybe SMPTestFailure -> [StyledString] -viewSMPTestResult = \case - Just SMPTestFailure {testStep, testError} -> +protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString +protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode + +viewServerTestResult :: AProtoServerWithAuth -> Maybe ProtocolTestFailure -> [StyledString] +viewServerTestResult (AProtoServerWithAuth p _) = \case + Just ProtocolTestFailure {testStep, testError} -> result - <> ["Server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH] - <> ["Possibly, certificate fingerprint in server address is incorrect" | testStep == TSConnect && brokerErr] + <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH] + <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && testError == XFTP XFTP.AUTH] + <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] where - result = ["SMP server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] + result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] brokerErr = case testError of BROKER _ NETWORK -> True _ -> False - _ -> ["SMP server test passed"] + _ -> [pName <> " server test passed"] + where + pName = protocolName p viewChatItemTTL :: Maybe Int64 -> [StyledString] viewChatItemTTL = \case @@ -825,8 +838,8 @@ viewConnectionStats ConnectionStats {rcvServers, sndServers} = ["receiving messages via: " <> viewServerHosts rcvServers | not $ null rcvServers] <> ["sending messages via: " <> viewServerHosts sndServers | not $ null sndServers] -viewServers :: ProtocolTypeI p => [ServerCfg p] -> StyledString -viewServers = plain . intercalate ", " . map (B.unpack . strEncode . (\ServerCfg {server} -> server)) +viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> StyledString +viewServers = plain . intercalate ", " . map (B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList viewServerHosts :: [SMPServer] -> StyledString viewServerHosts = plain . intercalate ", " . map showSMPServer @@ -1304,6 +1317,7 @@ viewChatError logLevel = \case CEDirectMessagesProhibited dir ct -> viewDirectMessagesProhibited dir ct CEAgentVersion -> ["unsupported agent version"] CEAgentNoSubResult connId -> ["no subscription result for connection: " <> sShow connId] + CEServerProtocol p -> [plain $ "Servers for protocol " <> strEncode p <> " cannot be configured by the users"] CECommandError e -> ["bad chat command: " <> plain e] CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] diff --git a/stack.yaml b/stack.yaml index 1ec5cb240..d50a589b7 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 511a97c5d080193a84c4db1ddbc503d2eab3441f + commit: 9f8db135537a121dc1210e3dff96b4a480666c6f - github: kazu-yamamoto/http2 commit: b3b903e8130a7172b8dfa18b67bcc59620fc0ca0 # - ../direct-sqlcipher diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index f888bedcc..3a6315841 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -59,7 +59,7 @@ testOpts = 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=@localhost:7002"], + xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], networkConfig = defaultNetworkConfig, logLevel = CLLImportant, logConnections = False, diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 14345f145..92dc654cc 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -36,6 +36,9 @@ chatDirectTests = do describe "SMP servers" $ do it "get and set SMP servers" testGetSetSMPServers it "test SMP server connection" testTestSMPServerConnection + describe "XFTP servers" $ do + it "get and set XFTP servers" testGetSetXFTPServers + it "test XFTP server connection" testTestXFTPServer describe "async connection handshake" $ do it "connect when initiating client goes offline" testAsyncInitiatingOffline it "connect when accepting client goes offline" testAsyncAcceptingOffline @@ -393,7 +396,7 @@ testGetSetSMPServers :: HasCallStack => FilePath -> IO () testGetSetSMPServers = testChat2 aliceProfile bobProfile $ \alice _ -> do - alice #$> ("/_smp 1", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") + alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok") alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im") alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok") @@ -416,7 +419,36 @@ testTestSMPServerConnection = alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK" - alice <## "Possibly, certificate fingerprint in server address is incorrect" + alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" + +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 #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok") + alice #$> ("/xftp", id, "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 xftp://2345-w==@xftp2.example.im;xftp://3456-w==@xftp3.example.im:5224", id, "ok") + alice #$> ("/xftp", id, "xftp://2345-w==@xftp2.example.im, xftp://3456-w==@xftp3.example.im:5224") + alice #$> ("/xftp default", id, "ok") + alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") + +testTestXFTPServer :: HasCallStack => FilePath -> IO () +testTestXFTPServer = + testChat2 aliceProfile bobProfile $ + \alice _ -> withXFTPServer $ do + alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002" + alice <## "XFTP server test passed" + -- to test with password: + -- alice <## "XFTP server test failed at CreateFile, error: XFTP AUTH" + -- alice <## "Server requires authorization to upload files, check password" + alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" + alice <## "XFTP server test passed" + alice ##> "/xftp test xftp://LcJU@localhost:7002" + alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK" + alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO () testAsyncInitiatingOffline tmp = do