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
This commit is contained in:
Evgeny
2025-07-07 11:01:03 +01:00
committed by GitHub
parent f2e8545c0a
commit 88547ab704
41 changed files with 695 additions and 408 deletions
+162 -67
View File
@@ -94,14 +94,14 @@ func chatSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true,
return try apiResult(res)
}
func chatApiSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) -> APIResult<R> {
func chatApiSendCmdSync<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) -> APIResult<R> {
if log {
logger.debug("chatSendCmd \(cmd.cmdType)")
}
let start = Date.now
let resp: APIResult<R> = 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<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDe
return try apiResult(res)
}
func chatApiSendCmdWithRetry<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, retryNum: Int32 = 0) async -> APIResult<R>? {
let r: APIResult<R> = 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<R>? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum + 1)
cont.resume(returning: r1)
}
)
}
} else {
return r
}
}
@inline(__always)
func chatApiSendCmd<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async -> APIResult<R> {
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<R: ChatAPIResult>(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult<R> {
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<ChatResponse0>? = 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<ChatResponse0>? = 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<ChatResponse1> = await chatApiSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false)
let r: APIResult<ChatResponse1>? = 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<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink))
let r: APIResult<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink))
let r: APIResult<ChatResponse1>? = 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>(_ r: APIResult<R>) -> 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<ChatResponse1> = await chatApiSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg))
let r: APIResult<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg))
let r: APIResult<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId))
let r: APIResult<ChatResponse1>? = 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<ChatResponse1>? = 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<ChatResponse1>? = 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<ChatResponse1>) 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<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiSetAddressSettings(userId: userId, addressSettings: settings))
let r: APIResult<ChatResponse1>? = 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<ChatResponse1> = await chatApiSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId))
let r: APIResult<ChatResponse1>? = 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<ChatResponse2> = await chatApiSendCmd(.apiJoinGroup(groupId: groupId))
func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult? {
let r: APIResult<ChatResponse2>? = 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<ChatResponse2>? = 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<ChatResponse2>? = 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<ChatResponse2>? = 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 {
@@ -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")
@@ -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,
@@ -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
]}
)
}
@@ -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")
@@ -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 {
@@ -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,
@@ -220,6 +220,29 @@ struct DropdownCustomTimePicker: View {
}
}
struct WrappedPicker<SelectionValue: Hashable, Content: View>: View {
var title: LocalizedStringKey
var selection: Binding<SelectionValue>
@ViewBuilder var content: () -> Content
init(_ title: LocalizedStringKey, selection: Binding<SelectionValue>, @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(
@@ -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)
@@ -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 {
+1 -1
View File
@@ -167,7 +167,7 @@ struct TerminalView: View {
func sendTerminalCmd(_ cmd: String) async {
let start: Date = .now
await withCheckedContinuation { (cont: CheckedContinuation<Void, Never>) 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<ChatResponse2>.error(.invalidJSON(json: nil)))
@@ -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<Int>, 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."
@@ -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
+8 -8
View File
@@ -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 = "<group>"; };
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
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 = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-HxN15V9QHR7IK0X8ySw3WC.a"; sourceTree = "<group>"; };
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 = "<group>"; };
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a"; sourceTree = "<group>"; };
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
+4 -4
View File
@@ -111,8 +111,8 @@ public func resetChatCtrl() {
}
@inline(__always)
public func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult<R> {
if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) {
public func sendSimpleXCmd<R: ChatAPIResult>(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil, retryNum: Int32 = 0) -> APIResult<R> {
if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl, retryNum: retryNum) {
decodeAPIResult(d)
} else {
APIResult.error(.invalidJSON(json: nil))
@@ -120,9 +120,9 @@ public func sendSimpleXCmd<R: ChatAPIResult>(_ 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
+11 -6
View File
@@ -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"
+22 -8
View File
@@ -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)
+1 -8
View File
@@ -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<A: View>(
@@ -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 {
+1 -1
View File
@@ -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);
@@ -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;
}
@@ -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;
}
@@ -149,8 +149,10 @@ class AppPreferences {
val networkHostMode: SharedPreference<HostMode> = mkSafeEnumPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.default)
val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false)
val networkSMPWebPortServers: SharedPreference<SMPWebPortServers> = 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<Long> {
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<Long> =
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<StringResource, String>, 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<StringResource, String>? {
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<API>(rStr)
@@ -1208,16 +1295,16 @@ object ChatController {
}
suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair<RcvMsgInfo?, ServerQueueInfo>? {
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<RcvMsgInfo?, ServerQueueInfo>? {
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<Pair<CreatedConnLink, PendingContactConnection>?, (() -> 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<CreatedConnLink, ConnectionPlan>? {
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 = "",
@@ -22,8 +22,8 @@ external fun pipeStdOutToSocket(socketName: String) : Int
typealias ChatCtrl = Long
external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any>
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
@@ -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<SMPProxyMode>,
smpProxyFallback: MutableState<SMPProxyFallback>,
smpWebPortServers: MutableState<SMPWebPortServers>,
networkTCPConnectTimeout: MutableState<Long>,
networkTCPTimeout: MutableState<Long>,
networkTCPConnectTimeoutInteractive: MutableState<Long>,
networkTCPConnectTimeoutBackground: MutableState<Long>,
networkTCPTimeoutInteractive: MutableState<Long>,
networkTCPTimeoutBackground: MutableState<Long>,
networkTCPTimeoutPerKb: MutableState<Long>,
networkRcvConcurrency: MutableState<Int>,
networkSMPPingInterval: MutableState<Long>,
@@ -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) },
@@ -147,7 +147,9 @@
<string name="network_error_desc">Please check your network connection with %1$s and try again.</string>
<string name="network_error_broker_host_desc">Server address is incompatible with network settings: %1$s.</string>
<string name="network_error_broker_version_desc">Server version is incompatible with your app: %1$s.</string>
<string name="private_routing_timeout">Private routing timeout</string>
<string name="private_routing_error">Private routing error</string>
<string name="private_routing_no_session">No private routing session</string>
<string name="smp_proxy_error_connecting">Error connecting to forwarding server %1$s. Please try later.</string>
<string name="smp_proxy_error_broker_host">Forwarding server address is incompatible with network settings: %1$s.</string>
<string name="smp_proxy_error_broker_version">Forwarding server version is incompatible with network settings: %1$s.</string>
@@ -1976,7 +1978,9 @@
<string name="network_options_reset_to_defaults">Reset to defaults</string>
<string name="network_option_seconds_label">sec</string>
<string name="network_option_tcp_connection_timeout">TCP connection timeout</string>
<string name="network_option_tcp_connection_timeout_background">TCP connection bg timeout</string>
<string name="network_option_protocol_timeout">Protocol timeout</string>
<string name="network_option_protocol_timeout_background">Protocol background timeout</string>
<string name="network_option_protocol_timeout_per_kb">Protocol timeout per KB</string>
<string name="network_option_rcv_concurrency">Receiving concurrency</string>
<string name="network_option_ping_interval">PING interval</string>
+1 -2
View File
@@ -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 ()
+1 -1
View File
@@ -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
+4
View File
@@ -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"
+2
View File
@@ -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
+1 -1
View File
@@ -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";
+3 -3
View File
@@ -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
+126 -131
View File
@@ -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
+4 -4
View File
@@ -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
+25 -9
View File
@@ -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
+14 -14
View File
@@ -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}
+4 -4
View File
@@ -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
+2 -3
View File
@@ -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
+3 -4
View File
@@ -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)
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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)