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
This commit is contained in:
Evgeny Poberezkin 2023-04-05 21:59:12 +01:00 committed by GitHub
parent 1e280fb7e1
commit 1a3f0bed47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 347 additions and 234 deletions

View file

@ -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?

View file

@ -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) }
}

View file

@ -37,7 +37,7 @@ struct NetworkAndServers: View {
List {
Section {
NavigationLink {
SMPServersView()
ProtocolServersView(serverProtocol: .smp)
.navigationTitle("Your SMP servers")
} label: {
Text("SMP servers")

View file

@ -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
)
}
}

View file

@ -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<ServerCfg>) -> some View {
private func protocolServerView(_ server: Binding<ServerCfg>) -> 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)
}
}

View file

@ -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([]))
}
}

View file

@ -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 = "<group>"; };
5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; 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>"; };
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = "<group>"; };
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServerView.swift; sourceTree = "<group>"; };
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecPlay.swift; sourceTree = "<group>"; };
5C9329402929248A0090FFF9 /* ScanSMPServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanSMPServer.swift; sourceTree = "<group>"; };
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanProtocolServer.swift; 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>"; };
@ -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 */,

View file

@ -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

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: 511a97c5d080193a84c4db1ddbc503d2eab3441f
tag: 9f8db135537a121dc1210e3dff96b4a480666c6f
source-repository-package
type: git

View file

@ -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.*

View file

@ -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";

View file

@ -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.*

View file

@ -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

View file

@ -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}

View file

@ -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);
|]

View file

@ -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;
|]

View file

@ -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
);

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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 <srv>" <> " to test SMP server connection",
"use " <> highlight' "/smp set <srv1[,srv2,...]>" <> " 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 <srv>") <> " to test " <> pName <> " server connection",
"use " <> highlight (srvCmd <> " set <srv1[,srv2,...]>") <> " 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]

View file

@ -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

View file

@ -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,

View file

@ -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