From 5ae0afe1fea05928a64ecb09b7cdf9d7bdfda536 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 6 Apr 2023 22:48:32 +0100 Subject: [PATCH] ios: update servers API/UI (#2149) * ios: update servers API/UI * fix UI * fix --- apps/ios/Shared/Model/SimpleXAPI.swift | 36 ++---- .../UserSettings/NetworkAndServers.swift | 12 +- .../UserSettings/ProtocolServerView.swift | 26 ++--- .../UserSettings/ProtocolServersView.swift | 26 +++-- apps/ios/SimpleX.xcodeproj/project.pbxproj | 40 +++---- apps/ios/SimpleXChat/APITypes.swift | 109 ++++++++++++------ src/Simplex/Chat/Controller.hs | 2 +- src/Simplex/Chat/Mobile.hs | 2 +- 8 files changed, 150 insertions(+), 103 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index e4291fd53a..ffaed725b6 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -391,36 +391,22 @@ func apiDeleteToken(token: DeviceToken) async throws { try await sendCommandOkResp(.apiDeleteToken(token: token)) } -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") - let r = chatSendCmdSync(.apiGetUserSMPServers(userId: userId)) - if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) } +func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers { + let userId = try currentUserId("getUserProtoServers") + let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol)) + if case let .userProtoServers(_, servers) = r { return servers } throw r } -func setUserProtocolServers(_ p: ServerProtocol, servers: [ServerCfg]) async throws { - if case .smp = p { - return try await setUserSMPServers(smpServers: servers) - } - throw RuntimeError("not supported") +func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws { + let userId = try currentUserId("setUserProtoServers") + try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers)) } -private func setUserSMPServers(smpServers: [ServerCfg]) async throws { - let userId = try currentUserId("setUserSMPServers") - try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers)) -} - -func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> { - let userId = try currentUserId("testSMPServer") - let r = await chatSendCmd(.apiTestSMPServer(userId: userId, smpServer: smpServer)) - if case let .smpTestResult(_, testFailure) = r { +func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> { + let userId = try currentUserId("testProtoServer") + let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server)) + if case let .serverTestResult(_, _, testFailure) = r { if let t = testFailure { return .failure(t) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index e18761ca54..77f043711b 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -25,6 +25,7 @@ private enum NetworkAlert: Identifiable { struct NetworkAndServers: View { @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @State private var netCfg = NetCfg.defaults @@ -43,6 +44,15 @@ struct NetworkAndServers: View { Text("SMP servers") } + if xftpSendEnabled { + NavigationLink { + ProtocolServersView(serverProtocol: .xftp) + .navigationTitle("Your XFTP servers") + } label: { + Text("XFTP servers") + } + } + Picker("Use .onion hosts", selection: $onionHosts) { ForEach(OnionHosts.values, id: \.self) { Text($0.text) } } @@ -62,7 +72,7 @@ struct NetworkAndServers: View { Text("Advanced network settings") } } header: { - Text("Messages") + Text("Messages & files") } footer: { Text("Using .onion hosts requires compatible VPN provider.") } diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift index 8e2e356137..48d5a66970 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift @@ -16,7 +16,7 @@ struct ProtocolServerView: View { @State var serverToEdit: ServerCfg @State private var showTestFailure = false @State private var testing = false - @State private var testFailure: SMPTestFailure? + @State private var testFailure: ProtocolTestFailure? var proto: String { serverProtocol.rawValue.uppercased() } @@ -60,7 +60,8 @@ struct ProtocolServerView: View { private func customServer() -> some View { VStack { - let valid = parseServerAddress(serverToEdit.server)?.valid == true + let serverAddress = parseServerAddress(serverToEdit.server) + let valid = serverAddress?.valid == true && serverAddress?.serverProtocol == serverProtocol List { Section { TextEditor(text: $serverToEdit.server) @@ -147,18 +148,17 @@ struct BackButton: ViewModifier { } } -func testServerConnection(server: Binding) async -> SMPTestFailure? { +func testServerConnection(server: Binding) async -> ProtocolTestFailure? { do { - let r = try await testSMPServer(smpServer: server.wrappedValue.server) - - switch r { - case .success: - await MainActor.run { server.wrappedValue.tested = true } - return nil - case let .failure(f): - await MainActor.run { server.wrappedValue.tested = false } - return f - } + let r = try await testProtoServer(server: server.wrappedValue.server) + switch r { + case .success: + await MainActor.run { server.wrappedValue.tested = true } + return nil + case let .failure(f): + await MainActor.run { server.wrappedValue.tested = false } + return f + } } catch let error { logger.error("testServerConnection \(responseError(error))") await MainActor.run { diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift index 60e53e695e..6b4e4bc927 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift @@ -21,7 +21,8 @@ struct ProtocolServersView: View { @State private var servers: [ServerCfg] = [] @State private var selectedServer: String? = nil @State private var showAddServer = false - @State private var showScanSMPServer = false + @State private var showScanProtoServer = false + @State private var justOpened = true @State private var testing = false @State private var alert: ServerAlert? = nil @State private var showSaveDialog = false @@ -38,7 +39,7 @@ struct ProtocolServersView: View { } enum ServerAlert: Identifiable { - case testsFailed(failures: [String: SMPTestFailure]) + case testsFailed(failures: [String: ProtocolTestFailure]) case error(title: LocalizedStringKey, error: LocalizedStringKey = "") var id: String { @@ -87,16 +88,17 @@ struct ProtocolServersView: View { servers.append(ServerCfg.empty) selectedServer = servers.last?.id } - Button("Scan server QR code") { showScanSMPServer = true } + Button("Scan server QR code") { showScanProtoServer = true } Button("Add preset servers", action: addAllPresets) .disabled(hasAllPresets()) } - .sheet(isPresented: $showScanSMPServer) { + .sheet(isPresented: $showScanProtoServer) { ScanProtocolServer(servers: $servers) } .modifier(BackButton { if saveDisabled { dismiss() + justOpened = false } else { showSaveDialog = true } @@ -105,6 +107,7 @@ struct ProtocolServersView: View { Button("Save") { saveServers() dismiss() + justOpened = false } Button("Exit without saving") { dismiss() } } @@ -125,8 +128,12 @@ struct ProtocolServersView: View { } } .onAppear { + // this condition is needed to prevent re-setting the servers when exiting single server view + if !justOpened { return } do { - (currServers, presetServers) = try getUserProtocolServers(serverProtocol) + let r = try getUserProtoServers(serverProtocol) + currServers = r.protoServers + presetServers = r.presetServers servers = currServers } catch let error { alert = .error( @@ -134,6 +141,7 @@ struct ProtocolServersView: View { error: "Error: \(responseError(error))" ) } + justOpened = false } } @@ -158,7 +166,7 @@ struct ProtocolServersView: View { let srv = server.wrappedValue return NavigationLink(tag: srv.id, selection: $selectedServer) { ProtocolServerView( - serverProtocol: .smp, + serverProtocol: serverProtocol, server: server, serverToEdit: srv ) @@ -258,8 +266,8 @@ struct ProtocolServersView: View { } } - private func runServersTest() async -> [String: SMPTestFailure] { - var fs: [String: SMPTestFailure] = [:] + private func runServersTest() async -> [String: ProtocolTestFailure] { + var fs: [String: ProtocolTestFailure] = [:] for i in 0.. String { - smpServers.isEmpty ? "default" : encodeJSON(SMPServersConfig(smpServers: smpServers)) + func protoServersStr(_ servers: [ServerCfg]) -> String { + encodeJSON(ProtoServersConfig(servers: servers)) } func chatItemTTLStr(seconds: Int64?) -> String { @@ -375,8 +375,8 @@ public enum ChatResponse: Decodable, Error { case chatSuspended case apiChats(user: User, chats: [ChatData]) case apiChat(user: User, chat: ChatData) - case userSMPServers(user: User, smpServers: [ServerCfg], presetSMPServers: [String]) - case smpTestResult(user: User, smpTestFailure: SMPTestFailure?) + case userProtoServers(user: User, servers: UserProtoServers) + case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?) case chatItemTTL(user: User, chatItemTTL: Int64?) case networkConfig(networkConfig: NetCfg) case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) @@ -488,8 +488,8 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return "chatSuspended" case .apiChats: return "apiChats" case .apiChat: return "apiChat" - case .userSMPServers: return "userSMPServers" - case .smpTestResult: return "smpTestResult" + case .userProtoServers: return "userProtoServers" + case .serverTestResult: return "serverTestResult" case .chatItemTTL: return "chatItemTTL" case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" @@ -601,8 +601,8 @@ public enum ChatResponse: Decodable, Error { case .chatSuspended: return noDetails case let .apiChats(u, chats): return withUser(u, String(describing: chats)) case let .apiChat(u, chat): return withUser(u, String(describing: chat)) - case let .userSMPServers(u, smpServers, presetServers): return withUser(u, "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))") - case let .smpTestResult(u, smpTestFailure): return withUser(u, String(describing: smpTestFailure)) + case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))") + case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\result: \(String(describing: testFailure))") case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL)) case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))") @@ -760,10 +760,19 @@ struct SMPServersConfig: Encodable { var smpServers: [ServerCfg] } -public enum ServerProtocol: String { +public enum ServerProtocol: String, Decodable { case smp case xftp - case ntf +} + +public struct ProtoServersConfig: Codable { + public var servers: [ServerCfg] +} + +public struct UserProtoServers: Decodable { + public var serverProtocol: ServerProtocol + public var protoServers: [ServerCfg] + public var presetServers: [String] } public struct ServerCfg: Identifiable, Equatable, Codable { @@ -830,29 +839,39 @@ public struct ServerCfg: Identifiable, Equatable, Codable { } } -public enum SMPTestStep: String, Decodable, Equatable { +public enum ProtocolTestStep: String, Decodable, Equatable { case connect + case disconnect case createQueue case secureQueue case deleteQueue - case disconnect + case createFile + case uploadFile + case downloadFile + case compareFile + case deleteFile var text: String { switch self { case .connect: return NSLocalizedString("Connect", comment: "server test step") + case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step") case .createQueue: return NSLocalizedString("Create queue", comment: "server test step") case .secureQueue: return NSLocalizedString("Secure queue", comment: "server test step") case .deleteQueue: return NSLocalizedString("Delete queue", comment: "server test step") - case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step") + case .createFile: return NSLocalizedString("Create file", comment: "server test step") + case .uploadFile: return NSLocalizedString("Upload file", comment: "server test step") + case .downloadFile: return NSLocalizedString("Download file", comment: "server test step") + case .compareFile: return NSLocalizedString("Compare file", comment: "server test step") + case .deleteFile: return NSLocalizedString("Delete file", comment: "server test step") } } } -public struct SMPTestFailure: Decodable, Error, Equatable { - var testStep: SMPTestStep +public struct ProtocolTestFailure: Decodable, Error, Equatable { + var testStep: ProtocolTestStep var testError: AgentErrorType - public static func == (l: SMPTestFailure, r: SMPTestFailure) -> Bool { + public static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool { l.testStep == r.testStep } @@ -861,6 +880,8 @@ public struct SMPTestFailure: Decodable, Error, Equatable { switch testError { case .SMP(.AUTH): return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") + case .XFTP(.AUTH): + return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") case .BROKER(_, .NETWORK): return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error") default: @@ -870,12 +891,14 @@ public struct SMPTestFailure: Decodable, Error, Equatable { } public struct ServerAddress: Decodable { + public var serverProtocol: ServerProtocol? public var hostnames: [String] public var port: String public var keyHash: String public var basicAuth: String - public init(hostnames: [String], port: String, keyHash: String, basicAuth: String = "") { + public init(serverProtocol: ServerProtocol?, hostnames: [String], port: String, keyHash: String, basicAuth: String = "") { + self.serverProtocol = serverProtocol self.hostnames = hostnames self.port = port self.keyHash = keyHash @@ -883,21 +906,25 @@ public struct ServerAddress: Decodable { } public var uri: String { - "smp://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))" + "\(serverProtocol?.rawValue ?? "smp")://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))" } public var valid: Bool { hostnames.count > 0 && Set(hostnames).count == hostnames.count } - static public var empty = ServerAddress( - hostnames: [], - port: "", - keyHash: "", - basicAuth: "" - ) + static func empty(_ serverProtocol: ServerProtocol) -> ServerAddress { + ServerAddress( + serverProtocol: serverProtocol, + hostnames: [], + port: "", + keyHash: "", + basicAuth: "" + ) + } static public var sampleData = ServerAddress( + serverProtocol: .smp, hostnames: ["smp.simplex.im", "1234.onion"], port: "", keyHash: "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=", @@ -1242,6 +1269,7 @@ public enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) case SMP(smpErr: ProtocolErrorType) + case XFTP(xftpErr: XFTPErrorType) case NTF(ntfErr: ProtocolErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) @@ -1283,6 +1311,21 @@ public enum ProtocolErrorType: Decodable { case INTERNAL } +public enum XFTPErrorType: Decodable { + case BLOCK + case SESSION + case CMD(cmdErr: ProtocolCommandError) + case AUTH + case SIZE + case QUOTA + case DIGEST + case CRYPTO + case NO_FILE + case HAS_FILE + case FILE_IO + case INTERNAL +} + public enum ProtocolCommandError: Decodable { case UNKNOWN case SYNTAX diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 56139fcf38..6f2dafeba0 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -697,7 +697,7 @@ data ParsedServerAddress = ParsedServerAddress instance ToJSON ParsedServerAddress where toEncoding = J.genericToEncoding J.defaultOptions data ServerAddress = ServerAddress - { protocol :: AProtocolType, + { serverProtocol :: AProtocolType, hostnames :: NonEmpty String, port :: String, keyHash :: String, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index bb6a030465..ae8e3241b8 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -198,7 +198,7 @@ chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack toServerAddress = \case Right (AProtoServerWithAuth protocol (ProtoServerWithAuth ProtocolServer {host, port, keyHash = C.KeyHash kh} auth)) -> let basicAuth = maybe "" (\(BasicAuth a) -> enc a) auth - in ParsedServerAddress (Just ServerAddress {protocol = AProtocolType protocol, hostnames = L.map enc host, port, keyHash = enc kh, basicAuth}) "" + in ParsedServerAddress (Just ServerAddress {serverProtocol = 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