From 88547ab704b57c222596ce9fef5c576f6f587e02 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Mon, 7 Jul 2025 11:01:03 +0100 Subject: [PATCH] core: use longer timeouts for background network requests, support retries with increased timeouts (#6043) * core: use longer timeouts for background network requests, support retries with increased timeouts * ios: update types * ios: allow user retry actions with failed network requests * build: add exporting API functions with retry * android, desktop: update types and C APIs * android, desktop: alert to retry action on network error * simplexmq --- apps/ios/Shared/Model/SimpleXAPI.swift | 229 +++++++++++----- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 5 +- .../Views/Chat/ChatItem/CILinkView.swift | 6 +- .../ContextPendingMemberActionsView.swift | 5 +- .../Chat/Group/GroupMemberInfoView.swift | 5 +- .../Views/ChatList/ChatListNavLink.swift | 21 +- .../Shared/Views/ChatList/TagListView.swift | 5 +- .../Views/Helpers/CustomTimePicker.swift | 23 ++ .../Shared/Views/NewChat/NewChatView.swift | 4 +- .../Onboarding/CreateSimpleXAddress.swift | 7 +- apps/ios/Shared/Views/TerminalView.swift | 2 +- .../AdvancedNetworkSettings.swift | 25 +- .../Views/UserSettings/UserAddressView.swift | 15 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +- apps/ios/SimpleXChat/API.swift | 8 +- apps/ios/SimpleXChat/APITypes.swift | 17 +- apps/ios/SimpleXChat/AppGroup.swift | 30 +- apps/ios/SimpleXChat/ErrorAlert.swift | 9 +- apps/ios/SimpleXChat/SimpleX.h | 2 +- .../src/commonMain/cpp/android/simplex-api.c | 12 +- .../src/commonMain/cpp/desktop/simplex-api.c | 12 +- .../chat/simplex/common/model/SimpleXAPI.kt | 204 +++++++++++--- .../chat/simplex/common/platform/Core.kt | 4 +- .../AdvancedNetworkSettings.kt | 60 ++-- .../commonMain/resources/MR/base/strings.xml | 4 + apps/simplex-chat/Server.hs | 3 +- cabal.project | 2 +- flake.nix | 4 + libsimplex.dll.def | 2 + scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat/Core.hs | 6 +- src/Simplex/Chat/Library/Commands.hs | 257 +++++++++--------- src/Simplex/Chat/Library/Internal.hs | 8 +- src/Simplex/Chat/Mobile.hs | 34 ++- src/Simplex/Chat/Remote.hs | 28 +- src/Simplex/Chat/Remote/Protocol.hs | 8 +- src/Simplex/Chat/Terminal/Input.hs | 5 +- src/Simplex/Chat/Terminal/Output.hs | 7 +- tests/ChatClient.hs | 2 +- tests/ChatTests/Direct.hs | 3 +- tests/Test.hs | 2 +- 41 files changed, 695 insertions(+), 408 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 6f1d70dc2c..5e926a9392 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -94,14 +94,14 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, return try apiResult(res) } -func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) -> APIResult { +func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) -> APIResult { if log { logger.debug("chatSendCmd \(cmd.cmdType)") } let start = Date.now let resp: APIResult = bgTask - ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } - : sendSimpleXCmd(cmd, ctrl) + ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) } + : sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) if log { logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") if case let .invalid(_, json) = resp { @@ -120,10 +120,98 @@ func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDe return try apiResult(res) } +func chatApiSendCmdWithRetry(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, retryNum: Int32 = 0) async -> APIResult? { + let r: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum) + if case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) { + return await withCheckedContinuation { cont in + showRetryAlert( + alert, + onCancel: { _ in + cont.resume(returning: nil) + }, + onRetry: { + let r1: APIResult? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum + 1) + cont.resume(returning: r1) + } + ) + } + } else { + return r + } +} + @inline(__always) -func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async -> APIResult { +func showRetryAlert(_ alert: (title: String, message: String), onCancel: @escaping (UIAlertAction) -> Void, onRetry: @escaping () async -> Void) { + DispatchQueue.main.async { + showAlert( + alert.title, + message: alert.message, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .cancel, + handler: onCancel + ), + UIAlertAction( + title: NSLocalizedString("Retry", comment: "alert action"), + style: .default, + handler: { _ in Task(operation: onRetry) } + ) + ]} + ) + } +} + +func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String)? { + switch e { + case let .errorAgent(.BROKER(addr, .TIMEOUT)): ( + title: NSLocalizedString("Connection timeout", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.BROKER(addr, .NETWORK)): ( + title: NSLocalizedString("Connection error", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.TIMEOUT)))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK)))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.TIMEOUT))))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK))))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.NO_SESSION)))): ( + title: NSLocalizedString("No private routing session", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + default: nil + } +} + +func serverErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: "alert message"), serverHostname(addr)) +} + +func proxyErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: "alert message"), serverHostname(addr)) +} + +func proxyDestinationErrorAlertMessage(proxyServer: String, destServer: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Forwarding server %1$@ failed to connect to destination server %2$@. Please try later.", comment: "alert message"), serverHostname(proxyServer), serverHostname(destServer)) +} + +@inline(__always) +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in - cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)) + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, retryNum: retryNum, log: log)) } } @@ -795,16 +883,16 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws - throw r.unexpected } -func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r: ChatResponse0 = try await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r.unexpected +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiContactQueueInfo(contactId: contactId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } -func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r.unexpected +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { @@ -874,9 +962,9 @@ func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactCo logger.error("apiAddContact: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } - let alert = connectionErrorAlert(r) + let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } return (nil, alert) } @@ -886,10 +974,10 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P throw r.unexpected } -func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection { - let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) - if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} - throw r.unexpected +func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiChangeConnectionUser(connId: connId, userId: userId)) + if case let .result(.connectionUserChanged(_, _, toConnection, _)) = r {return toConnection} + if let r { throw r.unexpected } else { return nil } } func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { @@ -897,9 +985,10 @@ func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan logger.error("apiConnectPlan: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink)) if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } - return (nil, apiConnectResponseAlert(r)) + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) } func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { @@ -917,7 +1006,7 @@ func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqT logger.error("apiConnect: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { case let .result(.sentConfirmation(_, connection)): @@ -932,7 +1021,8 @@ func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqT return (nil, alert) default: () } - return (nil, apiConnectResponseAlert(r)) + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) } private func apiConnectResponseAlert(_ r: APIResult) -> Alert { @@ -1026,16 +1116,16 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws - } func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { - let r: APIResult = await chatApiSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } return nil } func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> GroupInfo? { - let r: APIResult = await chatApiSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToGroup(_, groupInfo)) = r { return groupInfo } - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } return nil } @@ -1044,11 +1134,14 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co logger.error("apiConnectContactViaAddress: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } - logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") - let alert = connectionErrorAlert(r) - return (nil, alert) + if let r { + logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") + return (nil, connectionErrorAlert(r)) + } else { + return (nil, nil) + } } func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { @@ -1214,18 +1307,18 @@ func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bo } -func apiCreateUserAddress() async throws -> CreatedConnLink { +func apiCreateUserAddress() async throws -> CreatedConnLink? { let userId = try currentUserId("apiCreateUserAddress") - let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId)) - if case let .userContactLinkCreated(_, connLink) = r { return connLink } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateMyAddress(userId: userId)) + if case let .result(.userContactLinkCreated(_, connLink)) = r { return connLink } + if let r { throw r.unexpected } else { return nil } } func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") - let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMyAddress(userId: userId)) - if case let .userContactLinkDeleted(user) = r { return user } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteMyAddress(userId: userId)) + if case let .result(.userContactLinkDeleted(user)) = r { return user } + if let r { throw r.unexpected } else { return nil } } func apiGetUserAddress() throws -> UserContactLink? { @@ -1246,25 +1339,25 @@ private func userAddressResponse(_ r: APIResult) throws -> UserCo } } -func apiAddMyAddressShortLink() async throws -> UserContactLink { +func apiAddMyAddressShortLink() async throws -> UserContactLink? { let userId = try currentUserId("apiAddMyAddressShortLink") - let r: ChatResponse1 = try await chatSendCmd(.apiAddMyAddressShortLink(userId: userId)) - if case let .userContactLink(_, contactLink) = r { return contactLink } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddMyAddressShortLink(userId: userId)) + if case let .result(.userContactLink(_, contactLink)) = r { return contactLink } + if let r { throw r.unexpected } else { return nil } } func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> UserContactLink? { let userId = try currentUserId("apiSetUserAddressSettings") - let r: APIResult = await chatApiSendCmd(.apiSetAddressSettings(userId: userId, addressSettings: settings)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiSetAddressSettings(userId: userId, addressSettings: settings)) switch r { case let .result(.userContactLinkUpdated(_, contactLink)): return contactLink case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r.unexpected + default: if let r { throw r.unexpected } else { return nil } } } func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { - let r: APIResult = await chatApiSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) let am = AlertManager.shared if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } @@ -1273,14 +1366,16 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." ) - } else if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) - } else { - logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r.unexpected))" - ) + } else if let r { + if let networkErrorAlert = networkErrorAlert(r) { + am.showAlert(networkErrorAlert) + } else { + logger.error("apiAcceptContactRequest error: \(String(describing: r))") + am.showAlertMsg( + title: "Error accepting contact request", + message: "Error: \(responseError(r.unexpected))" + ) + } } return nil } @@ -1693,13 +1788,13 @@ enum JoinGroupResult { case groupNotFound } -func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { - let r: APIResult = await chatApiSendCmd(.apiJoinGroup(groupId: groupId)) +func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiJoinGroup(groupId: groupId)) switch r { case let .result(.userAcceptedGroupSent(_, groupInfo, _)): return .joined(groupInfo: groupInfo) case .error(.errorAgent(.SMP(_, .AUTH))): return .invitationRemoved case .error(.errorStore(.groupNotFound)): return .groupNotFound - default: throw r.unexpected + default: if let r { throw r.unexpected } else { return nil } } } @@ -1769,10 +1864,10 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r.unexpected } -func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink { - let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) - if case let .groupLinkCreated(_, _, groupLink) = r { return groupLink } - throw r.unexpected +func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) + if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } + if let r { throw r.unexpected } else { return nil } } func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink { @@ -1782,9 +1877,9 @@ func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .mem } func apiDeleteGroupLink(_ groupId: Int64) async throws { - let r: ChatResponse2 = try await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) - if case .groupLinkDeleted = r { return } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteGroupLink(groupId: groupId)) + if case .result(.groupLinkDeleted) = r { return } + if let r { throw r.unexpected } } func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? { @@ -1798,10 +1893,10 @@ func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? { } } -func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink { - let r: ChatResponse2 = try await chatSendCmd(.apiAddGroupShortLink(groupId: groupId)) - if case let .groupLink(_, _, groupLink) = r { return groupLink } - throw r.unexpected +func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddGroupShortLink(groupId: groupId)) + if case let .result(.groupLink(_, _, groupLink)) = r { return groupLink } + if let r { throw r.unexpected } else { return nil } } func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index dee034b5a1..c8cb131e2e 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -274,8 +274,9 @@ struct ChatInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiContactQueueInfo(chat.chatInfo.apiId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index f9dbaede63..02cdad715e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -49,11 +49,7 @@ func openBrowserAlert(uri: URL) { NSLocalizedString("Open link?", comment: "alert title"), message: uri.absoluteString, actions: {[ - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, - handler: { _ in } - ), + cancelAlertAction, UIAlertAction( title: NSLocalizedString("Open", comment: "alert action"), style: .default, diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift index 21fa55f493..143bf42ea4 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift @@ -72,10 +72,7 @@ func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismis acceptMember(groupInfo, member, .observer, dismiss: dismiss) } ), - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default - ) + cancelAlertAction ]} ) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 5b0d21a6d8..275b8dd357 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -197,8 +197,9 @@ struct GroupMemberInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index b43e9889da..fc151d4889 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -702,16 +702,17 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { Task { logger.debug("joinGroup") do { - let r = try await apiJoinGroup(groupId) - switch r { - case let .joined(groupInfo): - await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } - case .invitationRemoved: - AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") - await deleteGroup() - case .groupNotFound: - AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") - await deleteGroup() + if let r = try await apiJoinGroup(groupId) { + switch r { + case let .joined(groupInfo): + await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } + case .invitationRemoved: + AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") + await deleteGroup() + case .groupNotFound: + AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") + await deleteGroup() + } } await onComplete() } catch let error { diff --git a/apps/ios/Shared/Views/ChatList/TagListView.swift b/apps/ios/Shared/Views/ChatList/TagListView.swift index 2063fe15de..79d122eabf 100644 --- a/apps/ios/Shared/Views/ChatList/TagListView.swift +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -63,10 +63,7 @@ struct TagListView: View { NSLocalizedString("Delete list?", comment: "alert title"), message: String.localizedStringWithFormat(NSLocalizedString("All chats will be removed from the list %@, and the list deleted.", comment: "alert message"), text), actions: {[ - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default - ), + cancelAlertAction, UIAlertAction( title: NSLocalizedString("Delete", comment: "alert action"), style: .destructive, diff --git a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift index 09aae1cb15..32fc541604 100644 --- a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift +++ b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift @@ -220,6 +220,29 @@ struct DropdownCustomTimePicker: View { } } +struct WrappedPicker: View { + var title: LocalizedStringKey + var selection: Binding + @ViewBuilder var content: () -> Content + + init(_ title: LocalizedStringKey, selection: Binding, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.selection = selection + self.content = content + } + + var body: some View { + HStack(alignment: .firstTextBaseline) { + Text(title).lineLimit(2) + Spacer() + Picker(selection: selection, content: content) { + EmptyView() + } + .frame(height: 36) + } + } +} + struct CustomTimePicker_Previews: PreviewProvider { static var previews: some View { CustomTimePicker( diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index c94a1a8f18..cadd551147 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -415,8 +415,8 @@ private struct ActiveProfilePicker: View { } Task { do { - if let contactConn = contactConnection { - let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) + if let contactConn = contactConnection, + let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { await MainActor.run { contactConnection = conn connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil) diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index 62eaa19071..73d8a8d370 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -77,9 +77,10 @@ struct CreateSimpleXAddress: View { progressIndicator = true Task { do { - let connLinkContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + if let connLinkContact = try await apiCreateUserAddress() { + DispatchQueue.main.async { + m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + } } await MainActor.run { progressIndicator = false } } catch let error { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 554219eb69..ac143fe044 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -167,7 +167,7 @@ struct TerminalView: View { func sendTerminalCmd(_ cmd: String) async { let start: Date = .now await withCheckedContinuation { (cont: CheckedContinuation) in - let d = sendSimpleXCmdStr(cmd) + let d = sendSimpleXCmdStr(cmd, retryNum: 0) Task { guard let d else { await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult.error(.invalidJSON(json: nil))) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift index fa698f8b7c..7bcf542701 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -67,7 +67,7 @@ struct AdvancedNetworkSettings: View { Text(netCfg.smpProxyMode.label) } } - + NavigationLink { List { Section { @@ -192,7 +192,7 @@ struct AdvancedNetworkSettings: View { netCfg.requiredHostMode = requiredHostMode } } - + if developerTools { Section { Picker("Transport isolation", selection: $netCfg.sessionMode) { @@ -220,10 +220,12 @@ struct AdvancedNetworkSettings: View { ? Text("Use TCP port 443 for preset servers only.") : Text("Use TCP port \(netCfg.smpWebPortServers == .all ? "443" : "5223") when no port is specified.") } - + Section("TCP connection") { - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout.interactiveTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection bg timeout", selection: $netCfg.tcpConnectTimeout.backgroundTimeout, values: [30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout.interactiveTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000], label: secondsLabel) + timeoutSettingPicker("Protocol background timeout", selection: $netCfg.tcpTimeout.backgroundTimeout, values: [15_000000, 20_000000, 30_000000, 45_000000, 60_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) // intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) @@ -243,7 +245,7 @@ struct AdvancedNetworkSettings: View { .foregroundColor(theme.colors.secondary) } } - + Section { Button("Reset to defaults") { updateNetCfgView(NetCfg.defaults, NetworkProxy.def) @@ -254,7 +256,7 @@ struct AdvancedNetworkSettings: View { updateNetCfgView(netCfg.withProxyTimeouts, netProxy) } .disabled(netCfg.hasProxyTimeouts) - + Button("Save and reconnect") { showSettingsAlert = .update } @@ -351,16 +353,15 @@ struct AdvancedNetworkSettings: View { } private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding, values: [Int], label: String) -> some View { - Picker(title, selection: selection) { + WrappedPicker(title, selection: selection) { let v = selection.wrappedValue let vs = values.contains(v) ? values : values + [v] ForEach(vs, id: \.self) { value in Text("\(String(format: "%g", (Double(value) / 1000000))) \(secondsLabel)") } } - .frame(height: 36) } - + private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { switch hosts { case .no: return "Onion hosts will not be used." @@ -378,7 +379,7 @@ struct AdvancedNetworkSettings: View { case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.") } } - + private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey { switch mode { case .always: return "Always use private routing." @@ -387,7 +388,7 @@ struct AdvancedNetworkSettings: View { case .never: return "Do NOT use private routing." } } - + private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { switch proxyFallback { case .allow: return "Send messages directly when your or destination server does not support private routing." diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 3d554fc5cc..46a9083436 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -198,11 +198,12 @@ struct UserAddressView: View { progressIndicator = true Task { do { - let connLinkContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) - alert = .shareOnCreate - progressIndicator = false + if let connLinkContact = try await apiCreateUserAddress() { + DispatchQueue.main.async { + chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + alert = .shareOnCreate + progressIndicator = false + } } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") @@ -488,7 +489,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on @@ -510,7 +511,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 1b612798d8..c02bfdcae5 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -543,8 +543,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -704,8 +704,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -790,8 +790,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 0dd3483fd7..92a2267656 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -111,8 +111,8 @@ public func resetChatCtrl() { } @inline(__always) -public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult { - if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) { +public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil, retryNum: Int32 = 0) -> APIResult { + if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl, retryNum: retryNum) { decodeAPIResult(d) } else { APIResult.error(.invalidJSON(json: nil)) @@ -120,9 +120,9 @@ public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: cha } @inline(__always) -public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil) -> Data? { +public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil, retryNum: Int32) -> Data? { var c = cmd.cString(using: .utf8)! - return if let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c) { + return if let cjson = chat_send_cmd_retry(ctrl ?? getChatCtrl(), &c, retryNum) { dataFromCString(cjson) } else { nil diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index aa0055fa70..5335a04ae3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -241,8 +241,8 @@ public struct NetCfg: Codable, Equatable { public var smpProxyMode: SMPProxyMode = .always public var smpProxyFallback: SMPProxyFallback = .allowProtected public var smpWebPortServers: SMPWebPortServers = .preset - public var tcpConnectTimeout: Int // microseconds - public var tcpTimeout: Int // microseconds + public var tcpConnectTimeout: NetworkTimeout + public var tcpTimeout: NetworkTimeout public var tcpTimeoutPerKb: Int // microseconds public var rcvConcurrency: Int // pool size public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults @@ -251,16 +251,16 @@ public struct NetCfg: Codable, Equatable { public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - tcpConnectTimeout: 25_000_000, - tcpTimeout: 15_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 45_000_000, interactiveTimeout: 15_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 30_000_000, interactiveTimeout: 10_000_000), tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, smpPingInterval: 1200_000_000 ) static let proxyDefaults: NetCfg = NetCfg( - tcpConnectTimeout: 35_000_000, - tcpTimeout: 20_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 60_000_000, interactiveTimeout: 30_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 40_000_000, interactiveTimeout: 20_000_000), tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, smpPingInterval: 1200_000_000 @@ -287,6 +287,11 @@ public struct NetCfg: Codable, Equatable { public var enableKeepAlive: Bool { tcpKeepAlive != nil } } +public struct NetworkTimeout: Codable, Equatable { + public var backgroundTimeout: Int // microseconds + public var interactiveTimeout: Int // microseconds +} + public enum HostMode: String, Codable { case onionViaSocks case onionHost = "onion" diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 29ccab7357..5eed01a2a2 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -41,8 +41,12 @@ let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" let GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS = "networkSMPWebPortServers" -let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" -let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" +//let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" +//let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" +let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND = "networkTCPConnectTimeoutBackground" +let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "networkTCPConnectTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND = "networkTCPTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE = "networkTCPTimeoutBackground" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" @@ -73,8 +77,10 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.unknown.rawValue, GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allowProtected.rawValue, GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue, - GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, - GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpTimeout.interactiveTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, @@ -349,8 +355,14 @@ public func getNetCfg() -> NetCfg { let smpProxyMode = networkSMPProxyModeGroupDefault.get() let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() let smpWebPortServers = networkSMPWebPortServersDefault.get() - let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + let tcpConnectTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + ) + let tcpTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) + ) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) @@ -392,8 +404,10 @@ public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) { let socksProxy = networkProxy?.toProxyString() groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) networkSMPWebPortServersDefault.set(cfg.smpWebPortServers) - groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + groupDefaults.set(cfg.tcpConnectTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpConnectTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + groupDefaults.set(cfg.tcpTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index a433d2313b..2f2dd31a83 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -11,7 +11,6 @@ import SwiftUI public struct ErrorAlert: Error { public let title: LocalizedStringKey public let message: LocalizedStringKey? - public let actions: Optional<() -> AnyView> public init( title: LocalizedStringKey, @@ -19,7 +18,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = nil } public init( @@ -29,7 +27,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = { AnyView(actions()) } } public init(_ title: LocalizedStringKey) { @@ -75,11 +72,7 @@ extension View { set: { if !$0 { errorAlert.wrappedValue = nil } } ), actions: { - if let actions_ = errorAlert.wrappedValue?.actions { - actions_() - } else { - if let alert = errorAlert.wrappedValue { actions(alert) } - } + if let alert = errorAlert.wrappedValue { actions(alert) } }, message: { if let message = errorAlert.wrappedValue?.message { diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 92dfafca21..8a443017e1 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -20,7 +20,7 @@ typedef void* chat_ctrl; extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, int backgroundMode, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctl); extern char *chat_reopen_store(chat_ctrl ctl); -extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index b9b5277aeb..d8fa2c65a7 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -58,8 +58,8 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); @@ -102,20 +102,20 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jc } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); //jint length = (jint) (*env)->GetStringUTFLength(env, msg); //for (int i = 0; i < length; ++i) // __android_log_print(ANDROID_LOG_ERROR, "simplex", "%d: %02x\n", i, _msg[i]); - jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 5c921c400d..adb29e0cc2 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -31,8 +31,8 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); @@ -115,17 +115,17 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass claz } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_cmd((void*)controller, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 394d598675..53f58bba0b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -149,8 +149,10 @@ class AppPreferences { val networkHostMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.default) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkSMPWebPortServers: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS, SMPWebPortServers.default) - val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) - val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) + val networkTCPConnectTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.backgroundTimeout) + val networkTCPConnectTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.interactiveTimeout) + val networkTCPTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpTimeout.backgroundTimeout) + val networkTCPTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpTimeout.interactiveTimeout) val networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) val networkRcvConcurrency = mkIntPreference(SHARED_PREFS_NETWORK_RCV_CONCURRENCY, NetCfg.defaults.rcvConcurrency) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) @@ -271,13 +273,14 @@ class AppPreferences { set = fun(value) = settings.putFloat(prefName, value) ) - private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference { - val d = if (networkUseSocksProxy.get()) proxyDefault else default - return SharedPreference( - get = fun() = settings.getLong(prefName, d), + private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference = + SharedPreference( + get = { + val d = if (networkUseSocksProxy.get()) proxyDefault else default + settings.getLong(prefName, d) + }, set = fun(value) = settings.putLong(prefName, value) ) - } private fun mkBoolPreference(prefName: String, default: Boolean) = SharedPreference( @@ -402,8 +405,12 @@ class AppPreferences { private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS = "NetworkSMPWebPortServers" - private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" - private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" +// private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" +// private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND = "NetworkTCPConnectTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "NetworkTCPConnectTimeoutInteractive" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND = "NetworkTCPTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE = "NetworkTCPTimeoutInteractive" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" private const val SHARED_PREFS_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" @@ -666,7 +673,87 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): API { + private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, retryNum: Int = 0): API? { + val r = sendCmd(rhId, cmd, retryNum = retryNum) + val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null + if (alert == null) return r + else return suspendCancellableCoroutine { cont -> + showRetryAlert( + alert, + onCancel = { + cont.resumeWith(Result.success(null)) + }, + onRetry = { + withLongRunningApi { + cont.resumeWith( + runCatching { + coroutineScope { + sendCmdWithRetry(rhId, cmd, retryNum = retryNum + 1) + } + } + ) + } + } + ) + + cont.invokeOnCancellation { + cont.resumeWith(Result.success(null)) + } + } + } + + private fun showRetryAlert(alert: Pair, onCancel: () -> Unit, onRetry: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(alert.first), + text = alert.second, + confirmText = generalGetString(MR.strings.retry_verb), + onConfirm = onRetry, + onDismiss = onCancel, + onDismissRequest = onCancel + ) + } + + private fun retryableNetworkErrorAlert(err: ChatError): Pair? { + if (err !is ChatError.ChatErrorAgent) return null + val e = err.agentError + when (e) { + is AgentErrorType.BROKER -> { + val message = { String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.brokerAddress)) } + return when (e.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.connection_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.connection_error to message() + else -> null + } + } + is AgentErrorType.SMP -> + if (e.smpErr is SMPErrorType.PROXY && e.smpErr.proxyErr is ProxyError.BROKER) { + val message = { String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(e.serverAddress)) } + return when (e.smpErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + else -> null + } + } + is AgentErrorType.PROXY -> + if (e.proxyErr is ProxyClientError.ProxyProtocolError && e.proxyErr.protocolErr is SMPErrorType.PROXY) { + val message = { String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(e.proxyServer), serverHostname(e.relayServer)) } + return when (e.proxyErr.protocolErr.proxyErr) { + is ProxyError.BROKER -> + when (e.proxyErr.protocolErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + else -> null + } + is ProxyError.NO_SESSION -> MR.strings.private_routing_no_session to message() + else -> null + } + } + else -> return null + } + return null + } + + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API { val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { @@ -675,7 +762,7 @@ object ChatController { chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) Log.d(TAG, "sendCmd: ${cmd.cmdType}") } - val rStr = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) + val rStr = if (rhId == null) chatSendCmdRetry(ctrl, c, retryNum) else chatSendRemoteCmdRetry(ctrl, rhId.toInt(), c, retryNum) // coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups) interruptIfCancelled() val r = json.decodeFromString(rStr) @@ -1208,16 +1295,16 @@ object ChatController { } suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair? { - val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) + val r = sendCmdWithRetry(rh, CC.APIContactQueueInfo(contactId)) if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo - apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + if (r != null) apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) return null } suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { - val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + val r = sendCmdWithRetry(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo - apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + if (r != null) apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) return null } @@ -1293,9 +1380,10 @@ object ChatController { suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> { val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } - val r = sendCmd(rh, CC.APIAddContact(userId, incognito = incognito)) + val r = sendCmdWithRetry(rh, CC.APIAddContact(userId, incognito = incognito)) return when { r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null + r == null -> null to null !(networkErrorAlert(r)) -> null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } else -> null to null } @@ -1311,8 +1399,9 @@ object ChatController { } suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { - val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId)) + val r = sendCmdWithRetry(rh, CC.ApiChangeConnectionUser(connId, userId)) if (r is API.Result && r.res is CR.ConnectionUserChanged) return r.res.toConnection + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) } @@ -1321,15 +1410,15 @@ object ChatController { suspend fun apiConnectPlan(rh: Long?, connLink: String): Pair? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIConnectPlan(userId, connLink)) + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink)) if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan - apiConnectResponseAlert(r) + if (r != null) apiConnectResponseAlert(r) return null } suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? { val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIConnect(userId, incognito, connLink)) + val r = sendCmdWithRetry(rh, CC.APIConnect(userId, incognito, connLink)) when { r is API.Result && r.res is CR.SentConfirmation -> return r.res.connection r is API.Result && r.res is CR.SentInvitation -> return r.res.connection @@ -1338,7 +1427,7 @@ object ChatController { generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.res.contact.displayName) ) - else -> apiConnectResponseAlert(r) + r != null -> apiConnectResponseAlert(r) } return null } @@ -1426,8 +1515,9 @@ object ChatController { } suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent?): Contact? { - val r = sendCmd(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact + if (r == null) return null Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}") if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectPreparedContact", generalGetString(MR.strings.connection_error), r) @@ -1436,8 +1526,9 @@ object ChatController { } suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? { - val r = sendCmd(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg)) + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg)) if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo + if (r == null) return null Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}") if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectPreparedGroup", generalGetString(MR.strings.connection_error), r) @@ -1447,8 +1538,9 @@ object ChatController { suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) + val r = sendCmdWithRetry(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) if (r is API.Result && r.res is CR.SentInvitationToContact) return r.res.contact + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) } @@ -1592,8 +1684,9 @@ object ChatController { suspend fun apiCreateUserAddress(rh: Long?): CreatedConnLink? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) + val r = sendCmdWithRetry(rh, CC.ApiCreateMyAddress(userId)) if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) } @@ -1602,8 +1695,9 @@ object ChatController { suspend fun apiDeleteUserAddress(rh: Long?): User? { val userId = try { currentUserId("apiDeleteUserAddress") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.ApiDeleteMyAddress(userId)) + val r = sendCmdWithRetry(rh, CC.ApiDeleteMyAddress(userId)) if (r is API.Result && r.res is CR.UserContactLinkDeleted) return r.res.user.updateRemoteHostId(rh) + if (r == null) return null Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return null } @@ -1623,8 +1717,9 @@ object ChatController { suspend fun apiAddMyAddressShortLink(rh: Long?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiAddMyAddressShortLink") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiAddMyAddressShortLink(userId)) + val r = sendCmdWithRetry(rh, CC.ApiAddMyAddressShortLink(userId)) if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddMyAddressShortLink", generalGetString(MR.strings.error_creating_address), r) } @@ -1633,19 +1728,20 @@ object ChatController { suspend fun apiSetUserAddressSettings(rh: Long?, settings: AddressSettings): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiSetUserAddressSettings") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiSetAddressSettings(userId, settings)) + val r = sendCmdWithRetry(rh, CC.ApiSetAddressSettings(userId, settings)) if (r is API.Result && r.res is CR.UserContactLinkUpdated) return r.res.contactLink if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } + if (r == null) return null Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? { - val r = sendCmd(rh, CC.ApiAcceptContact(incognito, contactReqId)) + val r = sendCmdWithRetry(rh, CC.ApiAcceptContact(incognito, contactReqId)) return when { r is API.Result && r.res is CR.AcceptingContactRequest -> r.res.contact r is API.Error && r.err is ChatError.ChatErrorAgent @@ -1657,12 +1753,13 @@ object ChatController { ) null } - else -> { + r != null -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiAcceptContactRequest", generalGetString(MR.strings.error_accepting_contact_request), r) } null } + else -> null } } @@ -1944,7 +2041,7 @@ object ChatController { } suspend fun apiJoinGroup(rh: Long?, groupId: Long) { - val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) + val r = sendCmdWithRetry(rh, CC.ApiJoinGroup(groupId)) when { r is API.Result && r.res is CR.UserAcceptedGroupSent -> withContext(Dispatchers.Main) { @@ -1965,7 +2062,7 @@ object ChatController { apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } - else -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) + r != null -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } @@ -2046,8 +2143,9 @@ object ChatController { } suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { - val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole)) + val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) } @@ -2064,8 +2162,9 @@ object ChatController { } suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean { - val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId)) + val r = sendCmdWithRetry(rh, CC.APIDeleteGroupLink(groupId)) if (r is API.Result && r.res is CR.GroupLinkDeleted) return true + if (r == null) return false if (!(networkErrorAlert(r))) { apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) } @@ -2080,8 +2179,9 @@ object ChatController { } suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): GroupLink? { - val r = sendCmd(rh, CC.ApiAddGroupShortLink(groupId)) + val r = sendCmdWithRetry(rh, CC.ApiAddGroupShortLink(groupId)) if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r) } @@ -3312,8 +3412,14 @@ object ChatController { val smpProxyMode = appPrefs.networkSMPProxyMode.get() val smpProxyFallback = appPrefs.networkSMPProxyFallback.get() val smpWebPortServers = appPrefs.networkSMPWebPortServers.get() - val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() - val tcpTimeout = appPrefs.networkTCPTimeout.get() + val tcpConnectTimeout = NetworkTimeout( + backgroundTimeout = appPrefs.networkTCPConnectTimeoutBackground.get(), + interactiveTimeout = appPrefs.networkTCPConnectTimeoutInteractive.get() + ) + val tcpTimeout = NetworkTimeout( + backgroundTimeout = appPrefs.networkTCPTimeoutBackground.get(), + interactiveTimeout = appPrefs.networkTCPTimeoutInteractive.get() + ) val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() val rcvConcurrency = appPrefs.networkRcvConcurrency.get() val smpPingInterval = appPrefs.networkSMPPingInterval.get() @@ -3356,8 +3462,10 @@ object ChatController { appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode) appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback) appPrefs.networkSMPWebPortServers.set(cfg.smpWebPortServers) - appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) - appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) + appPrefs.networkTCPConnectTimeoutBackground.set(cfg.tcpConnectTimeout.backgroundTimeout) + appPrefs.networkTCPConnectTimeoutInteractive.set(cfg.tcpConnectTimeout.interactiveTimeout) + appPrefs.networkTCPTimeoutBackground.set(cfg.tcpTimeout.backgroundTimeout) + appPrefs.networkTCPTimeoutInteractive.set(cfg.tcpTimeout.interactiveTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) appPrefs.networkRcvConcurrency.set(cfg.rcvConcurrency) appPrefs.networkSMPPingInterval.set(cfg.smpPingInterval) @@ -4508,8 +4616,8 @@ data class NetCfg( val smpProxyMode: SMPProxyMode = SMPProxyMode.default, val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.default, val smpWebPortServers: SMPWebPortServers = SMPWebPortServers.default, - val tcpConnectTimeout: Long, // microseconds - val tcpTimeout: Long, // microseconds + val tcpConnectTimeout: NetworkTimeout, + val tcpTimeout: NetworkTimeout, val tcpTimeoutPerKb: Long, // microseconds val rcvConcurrency: Int, // pool size val tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults, @@ -4528,8 +4636,8 @@ data class NetCfg( val defaults: NetCfg = NetCfg( socksProxy = null, - tcpConnectTimeout = 25_000_000, - tcpTimeout = 15_000_000, + tcpConnectTimeout = NetworkTimeout(backgroundTimeout = 45_000_000, interactiveTimeout = 15_000_000), + tcpTimeout = NetworkTimeout(backgroundTimeout = 30_000_000, interactiveTimeout = 10_000_000), tcpTimeoutPerKb = 10_000, rcvConcurrency = 12, smpPingInterval = 1200_000_000 @@ -4538,8 +4646,8 @@ data class NetCfg( val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - tcpConnectTimeout = 35_000_000, - tcpTimeout = 20_000_000, + tcpConnectTimeout = NetworkTimeout(backgroundTimeout = 60_000_000, interactiveTimeout = 30_000_000), + tcpTimeout = NetworkTimeout(backgroundTimeout = 40_000_000, interactiveTimeout = 20_000_000), tcpTimeoutPerKb = 15_000, rcvConcurrency = 8, smpPingInterval = 1200_000_000 @@ -4563,6 +4671,12 @@ data class NetCfg( } } +@Serializable +data class NetworkTimeout( + val backgroundTimeout: Long, // microseconds + val interactiveTimeout: Long // microseconds +) + @Serializable data class NetworkProxy( val username: String = "", diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index a9f2dcaffc..9d8a699775 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -22,8 +22,8 @@ external fun pipeStdOutToSocket(socketName: String) : Int typealias ChatCtrl = Long external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatCloseStore(ctrl: ChatCtrl): String -external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String -external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String +external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String +external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 0c38b0c045..40d664a257 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -48,8 +48,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) } val smpWebPortServers = remember { mutableStateOf(currentCfgVal.smpWebPortServers) } - val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } - val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } + val networkTCPConnectTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.interactiveTimeout) } + val networkTCPConnectTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.backgroundTimeout) } + val networkTCPTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpTimeout.interactiveTimeout) } + val networkTCPTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpTimeout.backgroundTimeout) } val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } @@ -86,8 +88,14 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode = smpProxyMode.value, smpProxyFallback = smpProxyFallback.value, smpWebPortServers = smpWebPortServers.value, - tcpConnectTimeout = networkTCPConnectTimeout.value, - tcpTimeout = networkTCPTimeout.value, + tcpConnectTimeout = NetworkTimeout( + backgroundTimeout = networkTCPConnectTimeoutBackground.value, + interactiveTimeout = networkTCPConnectTimeoutInteractive.value + ) , + tcpTimeout = NetworkTimeout( + backgroundTimeout = networkTCPTimeoutBackground.value, + interactiveTimeout = networkTCPTimeoutInteractive.value + ), tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, rcvConcurrency = networkRcvConcurrency.value, tcpKeepAlive = tcpKeepAlive, @@ -101,8 +109,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode.value = cfg.smpProxyMode smpProxyFallback.value = cfg.smpProxyFallback smpWebPortServers.value = cfg.smpWebPortServers - networkTCPConnectTimeout.value = cfg.tcpConnectTimeout - networkTCPTimeout.value = cfg.tcpTimeout + networkTCPConnectTimeoutInteractive.value = cfg.tcpConnectTimeout.interactiveTimeout + networkTCPConnectTimeoutBackground.value = cfg.tcpConnectTimeout.backgroundTimeout + networkTCPTimeoutInteractive.value = cfg.tcpTimeout.interactiveTimeout + networkTCPTimeoutBackground.value = cfg.tcpTimeout.backgroundTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb networkRcvConcurrency.value = cfg.rcvConcurrency networkSMPPingInterval.value = cfg.smpPingInterval @@ -156,8 +166,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, smpWebPortServers, - networkTCPConnectTimeout, - networkTCPTimeout, + networkTCPConnectTimeoutInteractive, + networkTCPConnectTimeoutBackground, + networkTCPTimeoutInteractive, + networkTCPTimeoutBackground, networkTCPTimeoutPerKb, networkRcvConcurrency, networkSMPPingInterval, @@ -189,8 +201,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode: MutableState, smpProxyFallback: MutableState, smpWebPortServers: MutableState, - networkTCPConnectTimeout: MutableState, - networkTCPTimeout: MutableState, + networkTCPConnectTimeoutInteractive: MutableState, + networkTCPConnectTimeoutBackground: MutableState, + networkTCPTimeoutInteractive: MutableState, + networkTCPTimeoutBackground: MutableState, networkTCPTimeoutPerKb: MutableState, networkRcvConcurrency: MutableState, networkSMPPingInterval: MutableState, @@ -242,14 +256,26 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { SectionItemView { TimeoutSettingRow( - stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeoutInteractive, + listOf(10_000000, 15_000000, 20_000000, 30_000000), secondsLabel ) } SectionItemView { TimeoutSettingRow( - stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeout, - listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel + stringResource(MR.strings.network_option_tcp_connection_timeout_background), networkTCPConnectTimeoutBackground, + listOf(30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + ) + } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeoutInteractive, + listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000), secondsLabel + ) + } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout_background), networkTCPTimeoutBackground, + listOf(15_000000, 20_000000, 30_000000, 45_000000, 60_000_000), secondsLabel ) } SectionItemView { @@ -555,8 +581,10 @@ fun PreviewAdvancedNetworkSettingsLayout() { smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) }, - networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, - networkTCPTimeout = remember { mutableStateOf(10_000000) }, + networkTCPConnectTimeoutInteractive = remember { mutableStateOf(15_000000) }, + networkTCPConnectTimeoutBackground = remember { mutableStateOf(45_000000) }, + networkTCPTimeoutInteractive = remember { mutableStateOf(10_000000) }, + networkTCPTimeoutBackground = remember { mutableStateOf(30_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, networkRcvConcurrency = remember { mutableStateOf(8) }, networkSMPPingInterval = remember { mutableStateOf(10_000000) }, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 8f312d579e..00419c6703 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -147,7 +147,9 @@ Please check your network connection with %1$s and try again. Server address is incompatible with network settings: %1$s. Server version is incompatible with your app: %1$s. + Private routing timeout Private routing error + No private routing session Error connecting to forwarding server %1$s. Please try later. Forwarding server address is incompatible with network settings: %1$s. Forwarding server version is incompatible with network settings: %1$s. @@ -1976,7 +1978,9 @@ Reset to defaults sec TCP connection timeout + TCP connection bg timeout Protocol timeout + Protocol background timeout Protocol timeout per KB Receiving concurrency PING interval diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index 0906d14536..883c5cb473 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -13,7 +13,6 @@ module Server where import Control.Monad -import Control.Monad.Except import Control.Monad.Reader import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as J @@ -127,7 +126,7 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do where sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = CSRBody $ chatCmdError e} processCommand (corrId, cmd) = - response <$> runReaderT (runExceptT $ processChatCommand cmd) cc + response <$> runReaderT (execChatCommand' cmd 0) cc where response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r} clientDisconnected _ = pure () diff --git a/cabal.project b/cabal.project index 4def04b5dc..9d1df48cc8 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 1b8613d7679768a8b870fc2f2ccffb25328f00ea + tag: 36f05e272e76c21b69752d75552afb0fe005500f source-repository-package type: git diff --git a/flake.nix b/flake.nix index 9ee0c52020..a5c4eb2a27 100644 --- a/flake.nix +++ b/flake.nix @@ -387,7 +387,9 @@ "chat_recv_msg" "chat_recv_msg_wait" "chat_send_cmd" + "chat_send_cmd_retry" "chat_send_remote_cmd" + "chat_send_remote_cmd_retry" "chat_valid_name" "chat_json_length" "chat_write_file" @@ -492,7 +494,9 @@ "chat_recv_msg" "chat_recv_msg_wait" "chat_send_cmd" + "chat_send_cmd_retry" "chat_send_remote_cmd" + "chat_send_remote_cmd_retry" "chat_valid_name" "chat_json_length" "chat_write_file" diff --git a/libsimplex.dll.def b/libsimplex.dll.def index 592e6db4f2..2945d52e83 100644 --- a/libsimplex.dll.def +++ b/libsimplex.dll.def @@ -5,7 +5,9 @@ EXPORTS chat_migrate_init chat_close_store chat_send_cmd + chat_send_cmd_retry chat_send_remote_cmd + chat_send_remote_cmd_retry chat_recv_msg chat_recv_msg_wait chat_parse_markdown diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 51030b8a9b..508bef5c55 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."1b8613d7679768a8b870fc2f2ccffb25328f00ea" = "155vrif1yj6d1h0nc3ghs025gv9f8ssnnp6fw0kzwgv6mijixss0"; + "https://github.com/simplex-chat/simplexmq.git"."36f05e272e76c21b69752d75552afb0fe005500f" = "1piivvsjvxm955f2x06vp8sxxqhlrf8nxbjgbcp7zrdgqlnn1hyx"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 865eb6a760..08c0a30c6d 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -65,10 +65,10 @@ runSimplexChat ChatOpts {maintenance} u cc chat waitEither_ a1 a2 sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse) -sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc +sendChatCmdStr cc s = runReaderT (execChatCommand Nothing (encodeUtf8 $ T.pack s) 0) cc sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse) -sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc +sendChatCmd cc cmd = runReaderT (execChatCommand' cmd 0) cc getSelectActiveUser :: DBStore -> IO (Maybe User) getSelectActiveUser st = do @@ -108,7 +108,7 @@ createActiveUser cc = do loop = do displayName <- T.pack <$> getWithPrompt "display name" let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} - execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case + execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user r -> printResponseEvent (Nothing, Nothing) (config cc) r >> loop diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 9e38d2c96d..42499bd6a6 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -95,7 +95,7 @@ import Simplex.Messaging.Agent.Store.Interface (execSQL) import Simplex.Messaging.Agent.Store.Shared (upMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) -import Simplex.Messaging.Client (NetworkConfig (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) import Simplex.Messaging.Compression (compressionLevel) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) @@ -263,7 +263,8 @@ updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} = let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ - cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ + cfg3 = maybe cfg2 (\t -> cfg2 {tcpTimeout = nt t, tcpConnectTimeout = nt ((t * 3) `div` 2)}) tcpTimeout_ + nt t = NetworkTimeout {backgroundTimeout = t * 3, interactiveTimeout = t} in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors} useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) @@ -272,25 +273,27 @@ useServers as opDomains uss = xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss in (smp', xftp') -execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' (Either ChatError ChatResponse) -execChatCommand rh s = +execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse) +execChatCommand rh s retryNum = case parseChatCommand s of Left e -> pure $ chatCmdError e Right cmd -> case rh of Just rhId - | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s + | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s retryNum | otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand _ -> do cc@ChatController {config = ChatConfig {chatHooks}} <- ask case preCmdHook chatHooks of - Just hook -> liftIO (hook cc cmd) >>= either pure execChatCommand' - Nothing -> execChatCommand' cmd + Just hook -> liftIO (hook cc cmd) >>= either pure (`execChatCommand'` retryNum) + Nothing -> execChatCommand' cmd retryNum -execChatCommand' :: ChatCommand -> CM' (Either ChatError ChatResponse) -execChatCommand' cmd = handleCommandError $ processChatCommand cmd +execChatCommand' :: ChatCommand -> Int -> CM' (Either ChatError ChatResponse) +execChatCommand' cmd retryNum = handleCommandError $ do + vr <- chatVersionRange + processChatCommand vr (NRMInteractive' retryNum) cmd -execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> CM' (Either ChatError ChatResponse) -execRemoteCommand rhId cmd s = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s +execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> Int -> CM' (Either ChatError ChatResponse) +execRemoteCommand rhId cmd s retryNum = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s retryNum handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse) handleCommandError a = runExceptT a `E.catches` ioErrors @@ -304,13 +307,8 @@ parseChatCommand :: ByteString -> Either String ChatCommand parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace -- | Chat API commands interpreted in context of a local zone -processChatCommand :: ChatCommand -> CM ChatResponse -processChatCommand cmd = - chatVersionRange >>= (`processChatCommand'` cmd) -{-# INLINE processChatCommand #-} - -processChatCommand' :: VersionRangeChat -> ChatCommand -> CM ChatResponse -processChatCommand' vr = \case +processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> CM ChatResponse +processChatCommand vr nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser NewUser {profile, pastTimestamp} -> do forM_ profile $ \Profile {displayName} -> checkValidName displayName @@ -367,20 +365,20 @@ processChatCommand' vr = \case SetActiveUser uName viewPwd_ -> do tryChatError (withFastStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown - Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ + Right userId -> processChatCommand vr nm $ APISetActiveUser userId viewPwd_ SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ APISetUserContactReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserContactReceipts db user' settings ok user - SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings + SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserContactReceipts userId settings APISetUserGroupReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserGroupReceipts db user' settings ok user - SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings + SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserGroupReceipts userId settings APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -406,15 +404,15 @@ processChatCommand' vr = \case setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True} APIMuteUser userId' -> setUserNotifications userId' False APIUnmuteUser userId' -> setUserNotifications userId' True - HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIHideUser userId viewPwd - UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId viewPwd - MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId - UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId + HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIHideUser userId viewPwd + UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnhideUser userId viewPwd + MuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIMuteUser userId + UnmuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnmuteUser userId APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' viewPwd_ checkDeleteChatUser user' - withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues + withChatLock "deleteUser" $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ StartChat {mainApp, enableSndFiles, largeLinkData} -> withUser' $ \_ -> do chatWriteVar useLargeLinkData largeLinkData @@ -474,7 +472,7 @@ processChatCommand' vr = \case ExportArchive -> do ts <- liftIO getCurrentTime let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" - processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing + processChatCommand vr nm $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing APIImportArchive cfg -> checkChatStopped $ do fileErrs <- lift $ importArchive cfg setStoreChanged @@ -586,7 +584,7 @@ processChatCommand' vr = \case ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage - processChatCommand $ APIReportMessage gId reportedItemId reportReason "" + processChatCommand vr nm $ APIReportMessage gId reportedItemId reportReason "" APIUpdateChatItem (ChatRef cType chatId scope) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do unless (null mentions) $ throwCmdError "mentions are not supported in this chat" @@ -993,7 +991,7 @@ processChatCommand' vr = \case let ext = takeExtension fileName pure $ prefix <> formattedDate <> ext APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user - UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId + UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId @@ -1070,7 +1068,7 @@ processChatCommand' vr = \case CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct - withContactLock "deleteChat direct" chatId . procCmd $ + withContactLock "deleteChat direct" chatId $ case cdm of CDMFull notify -> do deleteCIFiles user filesInfo @@ -1091,7 +1089,7 @@ processChatCommand' vr = \case getContact db vr user chatId pure $ CRContactDeleted user ct' CDMMessages -> do - void $ processChatCommand $ APIClearChat cRef + void $ processChatCommand vr nm $ APIClearChat cRef withFastStore' $ \db -> setContactChatDeleted db user ct True pure $ CRContactDeleted user ct {chatDeleted = True} where @@ -1100,7 +1098,7 @@ processChatCommand' vr = \case when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) deleteAgentConnectionsAsync' contactConnIds doSendDel - CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do + CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync acId withFastStore' $ \db -> deletePendingContactConnection db userId chatId @@ -1112,7 +1110,7 @@ processChatCommand' vr = \case canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo - withGroupLock "deleteChat group" chatId . procCmd $ do + withGroupLock "deleteChat group" chatId $ do deleteCIFiles user filesInfo let doSendDel = memberActive membership && isOwner recipients = filter memberCurrentOrPending members @@ -1171,7 +1169,7 @@ processChatCommand' vr = \case pure $ CRAcceptingContactRequest user ct where acceptCReq user cReq contactUsed = do - (ct, conn, sqSecured) <- acceptContactRequest user cReq incognito + (ct, conn, sqSecured) <- acceptContactRequest nm user cReq incognito ct' <- withStore' $ \db -> do updateContactAccepted db user ct contactUsed conn' <- @@ -1230,7 +1228,7 @@ processChatCommand' vr = \case else throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFCalls) SendCallInvitation cName callType -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName - processChatCommand $ APISendCallInvitation contactId callType + processChatCommand vr nm $ APISendCallInvitation contactId callType APIRejectCall contactId -> -- party accepting call withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of @@ -1344,10 +1342,10 @@ processChatCommand' vr = \case _ -> throwCmdError "not supported" APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> withUser $ \_ -> - CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) - APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ + CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a nm token mode) + APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a nm token nonce code) >> ok_ APICheckToken token -> withUser $ \_ -> - CRNtfTokenStatus <$> withAgent (`checkNtfToken` token) + CRNtfTokenStatus <$> withAgent (\a -> checkNtfToken a nm token) APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ APIGetNtfConns nonce encNtfInfo -> withUser $ \_ -> do ntfInfos <- withAgent $ \a -> getNotificationConns a nonce encNtfInfo @@ -1376,16 +1374,16 @@ processChatCommand' vr = \case [] -> throwCmdError "no servers" _ -> do srvs' <- mapM aUserServer srvs - processChatCommand $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers + processChatCommand vr nm $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers where aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of Just Refl -> pure $ AUS SDBNew $ newUserServer srv Nothing -> throwCmdError $ "incorrect server protocol: " <> B.unpack (strEncode srv) APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> - lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a (aUserId user) server) + lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a nm (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> - processChatCommand $ APITestProtoServer userId srv + processChatCommand vr nm $ APITestProtoServer userId srv APIGetServerOperators -> CRServerOperatorConditions <$> withFastStore getServerOperators APISetServerOperators operators -> do as <- asks randomAgentServers @@ -1410,7 +1408,7 @@ processChatCommand' vr = \case SetServerOperators operatorsRoles -> do ops <- serverOperators <$> withFastStore getServerOperators ops' <- mapM (updateOp ops) operatorsRoles - processChatCommand $ APISetServerOperators ops' + processChatCommand vr nm $ APISetServerOperators ops' where updateOp :: [ServerOperator] -> ServerOperatorRoles -> CM ServerOperator updateOp ops r = @@ -1481,7 +1479,7 @@ processChatCommand' vr = \case _ -> throwCmdError "not supported" SetChatTTL chatName newTTL -> withUser' $ \user@User {userId} -> do chatRef <- getChatRef user chatName - processChatCommand $ APISetChatTTL userId chatRef newTTL + processChatCommand vr nm $ APISetChatTTL userId chatRef newTTL GetChatTTL chatName -> withUser' $ \user -> do -- TODO [knocking] support scope in CLI apis ChatRef cType chatId _ <- getChatRef user chatName @@ -1501,18 +1499,18 @@ processChatCommand' vr = \case lift $ setChatItemsExpiration user newTTL ttlCount ok user SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do - processChatCommand $ APISetChatItemTTL userId newTTL_ + processChatCommand vr nm $ APISetChatItemTTL userId newTTL_ APIGetChatItemTTL userId -> withUserId' userId $ \user -> do ttl <- withFastStore' (`getChatItemTTL` user) pure $ CRChatItemTTL user (Just ttl) GetChatItemTTL -> withUser' $ \User {userId} -> do - processChatCommand $ APIGetChatItemTTL userId + processChatCommand vr nm $ APIGetChatItemTTL userId APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> CRNetworkConfig <$> lift getNetworkConfig SetNetworkConfig simpleNetCfg -> do cfg <- (`updateNetworkConfig` simpleNetCfg) <$> lift getNetworkConfig - void . processChatCommand $ APISetNetworkConfig cfg + void . processChatCommand vr nm $ APISetNetworkConfig cfg pure $ CRNetworkConfig cfg APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ @@ -1677,11 +1675,11 @@ processChatCommand' vr = \case -- TODO GRModerator when most users migrate when (membershipRole >= GRAdmin) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages let settings = (memberSettings m) {showMessages} - processChatCommand $ APISetMemberSettings gId mId settings + processChatCommand vr nm $ APISetMemberSettings gId mId settings ContactInfo cName -> withContactName cName APIContactInfo ShowGroupInfo gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGroupInfo groupId + processChatCommand vr nm $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo ContactQueueInfo cName -> withContactName cName APIContactQueueInfo GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo @@ -1699,19 +1697,19 @@ processChatCommand' vr = \case EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - APIAddContact userId incognito -> withUserId userId $ \user -> procCmd $ do + APIAddContact userId incognito -> withUserId userId $ \user -> do -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode userData <- contactShortLinkData (userProfileToSend user incognitoProfile Nothing False) Nothing -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation (Just userData) Nothing IKPQOn subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation (Just userData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn pure $ CRInvitation user ccLink' conn AddContact incognito -> withUser $ \User {userId} -> - processChatCommand $ APIAddContact userId incognito + processChatCommand vr nm $ APIAddContact userId incognito APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do conn <- withFastStore $ \db -> getPendingContactConnection db userId connId let PendingContactConnection {pccConnStatus, customUserProfileId} = conn @@ -1748,7 +1746,7 @@ processChatCommand' vr = \case then Just <$> contactShortLinkData (userProfileToSend newUser Nothing Nothing False) Nothing else pure Nothing -- TODO [certs rcv] - (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData_ Nothing IKPQOn subMode + (agConnId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId newUser) True SCMInvitation userData_ Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId @@ -1915,17 +1913,17 @@ processChatCommand' vr = \case APIListContacts userId -> withUserId userId $ \user -> CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> - processChatCommand $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do + processChatCommand vr nm $ APIListContacts userId + APICreateMyAddress userId -> withUserId userId $ \user -> do subMode <- chatReadVar subscriptionMode userData <- contactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just userData) Nothing IKPQOn subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode pure $ CRUserContactLinkCreated user ccLink' CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand $ APICreateMyAddress userId + processChatCommand vr nm $ APICreateMyAddress userId APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do conn <- withFastStore $ \db -> getUserAddressConnection db vr user withChatLock "deleteMyAddress" $ do @@ -1938,11 +1936,11 @@ processChatCommand' vr = \case _ -> user pure $ CRUserContactLinkDeleted user' DeleteMyAddress -> withUser $ \User {userId} -> - processChatCommand $ APIDeleteMyAddress userId + processChatCommand vr nm $ APIDeleteMyAddress userId APIShowMyAddress userId -> withUserId' userId $ \user -> CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> - processChatCommand $ APIShowMyAddress userId + processChatCommand vr nm $ APIShowMyAddress userId APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData user) APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do @@ -1954,7 +1952,7 @@ processChatCommand' vr = \case let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ profileContactLink ucl} updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl SetProfileAddress onOff -> withUser $ \User {userId} -> - processChatCommand $ APISetProfileAddress userId onOff + processChatCommand vr nm $ APISetProfileAddress userId onOff APISetAddressSettings userId settings@AddressSettings {businessAddress, autoAccept} -> withUserId userId $ \user -> do ucl@UserContactLink {userContactLinkId, shortLinkDataSet, addressSettings} <- withFastStore (`getUserAddress` user) forM_ autoAccept $ \AutoAccept {acceptIncognito} -> do @@ -1968,28 +1966,28 @@ processChatCommand' vr = \case withFastStore' $ \db -> updateUserAddressSettings db userContactLinkId settings pure $ CRUserContactLinkUpdated user ucl'' SetAddressSettings settings -> withUser $ \User {userId} -> - processChatCommand $ APISetAddressSettings userId settings + processChatCommand vr nm $ APISetAddressSettings userId settings AcceptContact incognito cName -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIAcceptContact incognito connReqId + processChatCommand vr nm $ APIAcceptContact incognito connReqId RejectContact cName -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIRejectContact connReqId + processChatCommand vr nm $ APIRejectContact connReqId ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing + processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing + processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing + processChatCommand vr nm $ APIForwardChatItems toChatRef (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg case sendName of @@ -1997,13 +1995,13 @@ processChatCommand' vr = \case withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let sendRef = SRDirect ctId - processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] Left _ -> withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case Right [(gInfo, member)] -> do let GroupInfo {localDisplayName = gName} = gInfo GroupMember {localDisplayName = mName} = member - processChatCommand $ SendMemberContactMessage gName mName msg + processChatCommand vr nm $ SendMemberContactMessage gName mName msg Right (suspectedMember : _) -> throwChatError $ CEContactNotFound name (Just suspectedMember) _ -> @@ -2016,10 +2014,10 @@ processChatCommand' vr = \case GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_ (gId,cScope_,) <$> liftIO (getMessageMentions db user gId msg) let sendRef = SRGroup gId cScope_ - processChatCommand $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand vr nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] SNLocal -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand $ APICreateChatItems folderId [composedMessage Nothing mc] + processChatCommand vr nm $ APICreateChatItems folderId [composedMessage Nothing mc] SendMemberContactMessage gName mName msg -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName m <- withFastStore $ \db -> getGroupMember db vr user gId mId @@ -2029,22 +2027,22 @@ processChatCommand' vr = \case g <- withFastStore $ \db -> getGroupInfo db vr user gId unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwCmdError "direct messages not allowed" toView $ CEvtNoMemberContactCreating user g m - processChatCommand (APICreateMemberContact gId mId) >>= \case + processChatCommand vr nm (APICreateMemberContact gId mId) >>= \case CRNewMemberContact _ ct@Contact {contactId} _ _ -> do toViewTE $ TENewMemberContact user ct g m - processChatCommand $ APISendMemberContactInvitation contactId (Just mc) + processChatCommand vr nm $ APISendMemberContactInvitation contactId (Just mc) cr -> pure cr Just ctId -> do let sendRef = SRDirect ctId - processChatCommand $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] SendLiveMessage chatName msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg withSendRef chatRef $ \sendRef -> do let mc = MCText msg - processChatCommand $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand vr nm $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] SendMessageBroadcast mc -> withUser $ \user -> do contacts <- withFastStore' $ \db -> getUserContacts db vr user - withChatLock "sendMessageBroadcast" . procCmd $ do + withChatLock "sendMessageBroadcast" $ do let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts case ctConns_ of Nothing -> do @@ -2085,28 +2083,28 @@ processChatCommand' vr = \case contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg - processChatCommand $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] + processChatCommand vr nm $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg - processChatCommand $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast + processChatCommand vr nm $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user gName deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg - processChatCommand $ APIDeleteMemberChatItem gId (deletedItemId :| []) + processChatCommand vr nm $ APIDeleteMemberChatItem gId (deletedItemId :| []) EditMessage chatName editedMsg msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg editedItemId <- getSentChatItemIdByText user chatRef editedMsg let mc = MCText msg - processChatCommand $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions + processChatCommand vr nm $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg let mc = MCText msg - processChatCommand $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions + processChatCommand vr nm $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions ReactToMessage add reaction chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg - processChatCommand $ APIChatItemReaction chatRef chatItemId add reaction + processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction APINewGroup userId incognito gProfile@GroupProfile {displayName} -> withUserId userId $ \user -> do checkValidName displayName gVar <- asks random @@ -2118,7 +2116,7 @@ processChatCommand' vr = \case createGroupFeatureItems user cd CISndGroupFeature gInfo pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> - processChatCommand $ APINewGroup userId incognito gProfile + processChatCommand vr nm $ APINewGroup userId incognito gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId @@ -2136,7 +2134,7 @@ processChatCommand' vr = \case gVar <- asks random subMode <- chatReadVar subscriptionMode -- TODO [certs rcv] - (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + (agentConnId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member @@ -2150,7 +2148,7 @@ processChatCommand' vr = \case Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do - withGroupLock "joinGroup" groupId . procCmd $ do + withGroupLock "joinGroup" groupId $ do (invitation, ct) <- withFastStore $ \db -> do inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId (inv,) <$> getContactViaMember db vr user fromMember @@ -2173,7 +2171,7 @@ processChatCommand' vr = \case updateGroupMemberStatus db userId membership GSMemAccepted -- MFAll is default for new groups unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs} - void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode) + void (withAgent $ \a -> joinConnection a nm (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode) `catchChatError` \e -> do withFastStore' $ \db -> do updateGroupMemberStatus db userId fromMember GSMemInvited @@ -2247,7 +2245,7 @@ processChatCommand' vr = \case (gInfo', m') <- withFastStore' $ \db -> deleteGroupMemberSupportChat db user gInfo m pure $ CRMemberSupportChatDeleted user gInfo' m' APIMembersRole groupId memberIds newRole -> withUser $ \user -> - withGroupLock "memberRole" groupId . procCmd $ do + withGroupLock "memberRole" groupId $ do g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId when (selfSelected gInfo) $ throwCmdError "can't change role for self" let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members @@ -2315,7 +2313,7 @@ processChatCommand' vr = \case updateGroupMemberRole db user m newRole pure (m :: GroupMember) {memberRole = newRole} APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user -> - withGroupLock "blockForAll" groupId . procCmd $ do + withGroupLock "blockForAll" groupId $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId when (selfSelected gInfo) $ throwCmdError "can't block/unblock self" let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members @@ -2361,7 +2359,7 @@ processChatCommand' vr = \case ts = ciContentTexts content in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user -> - withGroupLock "removeMembers" groupId . procCmd $ do + withGroupLock "removeMembers" groupId $ do Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers members memCount = S.size groupMemberIds @@ -2447,7 +2445,7 @@ processChatCommand' vr = \case APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo - withGroupLock "leaveGroup" groupId . procCmd $ do + withGroupLock "leaveGroup" groupId $ do cancelFilesInProgress user filesInfo let recipients = filter memberCurrentOrPending members msg <- sendGroupMessage' user gInfo recipients XGrpLeave @@ -2471,10 +2469,10 @@ processChatCommand' vr = \case -- ok_ -- CRGroupConversationsDeleted AddMember gName cName memRole -> withUser $ \user -> do (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName - processChatCommand $ APIAddMember groupId contactId memRole + processChatCommand vr nm $ APIAddMember groupId contactId memRole JoinGroup gName enableNtfs -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIJoinGroup groupId enableNtfs + processChatCommand vr nm $ APIJoinGroup groupId enableNtfs AcceptMember gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIAcceptMember gId gMemberId memRole MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked @@ -2483,19 +2481,19 @@ processChatCommand' vr = \case gId <- getGroupIdByName db user gName gMemberIds <- S.fromList <$> mapM (getGroupMemberIdByName db user gId) (S.toList gMemberNames) pure (gId, gMemberIds) - processChatCommand $ APIRemoveMembers gId gMemberIds withMessages + processChatCommand vr nm $ APIRemoveMembers gId gMemberIds withMessages LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APILeaveGroup groupId + processChatCommand vr nm $ APILeaveGroup groupId DeleteGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) + processChatCommand vr nm $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) ClearGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIClearChat (ChatRef CTGroup groupId Nothing) + processChatCommand vr nm $ APIClearChat (ChatRef CTGroup groupId Nothing) ListMembers gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIListMembers groupId + processChatCommand vr nm $ APIListMembers groupId ListMemberSupportChats gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName (Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId @@ -2505,7 +2503,7 @@ processChatCommand' vr = \case CRGroupsList user <$> withFastStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName - processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ + processChatCommand vr nm $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do g <- withFastStore $ \db -> getGroup db vr user groupId runUpdateGroupProfile user g p' @@ -2526,7 +2524,7 @@ processChatCommand' vr = \case userData <- groupShortLinkData groupProfile let crClientData = encodeJSON $ CRDataGroup groupLinkId -- TODO [certs rcv] - (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just userData) (Just crClientData) IKPQOff subMode + (connId, (ccLink, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMContact (Just userData) (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink gLink <- withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo gLink @@ -2565,7 +2563,7 @@ processChatCommand' vr = \case subMode <- chatReadVar subscriptionMode -- TODO PQ should negotitate contact connection with PQSupportOn? -- TODO [certs rcv] - (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode + (connId, (CCLink cReq _, _serviceId)) <- withAgent $ \a -> createConnection a nm (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode -- TODO not sure it is correct to set connections status here? @@ -2589,16 +2587,16 @@ processChatCommand' vr = \case _ -> throwChatError CEGroupMemberNotActive CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APICreateGroupLink groupId mRole + processChatCommand vr nm $ APICreateGroupLink groupId mRole GroupLinkMemberRole gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGroupLinkMemberRole groupId mRole + processChatCommand vr nm $ APIGroupLinkMemberRole groupId mRole DeleteGroupLink gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteGroupLink groupId + processChatCommand vr nm $ APIDeleteGroupLink groupId ShowGroupLink gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIGetGroupLink groupId + processChatCommand vr nm $ APIGetGroupLink groupId SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do (groupId, quotedItemId, mentions) <- withFastStore $ \db -> do @@ -2606,10 +2604,10 @@ processChatCommand' vr = \case qiId <- getGroupChatItemIdByText db user gId cName quotedMsg (gId, qiId,) <$> liftIO (getMessageMentions db user gId msg) let mc = MCText msg - processChatCommand $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + processChatCommand vr nm $ APISendMessages (SRGroup groupId Nothing) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand $ APIClearChat (ChatRef CTLocal folderId Nothing) + processChatCommand vr nm $ APIClearChat (ChatRef CTLocal folderId Nothing) LastChats count_ -> withUser' $ \user -> do let count = fromMaybe 5000 count_ (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) @@ -2617,14 +2615,14 @@ processChatCommand' vr = \case pure $ CRChats previews LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand $ APIGetChat chatRef Nothing (CPLast count) search + chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast count) search pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) LastMessages Nothing count search -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search pure $ CRChatItems user Nothing chatItems LastChatItemId (Just chatName) index -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand (APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing) + chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) LastChatItemId Nothing index -> withUser $ \user -> do chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing @@ -2640,14 +2638,14 @@ processChatCommand' vr = \case ShowChatItemInfo chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName itemId <- getChatItemIdByText user chatRef msg - processChatCommand $ APIGetChatItemInfo chatRef itemId + processChatCommand vr nm $ APIGetChatItemInfo chatRef itemId ShowLiveItems on -> withUser $ \_ -> asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName case chatRef of - ChatRef CTLocal folderId _ -> processChatCommand $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] - _ -> withSendRef chatRef $ \sendRef -> processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] + ChatRef CTLocal folderId _ -> processChatCommand vr nm $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] + _ -> withSendRef chatRef $ \sendRef -> processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName withSendRef chatRef $ \sendRef -> do @@ -2656,25 +2654,25 @@ processChatCommand' vr = \case fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] + processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> throwCmdError "TODO" -- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> - withFileLock "receiveFile" fileId . procCmd $ do + withFileLock "receiveFile" fileId $ do (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft receiveFile' user ft' userApprovedRelays rcvInline_ filePath_ SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do - withFileLock "setFileToReceive" fileId . procCmd $ do + withFileLock "setFileToReceive" fileId $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> - withFileLock "cancelFile" fileId . procCmd $ + withFileLock "cancelFile" fileId $ withFastStore (\db -> getFileTransfer db user fileId) >>= \case FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" @@ -2864,9 +2862,6 @@ processChatCommand' vr = \case -- where Left means command result, and Right – some other command to be processed by this function. CustomChatCommand _cmd -> withUser $ \_ -> throwCmdError "not supported" where - procCmd :: CM ChatResponse -> CM ChatResponse - procCmd = id - {-# INLINE procCmd #-} ok_ = pure $ CRCmdOk Nothing ok = pure . CRCmdOk . Just getChatRef :: User -> ChatName -> CM ChatRef @@ -2896,13 +2891,13 @@ processChatCommand' vr = \case checkStoreNotChanged :: CM ChatResponse -> CM ChatResponse checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) withUserName :: UserName -> (UserId -> ChatCommand) -> CM ChatResponse - withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand . cmd + withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand vr nm . cmd withContactName :: ContactName -> (ContactId -> ChatCommand) -> CM ChatResponse withContactName cName cmd = withUser $ \user -> - withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd + withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand vr nm . cmd withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> CM ChatResponse withMemberName gName mName cmd = withUser $ \user -> - getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd + getGroupAndMemberId user gName mName >>= processChatCommand vr nm . uncurry cmd getConnectionCode :: ConnId -> CM Text getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) verifyConnectionCode :: User -> Connection -> Maybe Text -> CM ChatResponse @@ -2955,7 +2950,7 @@ processChatCommand' vr = \case joinPreparedConn conn incognitoProfile chatV = do let profileToSend = userProfileToSend user incognitoProfile Nothing False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a (aUserId user) (aConnId conn) True cReq dm pqSup' subMode + (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined conn' <- withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus pure (conn', incognitoProfile) @@ -3049,7 +3044,7 @@ processChatCommand' vr = \case let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend (Just xContactId) welcomeSharedMsgId msg_) subMode <- chatReadVar subscriptionMode - void $ withAgent $ \a -> joinConnection a (aUserId user) (aConnId conn) True cReq dm pqSup subMode + void $ withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup subMode withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared ConnJoined contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = @@ -3073,7 +3068,7 @@ processChatCommand' vr = \case contacts <- withFastStore' $ \db -> getUserContacts db vr user user' <- updateUser asks currentUser >>= atomically . (`writeTVar` Just user') - withChatLock "updateProfile" . procCmd $ do + withChatLock "updateProfile" $ do when shouldUpdateAddressData $ setMyAddressData' user' summary <- sendUpdateToContacts user' contacts pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary @@ -3126,7 +3121,7 @@ processChatCommand' vr = \case let shortLinkProfile = userProfileToSend user Nothing Nothing False -- TODO [short links] do not save address to server if data did not change, spinners, error handling userData <- contactShortLinkData shortLinkProfile $ Just addressSettings - sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact userData Nothing) + sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userData Nothing) withFastStore' $ \db -> setUserContactLinkShortLink db userContactLinkId sLnk let autoAccept' = (\aa -> aa {acceptIncognito = False}) <$> autoAccept addressSettings ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk), shortLinkDataSet = True, addressSettings = addressSettings {autoAccept = autoAccept'}} @@ -3186,7 +3181,7 @@ processChatCommand' vr = \case conn <- withFastStore $ \db -> getGroupLinkConnection db vr user gInfo userData <- groupShortLinkData groupProfile let crClientData = encodeJSON $ CRDataGroup groupLinkId - sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact userData (Just crClientData)) + sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userData (Just crClientData)) gLink' <- withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk pure $ CRGroupLink user gInfo gLink' checkValidName :: GroupName -> CM () @@ -3289,7 +3284,7 @@ processChatCommand' vr = \case FTSnd {fileTransferMeta = FileTransferMeta {filePath, xftpSndFile}} -> forward filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs _ -> throwChatError CEFileNotReceived {fileId} where - forward path cfArgs = processChatCommand . sendCommand chatName $ CryptoFile path cfArgs + forward path cfArgs = processChatCommand vr nm $ sendCommand chatName $ CryptoFile path cfArgs getGroupAndMemberId :: User -> GroupName -> ContactName -> CM (GroupId, GroupMemberId) getGroupAndMemberId user gName groupMemberName = withStore $ \db -> do @@ -3381,7 +3376,7 @@ processChatCommand' vr = \case GroupInfo {chatSettings} <- getGroupInfo db vr user gId pure (gId, chatSettings) _ -> throwCmdError "not supported" - processChatCommand $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings + processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan) connectPlan user (ACL SCMInvitation cLink) = case cLink of CLFull cReq -> invitationReqAndPlan cReq Nothing Nothing @@ -3451,8 +3446,8 @@ processChatCommand' vr = \case case plan of CPError e -> eToView e; _ -> pure () case plan of CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand $ APIConnect userId incognito ccLink + processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand vr nm $ APIConnect userId incognito ccLink | otherwise = pure $ CRConnectionPlan user ccLink plan invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> CM ConnectionPlan invitationRequestPlan user cReq contactSLinkData_ = do @@ -3539,7 +3534,7 @@ processChatCommand' vr = \case getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m, ConnLinkData m) getShortLinkConnReq user l = do l' <- restoreShortLink' l - (cReq, cData) <- withAgent (\a -> getConnShortLink a (aUserId user) l') + (cReq, cData) <- withAgent $ \a -> getConnShortLink a nm (aUserId user) l' case cData of ContactLinkData {direct} | not direct -> throwChatError CEUnsupportedConnReq _ -> pure () @@ -3593,7 +3588,7 @@ processChatCommand' vr = \case updatePCCShortLinkData conn@PendingContactConnection {connLinkInv} profile = forM (connShortLink =<< connLinkInv) $ \_ -> do userData <- contactShortLinkData profile Nothing - shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId' conn) SCMInvitation userData Nothing) + shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userData Nothing) shortenShortLink' :: ConnShortLink m -> CM (ConnShortLink m) shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config) shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) @@ -3890,7 +3885,7 @@ processChatCommand' vr = \case (CISndMsgContent mc, f, itemForwarded, ts) getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) - CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) + CRQueueInfo user msgInfo <$> withAgent (\a -> getConnectionQueueInfo a nm acId) withSendRef :: ChatRef -> (SendRef -> CM ChatResponse) -> CM ChatResponse withSendRef chatRef a = case chatRef of ChatRef CTDirect cId _ -> a $ SRDirect cId diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index ff27020aaa..81c4f080f4 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -84,7 +84,7 @@ import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Client (NetworkConfig (..)) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -881,8 +881,8 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of -- - xContactId is set on the contact at the first acceptance attempt, not after accept success, which prevents profile updates after such attempt. -- It may be reasonable to set it when contact is first prepared, but then we can't use it to ignore requests after acceptance, -- and it may lead to race conditions with XInfo events. -acceptContactRequest :: User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured) -acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId_, xContactId, pqSupport} incognito = do +acceptContactRequest :: NetworkRequestMode -> User -> UserContactRequest -> IncognitoEnabled -> CM (Contact, Connection, SndQueueSecured) +acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId = AgentInvId invId, contactId_, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId_, xContactId, pqSupport} incognito = do subMode <- chatReadVar subscriptionMode let pqSup = PQSupportOn pqSup' = pqSup `CR.pqSupportAnd` pqSupport @@ -912,7 +912,7 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = let profileToSend = userProfileToSend' user incognitoProfile (Just ct) False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend -- TODO [certs rcv] - (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (aUserId user) (aConnId conn) True invId dm pqSup' subMode) + (ct,conn,) . fst <$> withAgent (\a -> acceptContact a nm (aUserId user) (aConnId conn) True invId dm pqSup' subMode) acceptContactRequestAsync :: User -> Int64 -> Contact -> UserContactRequest -> Maybe IncognitoProfile -> CM Contact acceptContactRequestAsync diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 194fc1bb06..bb1f45fc67 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -97,8 +97,12 @@ foreign export ccall "chat_reopen_store" cChatReopenStore :: StablePtr ChatContr foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString +foreign export ccall "chat_send_cmd_retry" cChatSendCmdRetry :: StablePtr ChatController -> CString -> CInt -> IO CJSONString + foreign export ccall "chat_send_remote_cmd" cChatSendRemoteCmd :: StablePtr ChatController -> CInt -> CString -> IO CJSONString +foreign export ccall "chat_send_remote_cmd_retry" cChatSendRemoteCmdRetry :: StablePtr ChatController -> CInt -> CString -> CInt -> IO CJSONString + foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString @@ -155,20 +159,30 @@ cChatReopenStore cPtr = do c <- deRefStablePtr cPtr newCAString =<< chatReopenStore c --- | send command to chat (same syntax as in terminal for now) +-- | send command to chat cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString -cChatSendCmd cPtr cCmd = do +cChatSendCmd cPtr cCmd = cChatSendCmdRetry cPtr cCmd 0 + +-- | send command to chat with retry count +cChatSendCmdRetry :: StablePtr ChatController -> CString -> CInt -> IO CJSONString +cChatSendCmdRetry cPtr cCmd cRetryNum = do c <- deRefStablePtr cPtr cmd <- B.packCString cCmd - newCStringFromLazyBS =<< chatSendCmd c cmd + newCStringFromLazyBS =<< chatSendRemoteCmdRetry c Nothing cmd (fromIntegral cRetryNum) +{-# INLINE cChatSendCmdRetry #-} --- | send command to chat (same syntax as in terminal for now) +-- | send remote command to chat cChatSendRemoteCmd :: StablePtr ChatController -> CInt -> CString -> IO CJSONString -cChatSendRemoteCmd cPtr cRemoteHostId cCmd = do +cChatSendRemoteCmd cPtr cRhId cCmd = cChatSendRemoteCmdRetry cPtr cRhId cCmd 0 + +-- | send remote command to chat with retry count +cChatSendRemoteCmdRetry :: StablePtr ChatController -> CInt -> CString -> CInt -> IO CJSONString +cChatSendRemoteCmdRetry cPtr cRemoteHostId cCmd cRetryNum = do c <- deRefStablePtr cPtr cmd <- B.packCString cCmd let rhId = Just $ fromIntegral cRemoteHostId - newCStringFromLazyBS =<< chatSendRemoteCmd c rhId cmd + newCStringFromLazyBS =<< chatSendRemoteCmdRetry c rhId cmd (fromIntegral cRetryNum) +{-# INLINE cChatSendRemoteCmdRetry #-} -- | receive message from chat (blocking) cChatRecvMsg :: StablePtr ChatController -> IO CJSONString @@ -294,10 +308,11 @@ handleErr :: IO () -> IO String handleErr a = (a $> "") `catch` (pure . show @SomeException) chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString -chatSendCmd cc = chatSendRemoteCmd cc Nothing +chatSendCmd cc cmd = chatSendRemoteCmdRetry cc Nothing cmd 0 +{-# INLINE chatSendCmd #-} -chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString -chatSendRemoteCmd cc rh s = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s) cc +chatSendRemoteCmdRetry :: ChatController -> Maybe RemoteHostId -> B.ByteString -> Int -> IO JSONByteString +chatSendRemoteCmdRetry cc rh s retryNum = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s retryNum) cc chatRecvMsg :: ChatController -> IO JSONByteString chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse @@ -312,6 +327,7 @@ chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) chatParseMarkdown :: ByteString -> JSONByteString chatParseMarkdown = J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 +{-# INLINE chatParseMarkdown #-} chatParseServer :: ByteString -> JSONByteString chatParseServer = J.encode . toServerAddress . strDecode diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index aa741df09b..65b3c79312 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -366,8 +366,8 @@ getRemoteFile rhId rf = do createDirectoryIfMissing True dir liftRH rhId $ remoteGetFile c dir rf -processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> CM ChatResponse -processRemoteCommand remoteHostId c cmd s = case cmd of +processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> Int -> CM ChatResponse +processRemoteCommand remoteHostId c cmd s retryNum = case cmd of SendFile chatName f -> sendFile "/f" chatName f SendImage chatName f -> sendFile "/img" chatName f _ -> chatRemoteSend s @@ -380,7 +380,7 @@ processRemoteCommand remoteHostId c cmd s = case cmd of cryptoFileStr CryptoFile {filePath, cryptoArgs} = maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs <> encodeUtf8 (T.pack filePath) - chatRemoteSend = either throwError pure <=< liftRH remoteHostId . remoteSend c + chatRemoteSend cmd' = either throwError pure =<< liftRH remoteHostId (remoteSend c cmd' retryNum) liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) @@ -497,8 +497,8 @@ parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo parseCtrlAppInfo ctrlAppInfo = do liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo -handleRemoteCommand :: (ByteString -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () -handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do +handleRemoteCommand :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () +handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" liftIO (tryRemoteError' parseRequest) >>= \case Right (rfKN, getNext, rc) -> do @@ -514,7 +514,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque replyError = reply . RRChatResponse . RRError processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () processCommand user rfKN getNext = \case - RCSend {command} -> lift $ handleSend execChatCommand command >>= reply + RCSend {command, retryNumber} -> lift $ handleSend execCC command retryNumber >>= reply RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile rfKN fileName fileSize fileDigest getNext >>= reply RCGetFile {file} -> handleGetFile user file replyWith @@ -550,12 +550,12 @@ tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocol tryRemoteError' = tryAllErrors' (RPEException . tshow) {-# INLINE tryRemoteError' #-} -handleSend :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM' RemoteResponse -handleSend execChatCommand command = do +handleSend :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> Int -> CM' RemoteResponse +handleSend execCC command retryNum = do logDebug $ "Send: " <> tshow command - -- execChatCommand checks for remote-allowed commands - -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper - RRChatResponse . eitherToResult <$> execChatCommand (encodeUtf8 command) + -- execCC checks for remote-allowed commands + -- convert errors thrown in execCC into error responses to prevent aborting the protocol wrapper + RRChatResponse . eitherToResult <$> execCC (encodeUtf8 command) retryNum handleRecv :: Int -> TBQueue (Either ChatError ChatEvent) -> IO RemoteResponse handleRecv time events = do @@ -615,8 +615,8 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} -- | Take a look at emoji of tlsunique, commit pairing, and start session server -verifyRemoteCtrlSession :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo -verifyRemoteCtrlSession execChatCommand sessCode' = do +verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo +verifyRemoteCtrlSession execCC sessCode' = do (sseq, client, ctrlName, sessionCode, vars) <- chatReadVar remoteCtrlSession >>= \case Nothing -> throwError $ ChatErrorRemoteCtrl RCEInactive @@ -631,7 +631,7 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls cc <- ask - http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execChatCommand encryption remoteOutputQ req `runReaderT` cc + http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execCC encryption remoteOutputQ req `runReaderT` cc void . forkIO $ monitor sseq http2Server updateRemoteCtrlSession sseq $ \case RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ} diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index 207eade665..d8155db7d6 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -56,7 +56,7 @@ import System.FilePath (takeFileName, ()) import UnliftIO data RemoteCommand - = RCSend {command :: Text} -- TODO maybe ChatCommand here? + = RCSend {command :: Text, retryNumber :: Int} | RCRecv {wait :: Int} -- this wait should be less than HTTP timeout | -- local file encryption is determined by the host, but can be overridden for videos RCStoreFile {fileName :: String, fileSize :: Word32, fileDigest :: FileDigest} -- requires attachment @@ -133,9 +133,9 @@ closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClien -- ** Commands -remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) -remoteSend c cmd = - sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case +remoteSend :: RemoteHostClient -> ByteString -> Int -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) +remoteSend c cmd retryNumber = + sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd, retryNumber} >>= \case RRChatResponse cr -> pure $ resultToEither cr r -> badResponse r diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 7c52f59a50..b7eebd141a 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -14,7 +14,6 @@ module Simplex.Chat.Terminal.Input where import Control.Applicative (optional, (<|>)) import Control.Concurrent (forkFinally, forkIO, killThread, mkWeakThreadId, threadDelay) import Control.Monad -import Control.Monad.Except import Control.Monad.Reader import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (second) @@ -63,7 +62,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do cmd = parseChatCommand bs rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing unless (isMessage cmd) $ echo s - r <- runReaderT (execChatCommand rh' bs) cc + r <- execChatCommand rh' bs 0 `runReaderT` cc case r of Right r' -> processResp cmd rh r' Left _ -> when (isMessage cmd) $ echo s @@ -150,7 +149,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO (Either ChatError ChatResponse) sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg - runExceptT (processChatCommand cmd) `runReaderT` cc + execChatCommand' cmd 0 `runReaderT` cc runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index a43cad05b4..5731be7b0b 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -15,7 +15,6 @@ import Control.Concurrent (ThreadId) import Control.Logger.Simple import Control.Monad import Control.Monad.Catch (MonadMask) -import Control.Monad.Except import Control.Monad.Reader import Data.List (intercalate) import Data.Text (Text) @@ -23,7 +22,7 @@ import qualified Data.Text as T import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Simplex.Chat.Controller -import Simplex.Chat.Library.Commands (execChatCommand, processChatCommand) +import Simplex.Chat.Library.Commands (execChatCommand, execChatCommand') import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent (CIContent (..), SMsgDirection (..)) @@ -166,11 +165,11 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha (True, CISRcvNew) -> do let itemId = chatItemId' ci chatRef = chatInfoToRef chat - void $ runReaderT (runExceptT $ processChatCommand (APIChatItemsRead chatRef [itemId])) cc + void $ runReaderT (execChatCommand' (APIChatItemsRead chatRef [itemId]) 0) cc _ -> pure () logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s getRemoteUser rhId = - runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case + runReaderT (execChatCommand (Just rhId) "/user" 0) cc >>= \case Right CRActiveUser {user} -> updateRemoteUser ct user rhId cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 98a2213fd1..12a63d2333 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -312,7 +312,7 @@ startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts cc <- newChatController db (Just user) cfg opts False - void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc + void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") 0 `runReaderT` cc chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 66dc5b9571..d4ef4674bb 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -34,6 +34,7 @@ import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificati import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (NetworkTimeout (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport @@ -353,7 +354,7 @@ testRetryConnectingClientTimeout ps = do }, presetServers = let def@PresetServers {netCfg} = presetServers testCfg - in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} + in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}} } opts' = testOpts diff --git a/tests/Test.hs b/tests/Test.hs index b8e2c4a4c0..cbe2efd281 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -9,7 +9,6 @@ import ChatClient import ChatTests import ChatTests.DBUtils import ChatTests.Utils (xdescribe'') -import Control.Exception (bracket_) import Control.Logger.Simple import Data.Time.Clock.System import JSONTests @@ -24,6 +23,7 @@ import UnliftIO.Temporary (withTempDirectory) import ValidNames import ViewTests #if defined(dbPostgres) +import Control.Exception (bracket_) import PostgresSchemaDump import Simplex.Chat.Store.Postgres.Migrations (migrations) import Simplex.Messaging.Agent.Store.Postgres.Util (createDBAndUserIfNotExists, dropAllSchemasExceptSystem, dropDatabaseAndUser)