core, ios: advanced server config (#1371)

* ios: advanced server config

* simplify UI

* core: ServerCfg

* commit migration, update schema

* add preset servers to response

* return default servers if none saved

* fix test
This commit is contained in:
Evgeny Poberezkin
2022-11-16 15:37:20 +00:00
committed by GitHub
parent f8302e2030
commit 491fe4a9bf
21 changed files with 545 additions and 81 deletions
+11
View File
@@ -319,6 +319,17 @@ func setUserSMPServers(smpServers: [String]) async throws {
try await sendCommandOkResp(.setUserSMPServers(smpServers: smpServers))
}
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
let r = await chatSendCmd(.testSMPServer(smpServer: smpServer))
if case let .sMPTestResult(testFailure) = r {
if let t = testFailure {
return .failure(t)
}
return .success(())
}
throw r
}
func getChatItemTTL() throws -> ChatItemTTL {
let r = chatSendCmdSync(.apiGetChatItemTTL)
if case let .chatItemTTL(chatItemTTL) = r { return ChatItemTTL(chatItemTTL) }
@@ -40,6 +40,13 @@ struct NetworkAndServers: View {
Text("SMP servers")
}
NavigationLink {
SMPServersView()
.navigationTitle("Your SMP servers")
} label: {
Text("SMP servers (new)")
}
Picker("Use .onion hosts", selection: $onionHosts) {
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
}
@@ -0,0 +1,105 @@
//
// SMPServerView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 15/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct SMPServerView: View {
@State var server: ServerCfg
var body: some View {
if server.preset {
presetServer()
} else {
customServer()
}
}
private func presetServer() -> some View {
return VStack {
List {
Section("Preset server address") {
Text(server.server)
}
useServerSection()
}
}
}
private func customServer() -> some View {
VStack {
List {
Section("Your server address") {
TextEditor(text: $server.server)
.multilineTextAlignment(.leading)
.autocorrectionDisabled(true)
.autocapitalization(.none)
.lineLimit(10)
.frame(height: 108)
.padding(-6)
}
useServerSection()
Section("Add to another device") {
QRCode(uri: server.server)
.listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12))
}
}
}
}
private func useServerSection() -> some View {
Section("Use server") {
HStack {
Button("Test server") {
Task { await testServerConnection(server: $server) }
}
Spacer()
showTestStatus(server: server)
}
Toggle("Enabled", isOn: $server.enabled)
Button("Remove server", role: .destructive) {
}
}
}
}
@ViewBuilder func showTestStatus(server: ServerCfg) -> some View {
switch server.tested {
case .some(true):
Image(systemName: "checkmark")
.foregroundColor(.green)
case .some(false):
Image(systemName: "multiply")
.foregroundColor(.red)
case .none:
Color.clear
}
}
func testServerConnection(server: Binding<ServerCfg>) async {
do {
let r = try await testSMPServer(smpServer: server.wrappedValue.server)
await MainActor.run {
switch r {
case .success: server.wrappedValue.tested = true
case .failure: server.wrappedValue.tested = false
}
}
} catch let error {
await MainActor.run {
server.wrappedValue.tested = false
}
}
}
struct SMPServerView_Previews: PreviewProvider {
static var previews: some View {
SMPServerView(server: ServerCfg.sampleData.custom)
}
}
@@ -0,0 +1,106 @@
//
// SMPServersView.swift
// SimpleX (iOS)
//
// Created by Evgeny on 15/11/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct SMPServersView: View {
@Environment(\.editMode) var editMode
@State var servers: [ServerCfg] = [
ServerCfg.sampleData.preset,
ServerCfg.sampleData.custom,
ServerCfg.sampleData.untested,
]
@State var showAddServer = false
@State var showSaveAlert = false
var body: some View {
List {
Section("SMP servers") {
ForEach(servers) { srv in
smpServerView(srv)
}
.onMove { indexSet, offset in
servers.move(fromOffsets: indexSet, toOffset: offset)
}
.onDelete { indexSet in
servers.remove(atOffsets: indexSet)
}
if isEditing {
Button("Add server…") {
showAddServer = true
}
}
}
}
.onChange(of: isEditing) { value in
if value == false {
showSaveAlert = true
}
}
.toolbar { EditButton() }
.confirmationDialog("Add server…", isPresented: $showAddServer, titleVisibility: .hidden) {
Button("Scan server QR code") {
}
Button("Add preset servers") {
}
Button("Enter server manually") {
servers.append(ServerCfg.empty)
}
}
.confirmationDialog("Save servers?", isPresented: $showSaveAlert, titleVisibility: .visible) {
Button("Test & save servers") {
for i in 0..<servers.count {
servers[i].tested = nil
}
Task {
for i in 0..<servers.count {
await testServerConnection(server: $servers[i])
}
}
}
Button("Save servers") {
}
Button("Revert changes") {
}
Button("Cancel", role: .cancel) {
editMode?.wrappedValue = .active
}
}
}
private var isEditing: Bool {
editMode?.wrappedValue.isEditing == true
}
private func smpServerView(_ srv: ServerCfg) -> some View {
NavigationLink {
SMPServerView(server: srv)
.navigationBarTitle("Server")
.navigationBarTitleDisplayMode(.large)
} label: {
let v = Text(srv.server)
HStack {
showTestStatus(server: srv)
.frame(width: 16, alignment: .center)
.padding(.trailing, 4)
if srv.enabled {
v
} else {
(v + Text(" (disabled)")).foregroundColor(.secondary)
}
}
}
}
}
struct SMPServersView_Previews: PreviewProvider {
static var previews: some View {
SMPServersView()
}
}
+28 -20
View File
@@ -53,6 +53,13 @@
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 */; };
5C93293729241CDA0090FFF9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C93293229241CD90090FFF9 /* libffi.a */; };
5C93293829241CDA0090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C93293329241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */; };
5C93293929241CDA0090FFF9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C93293429241CD90090FFF9 /* libgmpxx.a */; };
5C93293A29241CDA0090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C93293529241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */; };
5C93293B29241CDA0090FFF9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C93293629241CDA0090FFF9 /* libgmp.a */; };
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 */; };
@@ -122,11 +129,6 @@
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; };
64328569291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */; };
6432856A291CDEF200FBE5C8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328565291CDEF200FBE5C8 /* libgmpxx.a */; };
6432856B291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */; };
6432856C291CDEF200FBE5C8 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328567291CDEF200FBE5C8 /* libffi.a */; };
6432856D291CDEF200FBE5C8 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64328568291CDEF200FBE5C8 /* libgmp.a */; };
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; };
@@ -250,6 +252,13 @@
5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = "<group>"; };
5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; };
5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; };
5C93292E29239A170090FFF9 /* SMPServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServersView.swift; sourceTree = "<group>"; };
5C93293029239BED0090FFF9 /* SMPServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServerView.swift; sourceTree = "<group>"; };
5C93293229241CD90090FFF9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
5C93293329241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a"; sourceTree = "<group>"; };
5C93293429241CD90090FFF9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
5C93293529241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a"; sourceTree = "<group>"; };
5C93293629241CDA0090FFF9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = "<group>"; };
@@ -323,11 +332,6 @@
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = "<group>"; };
64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a"; sourceTree = "<group>"; };
64328565291CDEF200FBE5C8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a"; sourceTree = "<group>"; };
64328567291CDEF200FBE5C8 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
64328568291CDEF200FBE5C8 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = "<group>"; };
@@ -378,13 +382,13 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6432856A291CDEF200FBE5C8 /* libgmpxx.a in Frameworks */,
6432856D291CDEF200FBE5C8 /* libgmp.a in Frameworks */,
6432856C291CDEF200FBE5C8 /* libffi.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
5C93293B29241CDA0090FFF9 /* libgmp.a in Frameworks */,
5C93293929241CDA0090FFF9 /* libgmpxx.a in Frameworks */,
5C93293729241CDA0090FFF9 /* libffi.a in Frameworks */,
5C93293A29241CDA0090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a in Frameworks */,
5C93293829241CDA0090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
64328569291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a in Frameworks */,
6432856B291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -440,11 +444,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
64328567291CDEF200FBE5C8 /* libffi.a */,
64328568291CDEF200FBE5C8 /* libgmp.a */,
64328565291CDEF200FBE5C8 /* libgmpxx.a */,
64328566291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD-ghc8.10.7.a */,
64328564291CDEF200FBE5C8 /* libHSsimplex-chat-4.2.0-LMeXtL0v5gA4lEDweXVrvD.a */,
5C93293229241CD90090FFF9 /* libffi.a */,
5C93293629241CDA0090FFF9 /* libgmp.a */,
5C93293429241CD90090FFF9 /* libgmpxx.a */,
5C93293329241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1-ghc8.10.7.a */,
5C93293529241CD90090FFF9 /* libHSsimplex-chat-4.2.1-HrC8bBnv4qm8Sq6Eue57W1.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -585,6 +589,8 @@
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
640F50E227CF991C001E05C2 /* SMPServers.swift */,
5C93292E29239A170090FFF9 /* SMPServersView.swift */,
5C93293029239BED0090FFF9 /* SMPServerView.swift */,
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */,
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
@@ -891,12 +897,14 @@
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */,
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */,
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */,
5CB0BA9A2827FD8800B3292C /* HowItWorks.swift in Sources */,
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
5C93293129239BED0090FFF9 /* SMPServerView.swift in Sources */,
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
+124 -3
View File
@@ -48,6 +48,7 @@ public enum ChatCommand {
case apiGetGroupLink(groupId: Int64)
case getUserSMPServers
case setUserSMPServers(smpServers: [String])
case testSMPServer(smpServer: String)
case apiSetChatItemTTL(seconds: Int64?)
case apiGetChatItemTTL
case apiSetNetworkConfig(networkConfig: NetCfg)
@@ -124,8 +125,9 @@ public enum ChatCommand {
case let .apiCreateGroupLink(groupId): return "/_create link #\(groupId)"
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
case .getUserSMPServers: return "/smp_servers"
case let .setUserSMPServers(smpServers): return "/smp_servers \(smpServersStr(smpServers: smpServers))"
case .getUserSMPServers: return "/smp"
case let .setUserSMPServers(smpServers): return "/smp \(smpServersStr(smpServers: smpServers))"
case let .testSMPServer(smpServer): return "/smp test \(smpServer)"
case let .apiSetChatItemTTL(seconds): return "/_ttl \(chatItemTTLStr(seconds: seconds))"
case .apiGetChatItemTTL: return "/ttl"
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
@@ -203,6 +205,7 @@ public enum ChatCommand {
case .apiGetGroupLink: return "apiGetGroupLink"
case .getUserSMPServers: return "getUserSMPServers"
case .setUserSMPServers: return "setUserSMPServers"
case .testSMPServer: return "testSMPServer"
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
@@ -289,6 +292,7 @@ public enum ChatResponse: Decodable, Error {
case apiChats(chats: [ChatData])
case apiChat(chat: ChatData)
case userSMPServers(smpServers: [String])
case sMPTestResult(smpTestFailure: SMPTestFailure?)
case chatItemTTL(chatItemTTL: Int64?)
case networkConfig(networkConfig: NetCfg)
case contactInfo(contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
@@ -390,6 +394,7 @@ public enum ChatResponse: Decodable, Error {
case .apiChats: return "apiChats"
case .apiChat: return "apiChat"
case .userSMPServers: return "userSMPServers"
case .sMPTestResult: return "smpTestResult"
case .chatItemTTL: return "chatItemTTL"
case .networkConfig: return "networkConfig"
case .contactInfo: return "contactInfo"
@@ -491,6 +496,7 @@ public enum ChatResponse: Decodable, Error {
case let .apiChats(chats): return String(describing: chats)
case let .apiChat(chat): return String(describing: chat)
case let .userSMPServers(smpServers): return String(describing: smpServers)
case let .sMPTestResult(smpTestFailure): return String(describing: smpTestFailure)
case let .chatItemTTL(chatItemTTL): return String(describing: chatItemTTL)
case let .networkConfig(networkConfig): return String(describing: networkConfig)
case let .contactInfo(contact, connectionStats, customUserProfile): return "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))"
@@ -613,7 +619,7 @@ public struct ArchiveConfig: Encodable {
}
}
public struct DBEncryptionConfig: Encodable {
public struct DBEncryptionConfig: Codable {
public init(currentKey: String, newKey: String) {
self.currentKey = currentKey
self.newKey = newKey
@@ -623,6 +629,121 @@ public struct DBEncryptionConfig: Encodable {
public var newKey: String
}
public struct ServerCfg: Identifiable, Decodable {
public var server: String
public var preset: Bool
public var tested: Bool?
public var enabled: Bool
// public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive?
// Even if we don't see the use case, it's probably better to allow it in the model
// In any case, "trusted/known" servers are out of scope of this change
public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) {
self.server = server
self.preset = preset
self.tested = tested
self.enabled = enabled
}
public var id: String { server }
public static var empty = ServerCfg(server: "", preset: false, tested: false, enabled: true)
public struct SampleData {
public var preset: ServerCfg
public var custom: ServerCfg
public var untested: ServerCfg
}
public static var sampleData = SampleData(
preset: ServerCfg(
server: "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
preset: true,
tested: true,
enabled: true
),
custom: ServerCfg(
server: "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
preset: false,
tested: false,
enabled: false
),
untested: ServerCfg(
server: "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
preset: false,
tested: nil,
enabled: true
)
)
}
public enum SMPTestStep: String, Decodable {
case connect
case createQueue
case secureQueue
case deleteQueue
case disconnect
var text: String {
switch self {
case .connect: return NSLocalizedString("Connect", 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")
}
}
}
public struct SMPTestFailure: Decodable, Error {
var testStep: SMPTestStep
var testError: AgentErrorType
var localizedDescription: String {
let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@", comment: "server test failure"), testStep.text)
switch testError {
case .SMP(.AUTH):
return err + "," + NSLocalizedString("Server requires authentication to create queues, 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:
return err
}
}
}
public struct ServerAddress {
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 = "") {
self.hostnames = hostnames
self.port = port
self.keyHash = keyHash
self.basicAuth = basicAuth
}
public var uri: String {
"smp://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))"
}
static public var empty = ServerAddress(
hostnames: [],
port: "",
keyHash: "",
basicAuth: ""
)
static public var sampleData = ServerAddress(
hostnames: ["smp.simplex.im", "1234.onion"],
port: "",
keyHash: "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=",
basicAuth: "server_password"
)
}
public struct NetCfg: Codable, Equatable {
public var socksProxy: String? = nil
public var hostMode: HostMode = .publicHost
+1 -1
View File
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package
type: git
location: https://github.com/simplex-chat/simplexmq.git
tag: f3b6ed4db024c946e6e5d119a07386d3746ec225
tag: c2342cba057fa2333b5936a2254507b5b62e8de2
source-repository-package
type: git
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."f3b6ed4db024c946e6e5d119a07386d3746ec225" = "0hsdrk80vy65dwf7gzmlkx48dy592qfm8d8sl7fpy9nr9y0rl8q1";
"https://github.com/simplex-chat/simplexmq.git"."c2342cba057fa2333b5936a2254507b5b62e8de2" = "0fsi4lgq5x3dgy79g85s7isg3387ppwrqm4v8dndixlxn8cx3pyp";
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
+1
View File
@@ -62,6 +62,7 @@ library
Simplex.Chat.Migrations.M20221025_chat_settings
Simplex.Chat.Migrations.M20221029_group_link_id
Simplex.Chat.Migrations.M20221112_server_password
Simplex.Chat.Migrations.M20221115_server_cfg
Simplex.Chat.Mobile
Simplex.Chat.Options
Simplex.Chat.ProfileGenerator
+33 -17
View File
@@ -133,13 +133,14 @@ createChatDatabase filePrefix key yesToMigrations = do
newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> IO ChatController
newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agentConfig = aCfg, tbqSize, defaultServers} ChatOpts {smpServers, networkConfig, logConnections, logServerHosts} sendToast = do
let config = cfg {subscriptionEvents = logConnections, hostEvents = logServerHosts}
servers <- resolveServers defaultServers
let servers' = servers {netCfg = networkConfig}
config = cfg {subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = servers'}
sendNotification = fromMaybe (const $ pure ()) sendToast
firstTime = dbNew chatStore
activeTo <- newTVarIO ActiveNone
currentUser <- newTVarIO user
servers <- resolveServers defaultServers
smpAgent <- getSMPAgentClient aCfg {database = AgentDB agentStore} servers {netCfg = networkConfig}
smpAgent <- getSMPAgentClient aCfg {database = AgentDB agentStore} servers'
agentAsync <- newTVarIO Nothing
idsDrg <- newTVarIO =<< drgNew
inputQ <- newTBQueueIO tbqSize
@@ -157,14 +158,21 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIsAsync, expireCIs}
where
resolveServers :: InitialAgentServers -> IO InitialAgentServers
resolveServers ss@InitialAgentServers {smp = defaultSMPServers} = case nonEmpty smpServers of
Just smpServers' -> pure ss {smp = smpServers'}
resolveServers ss = case nonEmpty smpServers of
Just smpServers' -> pure ss {smp = L.map (\ServerCfg {server} -> server) smpServers'}
_ -> case user of
Just usr -> do
userSmpServers <- withTransaction chatStore (`getSMPServers` usr)
pure ss {smp = fromMaybe defaultSMPServers $ nonEmpty userSmpServers}
Just user' -> do
userSmpServers <- withTransaction chatStore (`getSMPServers` user')
pure ss {smp = activeAgentServers cfg userSmpServers}
_ -> pure ss
activeAgentServers :: ChatConfig -> [ServerCfg] -> NonEmpty SMPServerWithAuth
activeAgentServers ChatConfig {defaultServers = InitialAgentServers {smp = defaultSMPServers}} =
fromMaybe defaultSMPServers
. nonEmpty
. map (\ServerCfg {server} -> server)
. filter (\ServerCfg {enabled} -> enabled)
startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> Bool -> m (Async ())
startChatController user subConns enableExpireCIs = do
asks smpAgent >>= resumeAgentClient
@@ -669,13 +677,19 @@ processChatCommand = \case
msgTs' = systemToUTCTime . (SMP.msgTs :: SMP.NMsgMeta -> SystemTime) <$> ntfMsgMeta
connEntity <- withStore (\db -> Just <$> getConnectionEntity db user (AgentConnId ntfConnId)) `catchError` \_ -> pure Nothing
pure CRNtfMessages {connEntity, msgTs = msgTs', ntfMessages}
GetUserSMPServers -> CRUserSMPServers <$> withUser (\user -> withStore' (`getSMPServers` user))
SetUserSMPServers smpServers -> withUser $ \user -> withChatLock "setUserSMPServers" $ do
withStore $ \db -> overwriteSMPServers db user smpServers
GetUserSMPServers -> do
ChatConfig {defaultServers = InitialAgentServers {smp = defaultSMPServers}} <- asks config
withAgent $ \a -> setSMPServers a (fromMaybe defaultSMPServers (nonEmpty smpServers))
smpServers <- withUser (\user -> withStore' (`getSMPServers` user))
let smpServers' = fromMaybe (L.map toServerCfg defaultSMPServers) $ nonEmpty smpServers
pure $ CRUserSMPServers smpServers' defaultSMPServers
where
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True}
SetUserSMPServers (SMPServersConfig smpServers) -> withUser $ \user -> withChatLock "setUserSMPServers" $ do
withStore $ \db -> overwriteSMPServers db user smpServers
cfg <- asks config
withAgent $ \a -> setSMPServers a $ activeAgentServers cfg smpServers
pure CRCmdOk
TestSMPServer smpServer -> CRSMPTestResult <$> withAgent (`testSMPServerConnection` smpServer)
TestSMPServer smpServer -> CRSmpTestResult <$> withAgent (`testSMPServerConnection` smpServer)
APISetChatItemTTL newTTL_ -> withUser' $ \user ->
checkStoreNotChanged $
withChatLock "setChatItemTTL" $ do
@@ -3201,12 +3215,14 @@ chatCommandP =
"/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal),
"/_leave #" *> (APILeaveGroup <$> A.decimal),
"/_members #" *> (APIListMembers <$> A.decimal),
"/smp_servers default" $> SetUserSMPServers [],
"/smp_servers " *> (SetUserSMPServers <$> smpServersP),
-- /smp_servers is deprecated, use /smp and /_smp
"/smp_servers default" $> SetUserSMPServers (SMPServersConfig []),
"/smp_servers " *> (SetUserSMPServers . SMPServersConfig <$> smpServersP),
"/smp_servers" $> GetUserSMPServers,
"/smp default" $> SetUserSMPServers [],
"/smp default" $> SetUserSMPServers (SMPServersConfig []),
"/smp test " *> (TestSMPServer <$> strP),
"/smp " *> (SetUserSMPServers <$> smpServersP),
"/_smp " *> (SetUserSMPServers <$> jsonP),
"/smp " *> (SetUserSMPServers . SMPServersConfig <$> smpServersP),
"/smp" $> GetUserSMPServers,
"/_ttl " *> (APISetChatItemTTL <$> ciTTLDecimal),
"/ttl " *> (APISetChatItemTTL <$> ciTTL),
+25 -3
View File
@@ -23,6 +23,7 @@ import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (ord)
import Data.Int (Int64)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.String
import Data.Text (Text)
@@ -186,7 +187,7 @@ data ChatCommand
| APIDeleteGroupLink GroupId
| APIGetGroupLink GroupId
| GetUserSMPServers
| SetUserSMPServers [SMPServerWithAuth]
| SetUserSMPServers SMPServersConfig
| TestSMPServer SMPServerWithAuth
| APISetChatItemTTL (Maybe Int64)
| APIGetChatItemTTL
@@ -262,8 +263,8 @@ data ChatResponse
| CRApiChat {chat :: AChat}
| CRLastMessages {chatItems :: [AChatItem]}
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
| CRUserSMPServers {smpServers :: [SMPServerWithAuth]}
| CRSMPTestResult {smpTestFailure :: Maybe SMPTestFailure}
| CRUserSMPServers {smpServers :: NonEmpty ServerCfg, presetSMPServers :: NonEmpty SMPServerWithAuth}
| CRSmpTestResult {smpTestFailure :: Maybe SMPTestFailure}
| CRChatItemTTL {chatItemTTL :: Maybe Int64}
| CRNetworkConfig {networkConfig :: NetworkConfig}
| CRContactInfo {contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
@@ -385,6 +386,9 @@ instance ToJSON ChatResponse where
toJSON = J.genericToJSON . sumTypeJSON $ dropPrefix "CR"
toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "CR"
data SMPServersConfig = SMPServersConfig {smpServers :: [ServerCfg]}
deriving (Show, Generic, FromJSON)
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath}
deriving (Show, Generic, FromJSON)
@@ -467,6 +471,24 @@ data SwitchProgress = SwitchProgress
instance ToJSON SwitchProgress where toEncoding = J.genericToEncoding J.defaultOptions
data ParsedServerAddress = ParsedServerAddress
{ serverAddress :: Maybe ServerAddress,
parseError :: String
}
deriving (Show, Generic)
instance ToJSON ParsedServerAddress where toEncoding = J.genericToEncoding J.defaultOptions
data ServerAddress = ServerAddress
{ hostnames :: NonEmpty String,
port :: String,
keyHash :: String,
basicAuth :: String
}
deriving (Show, Generic)
instance ToJSON ServerAddress where toEncoding = J.genericToEncoding J.defaultOptions
data ChatError
= ChatError {errorType :: ChatErrorType}
| ChatErrorAgent {agentError :: AgentErrorType}
@@ -0,0 +1,19 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20221115_server_cfg where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20221115_server_cfg :: Query
m20221115_server_cfg =
[sql|
PRAGMA ignore_check_constraints=ON;
ALTER TABLE smp_servers ADD COLUMN preset INTEGER DEFAULT 0 CHECK (preset NOT NULL);
ALTER TABLE smp_servers ADD COLUMN tested INTEGER;
ALTER TABLE smp_servers ADD COLUMN enabled INTEGER DEFAULT 1 CHECK (enabled NOT NULL);
UPDATE smp_servers SET preset = 0, enabled = 1;
PRAGMA ignore_check_constraints=OFF;
|]
@@ -385,6 +385,9 @@ CREATE TABLE smp_servers(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
basic_auth TEXT,
preset INTEGER DEFAULT 0 CHECK(preset NOT NULL),
tested INTEGER,
enabled INTEGER DEFAULT 1 CHECK(enabled NOT NULL),
UNIQUE(host, port)
);
CREATE INDEX idx_messages_shared_msg_id ON messages(shared_msg_id);
+22 -1
View File
@@ -16,6 +16,7 @@ import qualified Data.ByteString.Char8 as B
import qualified Data.ByteString.Lazy.Char8 as LB
import Data.Functor (($>))
import Data.List (find)
import qualified Data.List.NonEmpty as L
import Data.Maybe (fromMaybe)
import Database.SQLite.Simple (SQLError (..))
import qualified Database.SQLite.Simple as DB
@@ -34,8 +35,10 @@ import Simplex.Chat.Types
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (yesToMigrations), createAgentStore)
import Simplex.Messaging.Agent.Store.SQLite (closeSQLiteStore)
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 (CorrId (..))
import Simplex.Messaging.Protocol (BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..), SMPServerWithAuth)
import Simplex.Messaging.Util (catchAll, safeDecodeUtf8)
import System.Timeout (timeout)
@@ -58,6 +61,8 @@ foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatCont
foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO CJSONString
foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSONString
-- | check / migrate database and initialize chat controller on success
cChatMigrateInit :: CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString
cChatMigrateInit fp key ctrl = do
@@ -107,6 +112,10 @@ cChatRecvMsgWait cc t = deRefStablePtr cc >>= (`chatRecvMsgWait` fromIntegral t)
cChatParseMarkdown :: CString -> IO CJSONString
cChatParseMarkdown s = newCAString . chatParseMarkdown =<< peekCAString s
-- | parse server address - returns ParsedServerAddress JSON
cChatParseServer :: CString -> IO CJSONString
cChatParseServer s = newCAString . chatParseServer =<< peekCAString s
mobileChatOpts :: ChatOpts
mobileChatOpts =
ChatOpts
@@ -206,6 +215,18 @@ chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc)
chatParseMarkdown :: String -> JSONString
chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 . B.pack
chatParseServer :: String -> JSONString
chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack
where
toServerAddress :: Either String SMPServerWithAuth -> ParsedServerAddress
toServerAddress = \case
Right (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}) ""
Left e -> ParsedServerAddress Nothing e
enc :: StrEncoding a => a -> String
enc = B.unpack . strEncode
data APIResponse = APIResponse {corr :: Maybe CorrId, resp :: ChatResponse}
deriving (Generic)
+7 -5
View File
@@ -16,7 +16,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Options.Applicative
import Simplex.Chat.Controller (updateStr, versionStr)
import Simplex.Messaging.Agent.Protocol (SMPServerWithAuth)
import Simplex.Chat.Types (ServerCfg (..))
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (parseAll)
@@ -26,7 +26,7 @@ import System.FilePath (combine)
data ChatOpts = ChatOpts
{ dbFilePrefix :: String,
dbKey :: String,
smpServers :: [SMPServerWithAuth],
smpServers :: [ServerCfg],
networkConfig :: NetworkConfig,
logConnections :: Bool,
logServerHosts :: Bool,
@@ -155,7 +155,7 @@ fullNetworkConfig socksProxy tcpTimeout =
let tcpConnectTimeout = (tcpTimeout * 3) `div` 2
in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout}
parseSMPServers :: ReadM [SMPServerWithAuth]
parseSMPServers :: ReadM [ServerCfg]
parseSMPServers = eitherReader $ parseAll smpServersP . B.pack
parseSocksProxy :: ReadM (Maybe SocksProxy)
@@ -167,8 +167,10 @@ 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 ';'
smpServersP :: A.Parser [ServerCfg]
smpServersP = (toServerCfg <$> strP) `A.sepBy1` A.char ';'
where
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
getChatOpts :: FilePath -> FilePath -> IO ChatOpts
getChatOpts appDir defaultDbFileName =
+18 -13
View File
@@ -297,6 +297,7 @@ import Simplex.Chat.Migrations.M20221024_contact_used
import Simplex.Chat.Migrations.M20221025_chat_settings
import Simplex.Chat.Migrations.M20221029_group_link_id
import Simplex.Chat.Migrations.M20221112_server_password
import Simplex.Chat.Migrations.M20221115_server_cfg
import Simplex.Chat.Protocol
import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, InvitationId, MsgMeta (..))
@@ -304,7 +305,7 @@ import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore (..), createSQLiteStore
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), SMPServerWithAuth, pattern SMPServer)
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), pattern SMPServer)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
import UnliftIO.STM
@@ -344,7 +345,8 @@ schemaMigrations =
("20221024_contact_used", m20221024_contact_used),
("20221025_chat_settings", m20221025_chat_settings),
("20221029_group_link_id", m20221029_group_link_id),
("20221112_server_password", m20221112_server_password)
("20221112_server_password", m20221112_server_password),
("20221115_server_cfg", m20221115_server_cfg)
]
-- | The list of migrations in ascending order by date
@@ -4257,35 +4259,38 @@ 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) :. fileRow) :. memberRow_ :. quoteRow :. quotedMemberRow_)
toGroupChatItemList _ _ _ _ = []
getSMPServers :: DB.Connection -> User -> IO [SMPServerWithAuth]
getSMPServers :: DB.Connection -> User -> IO [ServerCfg]
getSMPServers db User {userId} =
map toSmpServer
map toServerCfg
<$> DB.query
db
[sql|
SELECT host, port, key_hash, basic_auth
SELECT host, port, key_hash, basic_auth, preset, tested, enabled
FROM smp_servers
WHERE user_id = ?;
|]
(Only userId)
where
toSmpServer :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text) -> SMPServerWithAuth
toSmpServer (host, port, keyHash, auth_) = ProtoServerWithAuth (SMPServer host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg
toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) =
let server = ProtoServerWithAuth (SMPServer host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
in ServerCfg {server, preset, tested, enabled}
overwriteSMPServers :: DB.Connection -> User -> [SMPServerWithAuth] -> ExceptT StoreError IO ()
overwriteSMPServers db User {userId} smpServers =
overwriteSMPServers :: DB.Connection -> User -> [ServerCfg] -> ExceptT StoreError IO ()
overwriteSMPServers db User {userId} servers =
checkConstraint SEUniqueID . ExceptT $ do
currentTs <- getCurrentTime
DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId)
forM_ smpServers $ \(ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_) ->
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, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?)
(host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?)
|]
(host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, userId, currentTs, currentTs)
(host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, preset, tested, enabled, userId, currentTs, currentTs)
pure $ Right ()
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
+16
View File
@@ -44,6 +44,7 @@ import GHC.Generics (Generic)
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON)
import Simplex.Messaging.Protocol (SMPServerWithAuth)
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
class IsContact a where
@@ -1449,3 +1450,18 @@ encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode
decodeJSON :: FromJSON a => Text -> Maybe a
decodeJSON = J.decode . LB.fromStrict . encodeUtf8
data ServerCfg = ServerCfg
{ server :: SMPServerWithAuth,
preset :: Bool,
tested :: Maybe Bool,
enabled :: Bool
}
deriving (Show, Generic)
instance ToJSON ServerCfg where
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
instance FromJSON ServerCfg where
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
+6 -5
View File
@@ -18,6 +18,7 @@ import Data.Char (toUpper)
import Data.Function (on)
import Data.Int (Int64)
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
import qualified Data.List.NonEmpty as L
import Data.Maybe (isJust, isNothing, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as T
@@ -65,8 +66,8 @@ responseToView user_ testView ts = \case
CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats]
CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat]
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
CRUserSMPServers smpServers -> viewSMPServers smpServers testView
CRSMPTestResult testFailure -> viewSMPTestResult testFailure
CRUserSMPServers smpServers _ -> viewSMPServers (L.toList smpServers) testView
CRSmpTestResult testFailure -> viewSMPTestResult testFailure
CRChatItemTTL ttl -> viewChatItemTTL ttl
CRNetworkConfig cfg -> viewNetworkConfig cfg
CRContactInfo ct cStats customUserProfile -> viewContactInfo ct cStats customUserProfile
@@ -622,7 +623,7 @@ viewUserProfile Profile {displayName, fullName} =
"(the updated profile will be sent to all your contacts)"
]
viewSMPServers :: [SMPServerWithAuth] -> Bool -> [StyledString]
viewSMPServers :: [ServerCfg] -> Bool -> [StyledString]
viewSMPServers smpServers testView =
if testView
then [customSMPServers]
@@ -690,8 +691,8 @@ viewConnectionStats ConnectionStats {rcvServers, sndServers} =
["receiving messages via: " <> viewServerHosts rcvServers | not $ null rcvServers]
<> ["sending messages via: " <> viewServerHosts sndServers | not $ null sndServers]
viewServers :: [SMPServerWithAuth] -> StyledString
viewServers = plain . intercalate ", " . map (B.unpack . strEncode)
viewServers :: [ServerCfg] -> StyledString
viewServers = plain . intercalate ", " . map (B.unpack . strEncode . (\ServerCfg {server} -> server))
viewServerHosts :: [SMPServer] -> StyledString
viewServerHosts = plain . intercalate ", " . map showSMPServer
+1 -1
View File
@@ -49,7 +49,7 @@ extra-deps:
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
# - ../simplexmq
- github: simplex-chat/simplexmq
commit: f3b6ed4db024c946e6e5d119a07386d3746ec225
commit: c2342cba057fa2333b5936a2254507b5b62e8de2
# - ../direct-sqlcipher
- github: simplex-chat/direct-sqlcipher
commit: 34309410eb2069b029b8fc1872deb1e0db123294
+2 -2
View File
@@ -25,7 +25,7 @@ import Simplex.Chat.Options
import Simplex.Chat.Store
import Simplex.Chat.Terminal
import Simplex.Chat.Terminal.Output (newChatTerminal)
import Simplex.Chat.Types (Profile, User (..))
import Simplex.Chat.Types (Profile, ServerCfg (..), User (..))
import Simplex.Messaging.Agent.Env.SQLite
import Simplex.Messaging.Agent.RetryInterval
import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig)
@@ -51,7 +51,7 @@ testOpts =
{ dbFilePrefix = undefined,
dbKey = "",
-- dbKey = "this is a pass-phrase to encrypt the database",
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001"],
smpServers = [ServerCfg "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001" False Nothing True],
networkConfig = defaultNetworkConfig,
logConnections = False,
logServerHosts = False,
+9 -9
View File
@@ -2867,15 +2867,15 @@ testGetSetSMPServers :: IO ()
testGetSetSMPServers =
testChat2 aliceProfile bobProfile $
\alice _ -> do
alice #$> ("/smp_servers", id, "no custom SMP servers saved")
alice #$> ("/smp_servers smp://1234-w==@smp1.example.im", id, "ok")
alice #$> ("/smp_servers", id, "smp://1234-w==@smp1.example.im")
alice #$> ("/smp_servers smp://1234-w==:password@smp1.example.im", id, "ok")
alice #$> ("/smp_servers", id, "smp://1234-w==:password@smp1.example.im")
alice #$> ("/smp_servers smp://2345-w==@smp2.example.im;smp://3456-w==@smp3.example.im:5224", id, "ok")
alice #$> ("/smp_servers", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
alice #$> ("/smp_servers default", id, "ok")
alice #$> ("/smp_servers", id, "no custom SMP servers saved")
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
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")
alice #$> ("/smp", id, "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", id, "smp://2345-w==@smp2.example.im, smp://3456-w==@smp3.example.im:5224")
alice #$> ("/smp default", id, "ok")
alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:5001")
testTestSMPServerConnection :: IO ()
testTestSMPServerConnection =