diff --git a/README.md b/README.md index 554c6068d9..835eb185ac 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,8 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Jul 3, 2025 SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) + [Mar 8, 2025. SimpleX Chat v6.3: new user experience and safety in public groups](./blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.md) [Jan 14, 2025. SimpleX network: large groups and privacy-preserving content moderation](./blog/20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 64fba806a9..5959562a96 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -1053,6 +1053,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact) case networkStatus(networkStatus: NetworkStatus, connections: [String]) case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) + case chatInfoUpdated(user: UserRef, chatInfo: ChatInfo) case newChatItems(user: UserRef, chatItems: [AChatItem]) case chatItemsStatusesUpdated(user: UserRef, chatItems: [AChatItem]) case chatItemUpdated(user: UserRef, chatItem: AChatItem) @@ -1131,6 +1132,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case .contactsMerged: "contactsMerged" case .networkStatus: "networkStatus" case .networkStatuses: "networkStatuses" + case .chatInfoUpdated: "chatInfoUpdated" case .newChatItems: "newChatItems" case .chatItemsStatusesUpdated: "chatItemsStatusesUpdated" case .chatItemUpdated: "chatItemUpdated" @@ -1204,6 +1206,7 @@ enum ChatEvent: Decodable, ChatAPIResult { case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)") case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) + case let .chatInfoUpdated(u, chatInfo): return withUser(u, String(describing: chatInfo)) case let .newChatItems(u, chatItems): let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n") return withUser(u, itemsString) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 9dd7fb86d9..5e926a9392 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -94,14 +94,14 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, return try apiResult(res) } -func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) -> APIResult { +func chatApiSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) -> APIResult { if log { logger.debug("chatSendCmd \(cmd.cmdType)") } let start = Date.now let resp: APIResult = bgTask - ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } - : sendSimpleXCmd(cmd, ctrl) + ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) } + : sendSimpleXCmd(cmd, ctrl, retryNum: retryNum) if log { logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") if case let .invalid(_, json) = resp { @@ -120,10 +120,98 @@ func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDe return try apiResult(res) } +func chatApiSendCmdWithRetry(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, retryNum: Int32 = 0) async -> APIResult? { + let r: APIResult = await chatApiSendCmd(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum) + if case let .error(e) = r, let alert = retryableNetworkErrorAlert(e) { + return await withCheckedContinuation { cont in + showRetryAlert( + alert, + onCancel: { _ in + cont.resume(returning: nil) + }, + onRetry: { + let r1: APIResult? = await chatApiSendCmdWithRetry(cmd, bgTask: bgTask, bgDelay: bgDelay, retryNum: retryNum + 1) + cont.resume(returning: r1) + } + ) + } + } else { + return r + } +} + @inline(__always) -func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, log: Bool = true) async -> APIResult { +func showRetryAlert(_ alert: (title: String, message: String), onCancel: @escaping (UIAlertAction) -> Void, onRetry: @escaping () async -> Void) { + DispatchQueue.main.async { + showAlert( + alert.title, + message: alert.message, + actions: {[ + UIAlertAction( + title: NSLocalizedString("Cancel", comment: "alert action"), + style: .cancel, + handler: onCancel + ), + UIAlertAction( + title: NSLocalizedString("Retry", comment: "alert action"), + style: .default, + handler: { _ in Task(operation: onRetry) } + ) + ]} + ) + } +} + +func retryableNetworkErrorAlert(_ e: ChatError) -> (title: String, message: String)? { + switch e { + case let .errorAgent(.BROKER(addr, .TIMEOUT)): ( + title: NSLocalizedString("Connection timeout", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.BROKER(addr, .NETWORK)): ( + title: NSLocalizedString("Connection error", comment: "alert title"), + message: serverErrorAlertMessage(addr) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.TIMEOUT)))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.SMP(serverAddress, .PROXY(.BROKER(.NETWORK)))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyErrorAlertMessage(serverAddress) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.TIMEOUT))))): ( + title: NSLocalizedString("Private routing timeout", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.BROKER(.NETWORK))))): ( + title: NSLocalizedString("Private routing error", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + case let .errorAgent(.PROXY(proxyServer, destServer, .protocolError(.PROXY(.NO_SESSION)))): ( + title: NSLocalizedString("No private routing session", comment: "alert title"), + message: proxyDestinationErrorAlertMessage(proxyServer: proxyServer, destServer: destServer) + ) + default: nil + } +} + +func serverErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Please check your network connection with %@ and try again.", comment: "alert message"), serverHostname(addr)) +} + +func proxyErrorAlertMessage(_ addr: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Error connecting to forwarding server %@. Please try later.", comment: "alert message"), serverHostname(addr)) +} + +func proxyDestinationErrorAlertMessage(proxyServer: String, destServer: String) -> String { + String.localizedStringWithFormat(NSLocalizedString("Forwarding server %1$@ failed to connect to destination server %2$@. Please try later.", comment: "alert message"), serverHostname(proxyServer), serverHostname(destServer)) +} + +@inline(__always) +func chatApiSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, ctrl: chat_ctrl? = nil, retryNum: Int32 = 0, log: Bool = true) async -> APIResult { await withCheckedContinuation { cont in - cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, log: log)) + cont.resume(returning: chatApiSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl: ctrl, retryNum: retryNum, log: log)) } } @@ -795,16 +883,16 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws - throw r.unexpected } -func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r: ChatResponse0 = try await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r.unexpected +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiContactQueueInfo(contactId: contactId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } -func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo) { - let r: ChatResponse0 = try await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) - if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } - throw r.unexpected +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, ServerQueueInfo)? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .result(.queueInfo(_, rcvMsgInfo, queueInfo)) = r { return (rcvMsgInfo, queueInfo) } + if let r { throw r.unexpected } else { return nil } } func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { @@ -874,9 +962,9 @@ func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactCo logger.error("apiAddContact: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) if case let .result(.invitation(_, connLinkInv, connection)) = r { return ((connLinkInv, connection), nil) } - let alert = connectionErrorAlert(r) + let alert: Alert? = if let r { connectionErrorAlert(r) } else { nil } return (nil, alert) } @@ -886,10 +974,10 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P throw r.unexpected } -func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection { - let r: ChatResponse1 = try await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) - if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} - throw r.unexpected +func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiChangeConnectionUser(connId: connId, userId: userId)) + if case let .result(.connectionUserChanged(_, _, toConnection, _)) = r {return toConnection} + if let r { throw r.unexpected } else { return nil } } func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) { @@ -897,9 +985,10 @@ func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan logger.error("apiConnectPlan: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnectPlan(userId: userId, connLink: connLink)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPlan(userId: userId, connLink: connLink)) if case let .result(.connectionPlan(_, connLink, connPlan)) = r { return ((connLink, connPlan), nil) } - return (nil, apiConnectResponseAlert(r)) + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) } func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? { @@ -917,7 +1006,7 @@ func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqT logger.error("apiConnect: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnect(userId: userId, incognito: incognito, connLink: connLink)) let m = ChatModel.shared switch r { case let .result(.sentConfirmation(_, connection)): @@ -932,7 +1021,8 @@ func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqT return (nil, alert) default: () } - return (nil, apiConnectResponseAlert(r)) + let alert: Alert? = if let r { apiConnectResponseAlert(r) } else { nil } + return (nil, alert) } private func apiConnectResponseAlert(_ r: APIResult) -> Alert { @@ -1026,16 +1116,16 @@ func apiChangePreparedGroupUser(groupId: Int64, newUserId: Int64) async throws - } func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent?) async -> Contact? { - let r: APIResult = await chatApiSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToContact(_, contact)) = r { return contact } - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } return nil } func apiConnectPreparedGroup(groupId: Int64, incognito: Bool, msg: MsgContent?) async -> GroupInfo? { - let r: APIResult = await chatApiSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito, msg: msg)) if case let .result(.startedConnectionToGroup(_, groupInfo)) = r { return groupInfo } - AlertManager.shared.showAlert(apiConnectResponseAlert(r)) + if let r { AlertManager.shared.showAlert(apiConnectResponseAlert(r)) } return nil } @@ -1044,11 +1134,14 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co logger.error("apiConnectContactViaAddress: no current user") return (nil, nil) } - let r: APIResult = await chatApiSendCmd(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiConnectContactViaAddress(userId: userId, incognito: incognito, contactId: contactId)) if case let .result(.sentInvitationToContact(_, contact, _)) = r { return (contact, nil) } - logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") - let alert = connectionErrorAlert(r) - return (nil, alert) + if let r { + logger.error("apiConnectContactViaAddress error: \(responseError(r.unexpected))") + return (nil, connectionErrorAlert(r)) + } else { + return (nil, nil) + } } func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { @@ -1214,18 +1307,18 @@ func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bo } -func apiCreateUserAddress() async throws -> CreatedConnLink { +func apiCreateUserAddress() async throws -> CreatedConnLink? { let userId = try currentUserId("apiCreateUserAddress") - let r: ChatResponse1 = try await chatSendCmd(.apiCreateMyAddress(userId: userId)) - if case let .userContactLinkCreated(_, connLink) = r { return connLink } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateMyAddress(userId: userId)) + if case let .result(.userContactLinkCreated(_, connLink)) = r { return connLink } + if let r { throw r.unexpected } else { return nil } } func apiDeleteUserAddress() async throws -> User? { let userId = try currentUserId("apiDeleteUserAddress") - let r: ChatResponse1 = try await chatSendCmd(.apiDeleteMyAddress(userId: userId)) - if case let .userContactLinkDeleted(user) = r { return user } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteMyAddress(userId: userId)) + if case let .result(.userContactLinkDeleted(user)) = r { return user } + if let r { throw r.unexpected } else { return nil } } func apiGetUserAddress() throws -> UserContactLink? { @@ -1246,25 +1339,25 @@ private func userAddressResponse(_ r: APIResult) throws -> UserCo } } -func apiAddMyAddressShortLink() async throws -> UserContactLink { +func apiAddMyAddressShortLink() async throws -> UserContactLink? { let userId = try currentUserId("apiAddMyAddressShortLink") - let r: ChatResponse1 = try await chatSendCmd(.apiAddMyAddressShortLink(userId: userId)) - if case let .userContactLink(_, contactLink) = r { return contactLink } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddMyAddressShortLink(userId: userId)) + if case let .result(.userContactLink(_, contactLink)) = r { return contactLink } + if let r { throw r.unexpected } else { return nil } } func apiSetUserAddressSettings(_ settings: AddressSettings) async throws -> UserContactLink? { let userId = try currentUserId("apiSetUserAddressSettings") - let r: APIResult = await chatApiSendCmd(.apiSetAddressSettings(userId: userId, addressSettings: settings)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiSetAddressSettings(userId: userId, addressSettings: settings)) switch r { case let .result(.userContactLinkUpdated(_, contactLink)): return contactLink case .error(.errorStore(storeError: .userContactLinkNotFound)): return nil - default: throw r.unexpected + default: if let r { throw r.unexpected } else { return nil } } } func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { - let r: APIResult = await chatApiSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) let am = AlertManager.shared if case let .result(.acceptingContactRequest(_, contact)) = r { return contact } @@ -1273,14 +1366,16 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." ) - } else if let networkErrorAlert = networkErrorAlert(r) { - am.showAlert(networkErrorAlert) - } else { - logger.error("apiAcceptContactRequest error: \(String(describing: r))") - am.showAlertMsg( - title: "Error accepting contact request", - message: "Error: \(responseError(r.unexpected))" - ) + } else if let r { + if let networkErrorAlert = networkErrorAlert(r) { + am.showAlert(networkErrorAlert) + } else { + logger.error("apiAcceptContactRequest error: \(String(describing: r))") + am.showAlertMsg( + title: "Error accepting contact request", + message: "Error: \(responseError(r.unexpected))" + ) + } } return nil } @@ -1693,13 +1788,13 @@ enum JoinGroupResult { case groupNotFound } -func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { - let r: APIResult = await chatApiSendCmd(.apiJoinGroup(groupId: groupId)) +func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiJoinGroup(groupId: groupId)) switch r { case let .result(.userAcceptedGroupSent(_, groupInfo, _)): return .joined(groupInfo: groupInfo) case .error(.errorAgent(.SMP(_, .AUTH))): return .invitationRemoved case .error(.errorStore(.groupNotFound)): return .groupNotFound - default: throw r.unexpected + default: if let r { throw r.unexpected } else { return nil } } } @@ -1769,10 +1864,10 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws throw r.unexpected } -func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink { - let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) - if case let .groupLinkCreated(_, _, groupLink) = r { return groupLink } - throw r.unexpected +func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) + if case let .result(.groupLinkCreated(_, _, groupLink)) = r { return groupLink } + if let r { throw r.unexpected } else { return nil } } func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink { @@ -1782,9 +1877,9 @@ func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .mem } func apiDeleteGroupLink(_ groupId: Int64) async throws { - let r: ChatResponse2 = try await chatSendCmd(.apiDeleteGroupLink(groupId: groupId)) - if case .groupLinkDeleted = r { return } - throw r.unexpected + let r: APIResult? = await chatApiSendCmdWithRetry(.apiDeleteGroupLink(groupId: groupId)) + if case .result(.groupLinkDeleted) = r { return } + if let r { throw r.unexpected } } func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? { @@ -1798,10 +1893,10 @@ func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? { } } -func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink { - let r: ChatResponse2 = try await chatSendCmd(.apiAddGroupShortLink(groupId: groupId)) - if case let .groupLink(_, _, groupLink) = r { return groupLink } - throw r.unexpected +func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink? { + let r: APIResult? = await chatApiSendCmdWithRetry(.apiAddGroupShortLink(groupId: groupId)) + if case let .result(.groupLink(_, _, groupLink)) = r { return groupLink } + if let r { throw r.unexpected } else { return nil } } func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { @@ -2208,6 +2303,12 @@ func processReceivedMsg(_ res: ChatEvent) async { n.networkStatuses = ns } } + case let .chatInfoUpdated(user, chatInfo): + if active(user) { + await MainActor.run { + m.updateChatInfo(chatInfo) + } + } case let .newChatItems(user, chatItems): for chatItem in chatItems { let cInfo = chatItem.chatInfo diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index dee034b5a1..c8cb131e2e 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -274,8 +274,9 @@ struct ChatInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiContactQueueInfo(chat.chatInfo.apiId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index f9dbaede63..02cdad715e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -49,11 +49,7 @@ func openBrowserAlert(uri: URL) { NSLocalizedString("Open link?", comment: "alert title"), message: uri.absoluteString, actions: {[ - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, - handler: { _ in } - ), + cancelAlertAction, UIAlertAction( title: NSLocalizedString("Open", comment: "alert action"), style: .default, diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 4080605eef..64166929aa 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -778,7 +778,8 @@ struct ChatView: View { } case let .group(groupInfo, _): switch (groupInfo.membership.memberStatus) { - case .memAccepted: "connecting…" // TODO [short links] add member status to show transition from prepared group to started connection earlier? + case .memUnknown: groupInfo.preparedGroup?.connLinkStartedConnection == true ? "connecting…" : nil + case .memAccepted: "connecting…" default: nil } default: nil diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 5894a9c923..cc31ec1855 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -350,12 +350,13 @@ struct ComposeView: View { @UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false var body: some View { VStack(spacing: 0) { Divider() - let contact = chat.chatInfo.contact - if (contact?.nextConnectPrepared ?? false) || (chat.chatInfo.groupInfo?.nextConnectPrepared ?? false), + + if chat.chatInfo.nextConnectPrepared, let user = chatModel.currentUser { ContextProfilePickerView( chat: chat, @@ -404,6 +405,8 @@ struct ComposeView: View { default: previewView() } + let contact = chat.chatInfo.contact + if chat.chatInfo.groupInfo?.nextConnectPrepared == true { if chat.chatInfo.groupInfo?.businessChat == nil { Button(action: connectPreparedGroup) { @@ -744,7 +747,8 @@ struct ComposeView: View { await MainActor.run { hideKeyboard() } await sending() let mc = connectCheckLinkPreview() - if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) { + let incognito = chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault + if let contact = await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: incognito, msg: mc) { await MainActor.run { self.chatModel.updateContact(contact) NetworkModel.shared.setContactNetworkStatus(contact, .connected) @@ -761,7 +765,8 @@ struct ComposeView: View { await MainActor.run { hideKeyboard() } await sending() let mc = connectCheckLinkPreview() - if let groupInfo = await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognitoGroupDefault.get(), msg: mc) { + let incognito = chat.chatInfo.profileChangeProhibited ? chat.chatInfo.incognito : incognitoDefault + if let groupInfo = await apiConnectPreparedGroup(groupId: chat.chatInfo.apiId, incognito: incognito, msg: mc) { await MainActor.run { self.chatModel.updateGroup(groupInfo) clearState() diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift index 21fa55f493..143bf42ea4 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextPendingMemberActionsView.swift @@ -72,10 +72,7 @@ func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismis acceptMember(groupInfo, member, .observer, dismiss: dismiss) } ), - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default - ) + cancelAlertAction ]} ) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift index 2e0b89020c..427a600627 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextProfilePickerView.swift @@ -38,7 +38,7 @@ struct ContextProfilePickerView: View { private func viewBody() -> some View { Group { - if !listExpanded { + if !listExpanded || chat.chatInfo.profileChangeProhibited { currentSelection() } else { profilePicker() @@ -59,7 +59,13 @@ struct ContextProfilePickerView: View { .padding(.leading, 12) .padding(.trailing) - if incognitoDefault { + if chat.chatInfo.profileChangeProhibited { + if chat.chatInfo.incognito { + incognitoOption() + } else { + profilerPickerUserOption(selectedUser) + } + } else if incognitoDefault { incognitoOption() } else { profilerPickerUserOption(selectedUser) @@ -140,15 +146,19 @@ struct ContextProfilePickerView: View { private func profilerPickerUserOption(_ user: User) -> some View { Button { - if selectedUser == user { - if !incognitoDefault { - listExpanded.toggle() - } else { - incognitoDefault = false - listExpanded = false + if !chat.chatInfo.profileChangeProhibited { + if selectedUser == user { + if !incognitoDefault { + listExpanded.toggle() + } else { + incognitoDefault = false + listExpanded = false + } + } else if selectedUser != user { + changeProfile(user) } - } else if selectedUser != user { - changeProfile(user) + } else { + showCantChangeProfileAlert() } } label: { HStack { @@ -166,7 +176,7 @@ struct ContextProfilePickerView: View { .font(.system(size: 12, weight: .bold)) .foregroundColor(theme.colors.secondary) .opacity(0.7) - } else { + } else if !chat.chatInfo.profileChangeProhibited { Image(systemName: "chevron.up") .font(.system(size: 12, weight: .bold)) .foregroundColor(theme.colors.secondary) @@ -226,11 +236,15 @@ struct ContextProfilePickerView: View { private func incognitoOption() -> some View { Button { - if incognitoDefault { - listExpanded.toggle() + if !chat.chatInfo.profileChangeProhibited { + if incognitoDefault { + listExpanded.toggle() + } else { + incognitoDefault = true + listExpanded = false + } } else { - incognitoDefault = true - listExpanded = false + showCantChangeProfileAlert() } } label : { HStack { @@ -253,7 +267,7 @@ struct ContextProfilePickerView: View { .font(.system(size: 12, weight: .bold)) .foregroundColor(theme.colors.secondary) .opacity(0.7) - } else { + } else if !chat.chatInfo.profileChangeProhibited { Image(systemName: "chevron.up") .font(.system(size: 12, weight: .bold)) .foregroundColor(theme.colors.secondary) @@ -274,6 +288,13 @@ struct ContextProfilePickerView: View { .frame(width: 38) .foregroundColor(.indigo) } + + private func showCantChangeProfileAlert() { + showAlert( + NSLocalizedString("Can't change profile", comment: "alert title"), + message: NSLocalizedString("To use another profile after connection attempt, delete the chat and use the link again.", comment: "alert message") + ) + } } #Preview { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 5b0d21a6d8..275b8dd357 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -197,8 +197,9 @@ struct GroupMemberInfoView: View { Button ("Debug delivery") { Task { do { - let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) - await MainActor.run { alert = .queueInfo(info: info) } + if let info = try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId) { + await MainActor.run { alert = .queueInfo(info: queueInfoText(info)) } + } } catch let e { logger.error("apiContactQueueInfo error: \(responseError(e))") let a = getErrorAlert(e, "Error") diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index b43e9889da..fc151d4889 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -702,16 +702,17 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { Task { logger.debug("joinGroup") do { - let r = try await apiJoinGroup(groupId) - switch r { - case let .joined(groupInfo): - await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } - case .invitationRemoved: - AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") - await deleteGroup() - case .groupNotFound: - AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") - await deleteGroup() + if let r = try await apiJoinGroup(groupId) { + switch r { + case let .joined(groupInfo): + await MainActor.run { ChatModel.shared.updateGroup(groupInfo) } + case .invitationRemoved: + AlertManager.shared.showAlertMsg(title: "Invitation expired!", message: "Group invitation is no longer valid, it was removed by sender.") + await deleteGroup() + case .groupNotFound: + AlertManager.shared.showAlertMsg(title: "No group!", message: "This group no longer exists.") + await deleteGroup() + } } await onComplete() } catch let error { diff --git a/apps/ios/Shared/Views/ChatList/TagListView.swift b/apps/ios/Shared/Views/ChatList/TagListView.swift index 2063fe15de..79d122eabf 100644 --- a/apps/ios/Shared/Views/ChatList/TagListView.swift +++ b/apps/ios/Shared/Views/ChatList/TagListView.swift @@ -63,10 +63,7 @@ struct TagListView: View { NSLocalizedString("Delete list?", comment: "alert title"), message: String.localizedStringWithFormat(NSLocalizedString("All chats will be removed from the list %@, and the list deleted.", comment: "alert message"), text), actions: {[ - UIAlertAction( - title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default - ), + cancelAlertAction, UIAlertAction( title: NSLocalizedString("Delete", comment: "alert action"), style: .destructive, diff --git a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift index 09aae1cb15..32fc541604 100644 --- a/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift +++ b/apps/ios/Shared/Views/Helpers/CustomTimePicker.swift @@ -220,6 +220,29 @@ struct DropdownCustomTimePicker: View { } } +struct WrappedPicker: View { + var title: LocalizedStringKey + var selection: Binding + @ViewBuilder var content: () -> Content + + init(_ title: LocalizedStringKey, selection: Binding, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.selection = selection + self.content = content + } + + var body: some View { + HStack(alignment: .firstTextBaseline) { + Text(title).lineLimit(2) + Spacer() + Picker(selection: selection, content: content) { + EmptyView() + } + .frame(height: 36) + } + } +} + struct CustomTimePicker_Previews: PreviewProvider { static var previews: some View { CustomTimePicker( diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index c94a1a8f18..cadd551147 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -415,8 +415,8 @@ private struct ActiveProfilePicker: View { } Task { do { - if let contactConn = contactConnection { - let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) + if let contactConn = contactConnection, + let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { await MainActor.run { contactConnection = conn connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil) diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index 62eaa19071..73d8a8d370 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -77,9 +77,10 @@ struct CreateSimpleXAddress: View { progressIndicator = true Task { do { - let connLinkContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + if let connLinkContact = try await apiCreateUserAddress() { + DispatchQueue.main.async { + m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + } } await MainActor.run { progressIndicator = false } } catch let error { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 554219eb69..ac143fe044 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -167,7 +167,7 @@ struct TerminalView: View { func sendTerminalCmd(_ cmd: String) async { let start: Date = .now await withCheckedContinuation { (cont: CheckedContinuation) in - let d = sendSimpleXCmdStr(cmd) + let d = sendSimpleXCmdStr(cmd, retryNum: 0) Task { guard let d else { await TerminalItems.shared.addCommand(start, ChatCommand.string(cmd), APIResult.error(.invalidJSON(json: nil))) diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift index fa698f8b7c..7bcf542701 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/AdvancedNetworkSettings.swift @@ -67,7 +67,7 @@ struct AdvancedNetworkSettings: View { Text(netCfg.smpProxyMode.label) } } - + NavigationLink { List { Section { @@ -192,7 +192,7 @@ struct AdvancedNetworkSettings: View { netCfg.requiredHostMode = requiredHostMode } } - + if developerTools { Section { Picker("Transport isolation", selection: $netCfg.sessionMode) { @@ -220,10 +220,12 @@ struct AdvancedNetworkSettings: View { ? Text("Use TCP port 443 for preset servers only.") : Text("Use TCP port \(netCfg.smpWebPortServers == .all ? "443" : "5223") when no port is specified.") } - + Section("TCP connection") { - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout.interactiveTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) + timeoutSettingPicker("TCP connection bg timeout", selection: $netCfg.tcpConnectTimeout.backgroundTimeout, values: [30_000000, 45_000000, 60_000000, 90_000000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout.interactiveTimeout, values: [5_000000, 7_000000, 10_000000, 15_000000, 20_000000], label: secondsLabel) + timeoutSettingPicker("Protocol background timeout", selection: $netCfg.tcpTimeout.backgroundTimeout, values: [15_000000, 20_000000, 30_000000, 45_000000, 60_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) // intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) @@ -243,7 +245,7 @@ struct AdvancedNetworkSettings: View { .foregroundColor(theme.colors.secondary) } } - + Section { Button("Reset to defaults") { updateNetCfgView(NetCfg.defaults, NetworkProxy.def) @@ -254,7 +256,7 @@ struct AdvancedNetworkSettings: View { updateNetCfgView(netCfg.withProxyTimeouts, netProxy) } .disabled(netCfg.hasProxyTimeouts) - + Button("Save and reconnect") { showSettingsAlert = .update } @@ -351,16 +353,15 @@ struct AdvancedNetworkSettings: View { } private func timeoutSettingPicker(_ title: LocalizedStringKey, selection: Binding, values: [Int], label: String) -> some View { - Picker(title, selection: selection) { + WrappedPicker(title, selection: selection) { let v = selection.wrappedValue let vs = values.contains(v) ? values : values + [v] ForEach(vs, id: \.self) { value in Text("\(String(format: "%g", (Double(value) / 1000000))) \(secondsLabel)") } } - .frame(height: 36) } - + private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { switch hosts { case .no: return "Onion hosts will not be used." @@ -378,7 +379,7 @@ struct AdvancedNetworkSettings: View { case .entity: Text("A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.") } } - + private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey { switch mode { case .always: return "Always use private routing." @@ -387,7 +388,7 @@ struct AdvancedNetworkSettings: View { case .never: return "Do NOT use private routing." } } - + private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { switch proxyFallback { case .allow: return "Send messages directly when your or destination server does not support private routing." diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 3d554fc5cc..46a9083436 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -198,11 +198,12 @@ struct UserAddressView: View { progressIndicator = true Task { do { - let connLinkContact = try await apiCreateUserAddress() - DispatchQueue.main.async { - chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) - alert = .shareOnCreate - progressIndicator = false + if let connLinkContact = try await apiCreateUserAddress() { + DispatchQueue.main.async { + chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: connLinkContact.connShortLink != nil, addressSettings: AddressSettings(businessAddress: false)) + alert = .shareOnCreate + progressIndicator = false + } } } catch let error { logger.error("UserAddressView apiCreateUserAddress: \(responseError(error))") @@ -488,7 +489,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on @@ -510,7 +511,7 @@ struct UserAddressSettingsView: View { actions: {[ UIAlertAction( title: NSLocalizedString("Cancel", comment: "alert action"), - style: .default, + style: .cancel, handler: { _ in ignoreShareViaProfileChange = true shareViaProfile = !on diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index d48dccc136..a36ca4472a 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -1356,6 +1356,10 @@ swipe action Обаждането на члена не е позволено No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Не може да покани контакта! @@ -7688,6 +7692,10 @@ You will be prompted to complete authentication before this feature is enabled.< За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 82edb2559e..a8c5abae88 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -1286,6 +1286,10 @@ swipe action Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Nelze pozvat kontakt! @@ -7441,6 +7445,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Pro podporu doručování okamžitých upozornění musí být přenesena chat databáze. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index aef203721c..a9c28f9154 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -1393,6 +1393,10 @@ swipe action Mitglied kann nicht angerufen werden No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kontakt kann nicht eingeladen werden! @@ -8160,6 +8164,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Um sofortige Push-Benachrichtigungen zu unterstützen, muss die Chat-Datenbank migriert werden. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Um die Server von **%@** zu nutzen, müssen Sie dessen Nutzungsbedingungen akzeptieren. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 51ce08b521..b7d7d24ee5 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -1396,6 +1396,11 @@ swipe action Can't call member No comment provided by engineer. + + Can't change profile + Can't change profile + alert title + Can't invite contact! Can't invite contact! @@ -8186,6 +8191,11 @@ You will be prompted to complete authentication before this feature is enabled.< To support instant push notifications the chat database has to be migrated. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. To use the servers of **%@**, accept conditions of use. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index fc0d044d21..09ad3d74b1 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -1393,6 +1393,10 @@ swipe action No se puede llamar al miembro No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! ¡No se puede invitar el contacto! @@ -8160,6 +8164,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.Para permitir las notificaciones automáticas instantáneas, la base de datos se debe migrar. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Para usar los servidores de **%@**, debes aceptar las condiciones de uso. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 97baa3bda4..0f2169ad05 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -1265,6 +1265,10 @@ swipe action Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kontaktia ei voi kutsua! @@ -7413,6 +7417,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index c485f95460..235441e46c 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -1388,6 +1388,10 @@ swipe action Impossible d'appeler le membre No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Impossible d'inviter le contact ! @@ -8069,6 +8073,10 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour prendre en charge les notifications push instantanées, la base de données du chat doit être migrée. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Pour utiliser les serveurs de **%@**, acceptez les conditions d'utilisation. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 41488f2b98..4af99d6724 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -1393,6 +1393,10 @@ swipe action Nem lehet felhívni a tagot No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Nem lehet meghívni a partnert! @@ -8160,6 +8164,10 @@ A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beáll Az azonnali push-értesítések támogatásához a csevegési adatbázis átköltöztetése szükséges. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. A(z) **%@** kiszolgálóinak használatához fogadja el a használati feltételeket. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 01d586464b..8ed9ed2e7c 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -1393,6 +1393,10 @@ swipe action Impossibile chiamare il membro No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Impossibile invitare il contatto! @@ -8160,6 +8164,10 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per supportare le notifiche push istantanee, il database della chat deve essere migrato. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Per usare i server di **%@**, accetta le condizioni d'uso. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 3dca9e8928..3ff281d1e6 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -1315,6 +1315,10 @@ swipe action Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! 連絡先を招待できません! @@ -7483,6 +7487,10 @@ You will be prompted to complete authentication before this feature is enabled.< インスタント プッシュ通知をサポートするには、チャット データベースを移行する必要があります。 No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index ebc0f739a0..02c6ae36df 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -1393,6 +1393,10 @@ swipe action Kan lid niet bellen No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kan contact niet uitnodigen! @@ -8160,6 +8164,10 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Om de servers van **%@** te gebruiken, moet u de gebruiksvoorwaarden accepteren. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 77da688b02..3c7f894acc 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -1384,6 +1384,10 @@ swipe action Nie można zadzwonić do członka No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Nie można zaprosić kontaktu! @@ -7949,6 +7953,10 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby obsługiwać natychmiastowe powiadomienia push, należy zmigrować bazę danych czatu. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index e82d982923..c76fa2c379 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -1394,6 +1394,10 @@ swipe action Не удаётся позвонить члену группы No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Нельзя пригласить контакт! @@ -8163,6 +8167,10 @@ You will be prompted to complete authentication before this feature is enabled.< Для поддержки мгновенный доставки уведомлений данные чата должны быть перемещены. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Чтобы использовать серверы оператора **%@**, примите условия использования. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index cf7bd4cdf3..1d9c2455dc 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -1257,6 +1257,10 @@ swipe action Can't call member No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! ไม่สามารถเชิญผู้ติดต่อได้! @@ -7385,6 +7389,10 @@ You will be prompted to complete authentication before this feature is enabled.< เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index b64aefebc7..ca18f03de9 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -1375,6 +1375,10 @@ swipe action Üye aranamaz No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Kişi davet edilemiyor! @@ -7974,6 +7978,10 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Anlık anlık bildirimleri desteklemek için sohbet veritabanının taşınması gerekir. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 37ce187558..d17fbbbbe2 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -1393,6 +1393,10 @@ swipe action Не вдається зателефонувати користувачеві No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! Не вдається запросити контакт! @@ -8093,6 +8097,10 @@ You will be prompted to complete authentication before this feature is enabled.< Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату. No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. Щоб користуватися серверами **%@**, прийміть умови використання. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index caea28c92b..33e1811e74 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -1390,6 +1390,10 @@ swipe action 无法呼叫成员 No comment provided by engineer. + + Can't change profile + alert title + Can't invite contact! 无法邀请联系人! @@ -8071,6 +8075,10 @@ You will be prompted to complete authentication before this feature is enabled.< 为了支持即时推送通知,聊天数据库必须被迁移。 No comment provided by engineer. + + To use another profile after connection attempt, delete the chat and use the link again. + alert message + To use the servers of **%@**, accept conditions of use. No comment provided by engineer. diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 176da2481e..efed487739 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -996,7 +996,11 @@ func receivedMsgNtf(_ res: NSEChatEvent) async -> (String, NSENotificationData)? // case let .contactConnecting(contact): // TODO profile update case let .receivedContactRequest(user, contactRequest): - return (UserContact(contactRequest: contactRequest).id, .contactRequest(user, contactRequest)) + if let userContactLinkId = contactRequest.userContactLinkId_ { + return (UserContact(userContactLinkId: userContactLinkId).id, .contactRequest(user, contactRequest)) + } else { + return nil + } case let .newChatItems(user, chatItems): // Received items are created one at a time if let chatItem = chatItems.first { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index d12b071d41..e57fc17e44 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -183,8 +183,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.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.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 */; }; @@ -553,8 +553,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.5-B9nBut3z2uE2oMf1gYzlJ.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -714,8 +714,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.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; @@ -800,8 +800,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.4-DWeDI2Xa9F86P3Q5uUF2Wy.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 = ""; @@ -2005,7 +2005,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2055,7 +2055,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2097,7 +2097,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2117,7 +2117,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2142,7 +2142,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2179,7 +2179,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2216,7 +2216,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2267,7 +2267,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2318,7 +2318,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2352,7 +2352,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 283; + CURRENT_PROJECT_VERSION = 287; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 0dd3483fd7..92a2267656 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -111,8 +111,8 @@ public func resetChatCtrl() { } @inline(__always) -public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil) -> APIResult { - if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl) { +public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: chat_ctrl? = nil, retryNum: Int32 = 0) -> APIResult { + if let d = sendSimpleXCmdStr(cmd.cmdString, ctrl, retryNum: retryNum) { decodeAPIResult(d) } else { APIResult.error(.invalidJSON(json: nil)) @@ -120,9 +120,9 @@ public func sendSimpleXCmd(_ cmd: ChatCmdProtocol, _ ctrl: cha } @inline(__always) -public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil) -> Data? { +public func sendSimpleXCmdStr(_ cmd: String, _ ctrl: chat_ctrl? = nil, retryNum: Int32) -> Data? { var c = cmd.cString(using: .utf8)! - return if let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c) { + return if let cjson = chat_send_cmd_retry(ctrl ?? getChatCtrl(), &c, retryNum) { dataFromCString(cjson) } else { nil diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index aa0055fa70..5335a04ae3 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -241,8 +241,8 @@ public struct NetCfg: Codable, Equatable { public var smpProxyMode: SMPProxyMode = .always public var smpProxyFallback: SMPProxyFallback = .allowProtected public var smpWebPortServers: SMPWebPortServers = .preset - public var tcpConnectTimeout: Int // microseconds - public var tcpTimeout: Int // microseconds + public var tcpConnectTimeout: NetworkTimeout + public var tcpTimeout: NetworkTimeout public var tcpTimeoutPerKb: Int // microseconds public var rcvConcurrency: Int // pool size public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults @@ -251,16 +251,16 @@ public struct NetCfg: Codable, Equatable { public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - tcpConnectTimeout: 25_000_000, - tcpTimeout: 15_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 45_000_000, interactiveTimeout: 15_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 30_000_000, interactiveTimeout: 10_000_000), tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, smpPingInterval: 1200_000_000 ) static let proxyDefaults: NetCfg = NetCfg( - tcpConnectTimeout: 35_000_000, - tcpTimeout: 20_000_000, + tcpConnectTimeout: NetworkTimeout(backgroundTimeout: 60_000_000, interactiveTimeout: 30_000_000), + tcpTimeout: NetworkTimeout(backgroundTimeout: 40_000_000, interactiveTimeout: 20_000_000), tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, smpPingInterval: 1200_000_000 @@ -287,6 +287,11 @@ public struct NetCfg: Codable, Equatable { public var enableKeepAlive: Bool { tcpKeepAlive != nil } } +public struct NetworkTimeout: Codable, Equatable { + public var backgroundTimeout: Int // microseconds + public var interactiveTimeout: Int // microseconds +} + public enum HostMode: String, Codable { case onionViaSocks case onionHost = "onion" diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 29ccab7357..5eed01a2a2 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -41,8 +41,12 @@ let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" let GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS = "networkSMPWebPortServers" -let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" -let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" +//let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" +//let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" +let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND = "networkTCPConnectTimeoutBackground" +let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "networkTCPConnectTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND = "networkTCPTimeoutInteractive" +let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE = "networkTCPTimeoutBackground" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" let GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" let GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL = "networkSMPPingInterval" @@ -73,8 +77,10 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.unknown.rawValue, GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allowProtected.rawValue, GROUP_DEFAULT_NETWORK_SMP_WEB_PORT_SERVERS: SMPWebPortServers.preset.rawValue, - GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, - GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND: NetCfg.defaults.tcpTimeout.backgroundTimeout, + GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE: NetCfg.defaults.tcpTimeout.interactiveTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY: NetCfg.defaults.rcvConcurrency, GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL: NetCfg.defaults.smpPingInterval, @@ -349,8 +355,14 @@ public func getNetCfg() -> NetCfg { let smpProxyMode = networkSMPProxyModeGroupDefault.get() let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() let smpWebPortServers = networkSMPWebPortServersDefault.get() - let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + let tcpConnectTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + ) + let tcpTimeout = NetworkTimeout( + backgroundTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND), + interactiveTimeout: groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) + ) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) let rcvConcurrency = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) let smpPingInterval = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) @@ -392,8 +404,10 @@ public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) { let socksProxy = networkProxy?.toProxyString() groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) networkSMPWebPortServersDefault.set(cfg.smpWebPortServers) - groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) - groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) + groupDefaults.set(cfg.tcpConnectTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpConnectTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE) + groupDefaults.set(cfg.tcpTimeout.backgroundTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_BACKGROUND) + groupDefaults.set(cfg.tcpTimeout.interactiveTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_INTERACTIVE) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) groupDefaults.set(cfg.rcvConcurrency, forKey: GROUP_DEFAULT_NETWORK_RCV_CONCURRENCY) groupDefaults.set(cfg.smpPingInterval, forKey: GROUP_DEFAULT_NETWORK_SMP_PING_INTERVAL) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 28780b21a9..daf7364a9b 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1346,6 +1346,26 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } + public var nextConnectPrepared: Bool { + get { + switch self { + case let .direct(contact): return contact.nextConnectPrepared + case let .group(groupInfo, _): return groupInfo.nextConnectPrepared + default: return false + } + } + } + + public var profileChangeProhibited: Bool { + get { + switch self { + case let .direct(contact): return contact.profileChangeProhibited + case let .group(groupInfo, _): return groupInfo.profileChangeProhibited + default: return false + } + } + } + public var userCantSendReason: (composeLabel: LocalizedStringKey, alertMessage: LocalizedStringKey?)? { get { switch self { @@ -1566,28 +1586,6 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { } } - var createdAt: Date { - switch self { - case let .direct(contact): return contact.createdAt - case let .group(groupInfo, _): return groupInfo.createdAt - case let .local(noteFolder): return noteFolder.createdAt - case let .contactRequest(contactRequest): return contactRequest.createdAt - case let .contactConnection(contactConnection): return contactConnection.createdAt - case .invalidJSON: return .now - } - } - - public var updatedAt: Date { - switch self { - case let .direct(contact): return contact.updatedAt - case let .group(groupInfo, _): return groupInfo.updatedAt - case let .local(noteFolder): return noteFolder.updatedAt - case let .contactRequest(contactRequest): return contactRequest.updatedAt - case let .contactConnection(contactConnection): return contactConnection.updatedAt - case .invalidJSON: return .now - } - } - public var chatTs: Date { switch self { case let .direct(contact): return contact.chatTs ?? contact.updatedAt @@ -1736,6 +1734,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var active: Bool { get { contactStatus == .active } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var nextConnectPrepared: Bool { preparedContact != nil && (activeConn == nil || activeConn?.connStatus == .prepared) } + public var profileChangeProhibited: Bool { activeConn != nil } public var nextAcceptContactRequest: Bool { contactRequestId != nil && (activeConn == nil || activeConn?.connStatus == .new) } public var sendMsgToConnect: Bool { nextSendGrpInv || nextConnectPrepared } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } @@ -1908,10 +1907,6 @@ public struct UserContact: Decodable, Hashable { self.userContactLinkId = userContactLinkId } - public init(contactRequest: UserContactRequest) { - self.userContactLinkId = contactRequest.userContactLinkId - } - public var id: String { "@>\(userContactLinkId)" } @@ -1919,7 +1914,7 @@ public struct UserContact: Decodable, Hashable { public struct UserContactRequest: Decodable, NamedChat, Hashable { var contactRequestId: Int64 - public var userContactLinkId: Int64 + public var userContactLinkId_: Int64? public var cReqChatVRange: VersionRange var localDisplayName: ContactName var profile: Profile @@ -1936,7 +1931,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable { public static let sampleData = UserContactRequest( contactRequestId: 1, - userContactLinkId: 1, + userContactLinkId_: 1, cReqChatVRange: VersionRange(1, 1), localDisplayName: "alice", profile: Profile.sampleData, @@ -2095,6 +2090,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var apiId: Int64 { get { groupId } } public var ready: Bool { get { true } } public var nextConnectPrepared: Bool { if let preparedGroup { !preparedGroup.connLinkStartedConnection } else { false } } + public var profileChangeProhibited: Bool { preparedGroup?.connLinkPreparedConnection ?? false } public var displayName: String { localAlias == "" ? groupProfile.displayName : localAlias } public var fullName: String { get { groupProfile.fullName } } public var image: String? { get { groupProfile.image } } @@ -2143,6 +2139,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public struct PreparedGroup: Decodable, Hashable { public var connLinkToConnect: CreatedConnLink + public var connLinkPreparedConnection: Bool public var connLinkStartedConnection: Bool } diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift index a433d2313b..2f2dd31a83 100644 --- a/apps/ios/SimpleXChat/ErrorAlert.swift +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -11,7 +11,6 @@ import SwiftUI public struct ErrorAlert: Error { public let title: LocalizedStringKey public let message: LocalizedStringKey? - public let actions: Optional<() -> AnyView> public init( title: LocalizedStringKey, @@ -19,7 +18,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = nil } public init( @@ -29,7 +27,6 @@ public struct ErrorAlert: Error { ) { self.title = title self.message = message - self.actions = { AnyView(actions()) } } public init(_ title: LocalizedStringKey) { @@ -75,11 +72,7 @@ extension View { set: { if !$0 { errorAlert.wrappedValue = nil } } ), actions: { - if let actions_ = errorAlert.wrappedValue?.actions { - actions_() - } else { - if let alert = errorAlert.wrappedValue { actions(alert) } - } + if let alert = errorAlert.wrappedValue { actions(alert) } }, message: { if let message = errorAlert.wrappedValue?.message { diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 92dfafca21..8a443017e1 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -20,7 +20,7 @@ typedef void* chat_ctrl; extern char *chat_migrate_init_key(char *path, char *key, int keepKey, char *confirm, int backgroundMode, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctl); extern char *chat_reopen_store(chat_ctrl ctl); -extern char *chat_send_cmd(chat_ctrl ctl, char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index b9b5277aeb..d8fa2c65a7 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -58,8 +58,8 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); @@ -102,20 +102,20 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, __unused jc } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); //jint length = (jint) (*env)->GetStringUTFLength(env, msg); //for (int i = 0; i < length; ++i) // __android_log_print(ANDROID_LOG_ERROR, "simplex", "%d: %02x\n", i, _msg[i]); - jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = (*env)->NewStringUTF(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 5c921c400d..adb29e0cc2 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -31,8 +31,8 @@ typedef long* chat_ctrl; extern char *chat_migrate_init(const char *path, const char *key, const char *confirm, chat_ctrl *ctrl); extern char *chat_close_store(chat_ctrl ctrl); -extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); -extern char *chat_send_remote_cmd(chat_ctrl ctrl, const int rhId, const char *cmd); +extern char *chat_send_cmd_retry(chat_ctrl ctrl, const char *cmd, const int retryNum); +extern char *chat_send_remote_cmd_retry(chat_ctrl ctrl, const int rhId, const char *cmd, const int retryNum); extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); @@ -115,17 +115,17 @@ Java_chat_simplex_common_platform_CoreKt_chatCloseStore(JNIEnv *env, jclass claz } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_cmd((void*)controller, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_cmd_retry((void*)controller, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmd(JNIEnv *env, jclass clazz, jlong controller, jint rhId, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendRemoteCmdRetry(JNIEnv *env, __unused jclass clazz, jlong controller, jint rhId, jstring msg, jint retryNum) { const char *_msg = encode_to_utf8_chars(env, msg); - jstring res = decode_to_utf8_string(env, chat_send_remote_cmd((void*)controller, rhId, _msg)); + jstring res = decode_to_utf8_string(env, chat_send_remote_cmd_retry((void*)controller, rhId, _msg, retryNum)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index c4328a6016..5cd9f51f75 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1288,6 +1288,8 @@ interface SomeChat { val ready: Boolean val chatDeleted: Boolean val nextConnect: Boolean + val nextConnectPrepared: Boolean + val profileChangeProhibited: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -1368,6 +1370,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contact.ready override val chatDeleted get() = contact.chatDeleted override val nextConnect get() = contact.nextConnect + override val nextConnectPrepared get() = contact.nextConnectPrepared + override val profileChangeProhibited get() = contact.profileChangeProhibited override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL @@ -1393,6 +1397,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = groupInfo.ready override val chatDeleted get() = groupInfo.chatDeleted override val nextConnect get() = groupInfo.nextConnect + override val nextConnectPrepared get() = groupInfo.nextConnectPrepared + override val profileChangeProhibited get() = groupInfo.profileChangeProhibited override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL @@ -1417,6 +1423,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = noteFolder.ready override val chatDeleted get() = noteFolder.chatDeleted override val nextConnect get() = noteFolder.nextConnect + override val nextConnectPrepared get() = noteFolder.nextConnectPrepared + override val profileChangeProhibited get() = noteFolder.profileChangeProhibited override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL @@ -1441,6 +1449,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contactRequest.ready override val chatDeleted get() = contactRequest.chatDeleted override val nextConnect get() = contactRequest.nextConnect + override val nextConnectPrepared get() = contactRequest.nextConnectPrepared + override val profileChangeProhibited get() = contactRequest.profileChangeProhibited override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL @@ -1465,6 +1475,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contactConnection.ready override val chatDeleted get() = contactConnection.chatDeleted override val nextConnect get() = contactConnection.nextConnect + override val nextConnectPrepared get() = contactConnection.nextConnectPrepared + override val profileChangeProhibited get() = contactConnection.profileChangeProhibited override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL @@ -1494,6 +1506,8 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = false override val chatDeleted get() = false override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -1688,7 +1702,8 @@ data class Contact( val active get() = contactStatus == ContactStatus.Active override val nextConnect get() = sendMsgToConnect val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent - val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) + override val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared) + override val profileChangeProhibited get() = activeConn != null val nextAcceptContactRequest get() = contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New) val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared override val incognito get() = contactConnIncognito @@ -1942,7 +1957,8 @@ data class GroupInfo ( override val apiId get() = groupId override val ready get() = membership.memberActive override val nextConnect get() = nextConnectPrepared - val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false + override val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false + override val profileChangeProhibited get() = preparedGroup?.connLinkPreparedConnection ?: false override val chatDeleted get() = false override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -2015,6 +2031,7 @@ data class GroupInfo ( @Serializable data class PreparedGroup ( val connLinkToConnect: CreatedConnLink, + val connLinkPreparedConnection: Boolean, val connLinkStartedConnection: Boolean ) @@ -2396,6 +2413,8 @@ class NoteFolder( override val chatDeleted get() = false override val ready get() = true override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice override val timedMessagesTTL: Int? get() = null @@ -2432,6 +2451,8 @@ class UserContactRequest ( override val chatDeleted get() = false override val ready get() = true override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -2473,6 +2494,8 @@ class PendingContactConnection( override val chatDeleted get() = false override val ready get() = false override val nextConnect get() = false + override val nextConnectPrepared get() = false + override val profileChangeProhibited get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index ee54803017..53f58bba0b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -149,8 +149,10 @@ class AppPreferences { val networkHostMode: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.default) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkSMPWebPortServers: SharedPreference = mkSafeEnumPreference(SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS, SMPWebPortServers.default) - val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) - val networkTCPTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT, NetCfg.defaults.tcpTimeout, NetCfg.proxyDefaults.tcpTimeout) + val networkTCPConnectTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpConnectTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.backgroundTimeout) + val networkTCPConnectTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpConnectTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpConnectTimeout.interactiveTimeout) + val networkTCPTimeoutBackground = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND, NetCfg.defaults.tcpTimeout.backgroundTimeout, NetCfg.proxyDefaults.tcpTimeout.backgroundTimeout) + val networkTCPTimeoutInteractive = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE, NetCfg.defaults.tcpTimeout.interactiveTimeout, NetCfg.proxyDefaults.tcpTimeout.interactiveTimeout) val networkTCPTimeoutPerKb = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB, NetCfg.defaults.tcpTimeoutPerKb, NetCfg.proxyDefaults.tcpTimeoutPerKb) val networkRcvConcurrency = mkIntPreference(SHARED_PREFS_NETWORK_RCV_CONCURRENCY, NetCfg.defaults.rcvConcurrency) val networkSMPPingInterval = mkLongPreference(SHARED_PREFS_NETWORK_SMP_PING_INTERVAL, NetCfg.defaults.smpPingInterval) @@ -271,13 +273,14 @@ class AppPreferences { set = fun(value) = settings.putFloat(prefName, value) ) - private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference { - val d = if (networkUseSocksProxy.get()) proxyDefault else default - return SharedPreference( - get = fun() = settings.getLong(prefName, d), + private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference = + SharedPreference( + get = { + val d = if (networkUseSocksProxy.get()) proxyDefault else default + settings.getLong(prefName, d) + }, set = fun(value) = settings.putLong(prefName, value) ) - } private fun mkBoolPreference(prefName: String, default: Boolean) = SharedPreference( @@ -402,8 +405,12 @@ class AppPreferences { private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_SMP_WEB_PORT_SERVERS = "NetworkSMPWebPortServers" - private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" - private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" +// private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" +// private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT = "NetworkTCPTimeout" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_BACKGROUND = "NetworkTCPConnectTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT_INTERACTIVE = "NetworkTCPConnectTimeoutInteractive" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_BACKGROUND = "NetworkTCPTimeoutBackground" + private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_INTERACTIVE = "NetworkTCPTimeoutInteractive" private const val SHARED_PREFS_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" private const val SHARED_PREFS_NETWORK_RCV_CONCURRENCY = "networkRcvConcurrency" private const val SHARED_PREFS_NETWORK_SMP_PING_INTERVAL = "NetworkSMPPingInterval" @@ -666,7 +673,87 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): API { + private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, retryNum: Int = 0): API? { + val r = sendCmd(rhId, cmd, retryNum = retryNum) + val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null + if (alert == null) return r + else return suspendCancellableCoroutine { cont -> + showRetryAlert( + alert, + onCancel = { + cont.resumeWith(Result.success(null)) + }, + onRetry = { + withLongRunningApi { + cont.resumeWith( + runCatching { + coroutineScope { + sendCmdWithRetry(rhId, cmd, retryNum = retryNum + 1) + } + } + ) + } + } + ) + + cont.invokeOnCancellation { + cont.resumeWith(Result.success(null)) + } + } + } + + private fun showRetryAlert(alert: Pair, onCancel: () -> Unit, onRetry: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(alert.first), + text = alert.second, + confirmText = generalGetString(MR.strings.retry_verb), + onConfirm = onRetry, + onDismiss = onCancel, + onDismissRequest = onCancel + ) + } + + private fun retryableNetworkErrorAlert(err: ChatError): Pair? { + if (err !is ChatError.ChatErrorAgent) return null + val e = err.agentError + when (e) { + is AgentErrorType.BROKER -> { + val message = { String.format(generalGetString(MR.strings.network_error_desc), serverHostname(e.brokerAddress)) } + return when (e.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.connection_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.connection_error to message() + else -> null + } + } + is AgentErrorType.SMP -> + if (e.smpErr is SMPErrorType.PROXY && e.smpErr.proxyErr is ProxyError.BROKER) { + val message = { String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(e.serverAddress)) } + return when (e.smpErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + else -> null + } + } + is AgentErrorType.PROXY -> + if (e.proxyErr is ProxyClientError.ProxyProtocolError && e.proxyErr.protocolErr is SMPErrorType.PROXY) { + val message = { String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(e.proxyServer), serverHostname(e.relayServer)) } + return when (e.proxyErr.protocolErr.proxyErr) { + is ProxyError.BROKER -> + when (e.proxyErr.protocolErr.proxyErr.brokerErr) { + is BrokerErrorType.TIMEOUT -> MR.strings.private_routing_timeout to message() + is BrokerErrorType.NETWORK -> MR.strings.private_routing_error to message() + else -> null + } + is ProxyError.NO_SESSION -> MR.strings.private_routing_no_session to message() + else -> null + } + } + else -> return null + } + return null + } + + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API { val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { @@ -675,7 +762,7 @@ object ChatController { chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) Log.d(TAG, "sendCmd: ${cmd.cmdType}") } - val rStr = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) + val rStr = if (rhId == null) chatSendCmdRetry(ctrl, c, retryNum) else chatSendRemoteCmdRetry(ctrl, rhId.toInt(), c, retryNum) // coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups) interruptIfCancelled() val r = json.decodeFromString(rStr) @@ -1208,16 +1295,16 @@ object ChatController { } suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair? { - val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) + val r = sendCmdWithRetry(rh, CC.APIContactQueueInfo(contactId)) if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo - apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + if (r != null) apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) return null } suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { - val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + val r = sendCmdWithRetry(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) if (r is API.Result && r.res is CR.QueueInfoR) return r.res.rcvMsgInfo to r.res.queueInfo - apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + if (r != null) apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) return null } @@ -1293,9 +1380,10 @@ object ChatController { suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair?, (() -> Unit)?> { val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } - val r = sendCmd(rh, CC.APIAddContact(userId, incognito = incognito)) + val r = sendCmdWithRetry(rh, CC.APIAddContact(userId, incognito = incognito)) return when { r is API.Result && r.res is CR.Invitation -> (r.res.connLinkInvitation to r.res.connection) to null + r == null -> null to null !(networkErrorAlert(r)) -> null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } else -> null to null } @@ -1311,8 +1399,9 @@ object ChatController { } suspend fun apiChangeConnectionUser(rh: Long?, connId: Long, userId: Long): PendingContactConnection? { - val r = sendCmd(rh, CC.ApiChangeConnectionUser(connId, userId)) + val r = sendCmdWithRetry(rh, CC.ApiChangeConnectionUser(connId, userId)) if (r is API.Result && r.res is CR.ConnectionUserChanged) return r.res.toConnection + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiChangeConnectionUser", generalGetString(MR.strings.error_sending_message), r) } @@ -1321,15 +1410,15 @@ object ChatController { suspend fun apiConnectPlan(rh: Long?, connLink: String): Pair? { val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } - val r = sendCmd(rh, CC.APIConnectPlan(userId, connLink)) + val r = sendCmdWithRetry(rh, CC.APIConnectPlan(userId, connLink)) if (r is API.Result && r.res is CR.CRConnectionPlan) return r.res.connLink to r.res.connectionPlan - apiConnectResponseAlert(r) + if (r != null) apiConnectResponseAlert(r) return null } suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? { val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.APIConnect(userId, incognito, connLink)) + val r = sendCmdWithRetry(rh, CC.APIConnect(userId, incognito, connLink)) when { r is API.Result && r.res is CR.SentConfirmation -> return r.res.connection r is API.Result && r.res is CR.SentInvitation -> return r.res.connection @@ -1338,7 +1427,7 @@ object ChatController { generalGetString(MR.strings.contact_already_exists), String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.res.contact.displayName) ) - else -> apiConnectResponseAlert(r) + r != null -> apiConnectResponseAlert(r) } return null } @@ -1426,8 +1515,9 @@ object ChatController { } suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent?): Contact? { - val r = sendCmd(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact + if (r == null) return null Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}") if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectPreparedContact", generalGetString(MR.strings.connection_error), r) @@ -1436,8 +1526,9 @@ object ChatController { } suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean, msg: MsgContent?): GroupInfo? { - val r = sendCmd(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg)) + val r = sendCmdWithRetry(rh, CC.APIConnectPreparedGroup(groupId, incognito, msg)) if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo + if (r == null) return null Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}") if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectPreparedGroup", generalGetString(MR.strings.connection_error), r) @@ -1447,8 +1538,9 @@ object ChatController { suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) + val r = sendCmdWithRetry(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) if (r is API.Result && r.res is CR.SentInvitationToContact) return r.res.contact + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r) } @@ -1592,8 +1684,9 @@ object ChatController { suspend fun apiCreateUserAddress(rh: Long?): CreatedConnLink? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) + val r = sendCmdWithRetry(rh, CC.ApiCreateMyAddress(userId)) if (r is API.Result && r.res is CR.UserContactLinkCreated) return r.res.connLinkContact + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) } @@ -1602,8 +1695,9 @@ object ChatController { suspend fun apiDeleteUserAddress(rh: Long?): User? { val userId = try { currentUserId("apiDeleteUserAddress") } catch (e: Exception) { return null } - val r = sendCmd(rh, CC.ApiDeleteMyAddress(userId)) + val r = sendCmdWithRetry(rh, CC.ApiDeleteMyAddress(userId)) if (r is API.Result && r.res is CR.UserContactLinkDeleted) return r.res.user.updateRemoteHostId(rh) + if (r == null) return null Log.e(TAG, "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") return null } @@ -1623,8 +1717,9 @@ object ChatController { suspend fun apiAddMyAddressShortLink(rh: Long?): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiAddMyAddressShortLink") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiAddMyAddressShortLink(userId)) + val r = sendCmdWithRetry(rh, CC.ApiAddMyAddressShortLink(userId)) if (r is API.Result && r.res is CR.UserContactLink) return r.res.contactLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddMyAddressShortLink", generalGetString(MR.strings.error_creating_address), r) } @@ -1633,19 +1728,20 @@ object ChatController { suspend fun apiSetUserAddressSettings(rh: Long?, settings: AddressSettings): UserContactLinkRec? { val userId = kotlin.runCatching { currentUserId("apiSetUserAddressSettings") }.getOrElse { return null } - val r = sendCmd(rh, CC.ApiSetAddressSettings(userId, settings)) + val r = sendCmdWithRetry(rh, CC.ApiSetAddressSettings(userId, settings)) if (r is API.Result && r.res is CR.UserContactLinkUpdated) return r.res.contactLink if (r is API.Error && r.err is ChatError.ChatErrorStore && r.err.storeError is StoreError.UserContactLinkNotFound ) { return null } + if (r == null) return null Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") return null } suspend fun apiAcceptContactRequest(rh: Long?, incognito: Boolean, contactReqId: Long): Contact? { - val r = sendCmd(rh, CC.ApiAcceptContact(incognito, contactReqId)) + val r = sendCmdWithRetry(rh, CC.ApiAcceptContact(incognito, contactReqId)) return when { r is API.Result && r.res is CR.AcceptingContactRequest -> r.res.contact r is API.Error && r.err is ChatError.ChatErrorAgent @@ -1657,12 +1753,13 @@ object ChatController { ) null } - else -> { + r != null -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiAcceptContactRequest", generalGetString(MR.strings.error_accepting_contact_request), r) } null } + else -> null } } @@ -1944,7 +2041,7 @@ object ChatController { } suspend fun apiJoinGroup(rh: Long?, groupId: Long) { - val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) + val r = sendCmdWithRetry(rh, CC.ApiJoinGroup(groupId)) when { r is API.Result && r.res is CR.UserAcceptedGroupSent -> withContext(Dispatchers.Main) { @@ -1965,7 +2062,7 @@ object ChatController { apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } - else -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) + r != null -> apiErrorAlert("apiJoinGroup", generalGetString(MR.strings.error_joining_group), r) } } @@ -2046,8 +2143,9 @@ object ChatController { } suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { - val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole)) + val r = sendCmdWithRetry(rh, CC.APICreateGroupLink(groupId, memberRole)) if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) } @@ -2064,8 +2162,9 @@ object ChatController { } suspend fun apiDeleteGroupLink(rh: Long?, groupId: Long): Boolean { - val r = sendCmd(rh, CC.APIDeleteGroupLink(groupId)) + val r = sendCmdWithRetry(rh, CC.APIDeleteGroupLink(groupId)) if (r is API.Result && r.res is CR.GroupLinkDeleted) return true + if (r == null) return false if (!(networkErrorAlert(r))) { apiErrorAlert("apiDeleteGroupLink", generalGetString(MR.strings.error_deleting_link_for_group), r) } @@ -2080,8 +2179,9 @@ object ChatController { } suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): GroupLink? { - val r = sendCmd(rh, CC.ApiAddGroupShortLink(groupId)) + val r = sendCmdWithRetry(rh, CC.ApiAddGroupShortLink(groupId)) if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink + if (r == null) return null if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r) } @@ -2510,6 +2610,12 @@ object ChatController { chatModel.networkStatuses[s.agentConnId] = s.networkStatus } } + is CR.ChatInfoUpdated -> + if (active(r.user)) { + withContext(Dispatchers.Main) { + chatModel.chatsContext.updateChatInfo(rhId, r.chatInfo) + } + } is CR.NewChatItems -> withBGApi { r.chatItems.forEach { chatItem -> val cInfo = chatItem.chatInfo @@ -3306,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() @@ -3350,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) @@ -4502,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, @@ -4522,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 @@ -4532,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 @@ -4557,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 = "", @@ -5959,6 +6079,7 @@ sealed class CR { // TODO remove above @Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List): CR() @Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List): CR() + @Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List): CR() @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @@ -6144,6 +6265,7 @@ sealed class CR { is ContactSubSummary -> "contactSubSummary" is NetworkStatusResp -> "networkStatus" is NetworkStatuses -> "networkStatuses" + is ChatInfoUpdated -> "chatInfoUpdated" is NewChatItems -> "newChatItems" is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated" is ChatItemUpdated -> "chatItemUpdated" @@ -6321,6 +6443,7 @@ sealed class CR { is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions)) is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections" is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses)) + is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo)) is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) }) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) @@ -6467,17 +6590,6 @@ data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) fun simplexChatUri(short: Boolean): String = if (short) connShortLink ?: simplexChatLink(connFullLink) else simplexChatLink(connFullLink) - - companion object { - val nullableStateSaver: Saver> = Saver( - save = { link -> link?.connFullLink to link?.connShortLink }, - restore = { saved -> - val connFullLink = saved.first - if (connFullLink == null) null - else CreatedConnLink(connFullLink = connFullLink, connShortLink = saved.second) - } - ) - } } fun simplexChatLink(uri: String): String = @@ -6645,7 +6757,45 @@ data class GroupLink( val shortLinkDataSet: Boolean, val groupLinkId: String, val acceptMemberRole: GroupMemberRole -) +) { + companion object { + val nullableStateSaver: Saver> = Saver( + save = { groupLink -> + if (groupLink == null) return@Saver null + + val conn = groupLink.connLinkContact + val connData = conn.connFullLink to conn.connShortLink + + listOf( + groupLink.userContactLinkId, + connData, + groupLink.shortLinkDataSet, + groupLink.groupLinkId, + groupLink.acceptMemberRole.name + ) + }, + restore = { saved -> + val list = saved as? List<*> ?: return@Saver null + + val userContactLinkId = list.getOrNull(0) as? Long ?: return@Saver null + val connPair = list.getOrNull(1) as? Pair<*, *> ?: return@Saver null + val connFullLink = connPair.first as? String ?: return@Saver null + val connShortLink = connPair.second as? String + val shortLinkDataSet = list.getOrNull(2) as? Boolean ?: return@Saver null + val groupLinkId = list.getOrNull(3) as? String ?: return@Saver null + val roleName = list.getOrNull(4) as? String ?: return@Saver null + + GroupLink( + userContactLinkId = userContactLinkId, + connLinkContact = CreatedConnLink(connFullLink, connShortLink), + shortLinkDataSet = shortLinkDataSet, + groupLinkId = groupLinkId, + acceptMemberRole = GroupMemberRole.valueOf(roleName) + ) + } + ) + } +} @Serializable data class CoreVersionInfo( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index a9f2dcaffc..9d8a699775 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -22,8 +22,8 @@ external fun pipeStdOutToSocket(socketName: String) : Int typealias ChatCtrl = Long external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array external fun chatCloseStore(ctrl: ChatCtrl): String -external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String -external fun chatSendRemoteCmd(ctrl: ChatCtrl, rhId: Int, msg: String): String +external fun chatSendCmdRetry(ctrl: ChatCtrl, msg: String, retryNum: Int): String +external fun chatSendRemoteCmdRetry(ctrl: ChatCtrl, rhId: Int, msg: String, retryNum: Int): String external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 6c7e775397..b77713831b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -729,7 +729,8 @@ private fun connectingText(chatInfo: ChatInfo): String? { is ChatInfo.Group -> when (chatInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending) // TODO [short links] add member status to show transition from prepared group to started connection earlier? + GroupMemberStatus.MemUnknown -> if (chatInfo.groupInfo.preparedGroup?.connLinkStartedConnection == true) generalGetString(MR.strings.group_connection_pending) else null + GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending) else -> null } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt index db02425d1f..ce023e83c9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextProfilePickerView.kt @@ -65,7 +65,7 @@ fun ComposeContextProfilePickerView( Modifier.size(20.dp), tint = MaterialTheme.colors.secondary, ) - } else { + } else if (!chat.chatInfo.profileChangeProhibited) { Icon( painterResource( MR.images.ic_chevron_up @@ -103,14 +103,21 @@ fun ComposeContextProfilePickerView( keepingChatId = chat.id ) if (chatModel.currentUser.value?.userId != newUser.userId) { - AlertManager.shared.showAlertMsg(generalGetString( - MR.strings.switching_profile_error_title), + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.switching_profile_error_title), String.format(generalGetString(MR.strings.switching_profile_error_message), newUser.chatViewName) ) } } } + fun showCantChangeProfileAlert() { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_title), + generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_message) + ) + } + @Composable fun ProfilePickerUserOption(user: User) { Row( @@ -118,15 +125,19 @@ fun ComposeContextProfilePickerView( .fillMaxWidth() .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) .clickable(onClick = { - if (selectedUser.value.userId == user.userId) { - if (!incognitoDefault) { - listExpanded.value = !listExpanded.value + if (!chat.chatInfo.profileChangeProhibited) { + if (selectedUser.value.userId == user.userId) { + if (!incognitoDefault) { + listExpanded.value = !listExpanded.value + } else { + chatModel.controller.appPrefs.incognito.set(false) + listExpanded.value = false + } } else { - chatModel.controller.appPrefs.incognito.set(false) - listExpanded.value = false + changeProfile(user) } } else { - changeProfile(user) + showCantChangeProfileAlert() } }) .padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp), @@ -156,11 +167,15 @@ fun ComposeContextProfilePickerView( .fillMaxWidth() .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp) .clickable(onClick = { - if (incognitoDefault) { - listExpanded.value = !listExpanded.value + if (!chat.chatInfo.profileChangeProhibited) { + if (incognitoDefault) { + listExpanded.value = !listExpanded.value + } else { + chatModel.controller.appPrefs.incognito.set(true) + listExpanded.value = false + } } else { - chatModel.controller.appPrefs.incognito.set(true) - listExpanded.value = false + showCantChangeProfileAlert() } }) .padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp), @@ -265,7 +280,13 @@ fun ComposeContextProfilePickerView( color = MaterialTheme.colors.secondary ) - if (incognitoDefault) { + if (chat.chatInfo.profileChangeProhibited) { + if (chat.chatInfo.incognito) { + IncognitoOption() + } else { + ProfilePickerUserOption(selectedUser.value) + } + } else if (incognitoDefault) { IncognitoOption() } else { ProfilePickerUserOption(selectedUser.value) @@ -273,9 +294,9 @@ fun ComposeContextProfilePickerView( } } - if (listExpanded.value) { - ProfilePicker() - } else { + if (!listExpanded.value || chat.chatInfo.profileChangeProhibited) { CurrentSelection() + } else { + ProfilePicker() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index d75069b922..5e34bfc0df 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -532,10 +532,11 @@ fun ComposeView( suspend fun sendConnectPreparedContact() { val mc = checkLinkPreview() sending() + val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() val contact = chatModel.controller.apiConnectPreparedContact( rh = chat.remoteHostId, contactId = chat.chatInfo.apiId, - incognito = chatModel.controller.appPrefs.incognito.get(), + incognito = incognito, msg = mc ) if (contact != null) { @@ -573,10 +574,11 @@ fun ComposeView( suspend fun connectPreparedGroup() { val mc = checkLinkPreview() sending() + val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() val groupInfo = chatModel.controller.apiConnectPreparedGroup( rh = chat.remoteHostId, groupId = chat.chatInfo.apiId, - incognito = chatModel.controller.appPrefs.incognito.get(), + incognito = incognito, msg = mc ) if (groupInfo != null) { @@ -1328,12 +1330,7 @@ fun ComposeView( Column { val currentUser = chatModel.currentUser.value - if (( - (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared) - || (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) - ) - && currentUser != null - ) { + if (chat.chatInfo.nextConnectPrepared && currentUser != null) { ComposeContextProfilePickerView( rhId = rhId, chat = chat, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index fc44fdb7a8..4bbe220aed 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -33,7 +33,7 @@ fun GroupLinkView( creatingGroup: Boolean = false, close: (() -> Unit)? = null ) { - var groupLinkVar by rememberSaveable { mutableStateOf(groupLink) } + var groupLinkVar by rememberSaveable(stateSaver = GroupLink.nullableStateSaver) { mutableStateOf(groupLink) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(groupLink?.acceptMemberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } fun createLink() { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index a5deb0e88c..dc208ae099 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -138,19 +138,21 @@ fun ChatPreviewView( val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } when (cInfo) { is ChatInfo.Direct -> { - if (cInfo.contact.verified) { - VerifiedIcon() + Row(verticalAlignment = Alignment.CenterVertically) { + if (cInfo.contact.verified) { + VerifiedIcon() + } + val color = if (deleting) + MaterialTheme.colors.secondary + else if (cInfo.contact.nextAcceptContactRequest || cInfo.contact.sendMsgToConnect) { + MaterialTheme.colors.primary + } else if (!cInfo.contact.sndReady) { + MaterialTheme.colors.secondary + } else { + Color.Unspecified + } + chatPreviewTitleText(color = color) } - val color = if (deleting) - MaterialTheme.colors.secondary - else if (cInfo.contact.nextAcceptContactRequest || cInfo.contact.sendMsgToConnect) { - MaterialTheme.colors.primary - } else if (!cInfo.contact.sndReady) { - MaterialTheme.colors.secondary - } else { - Color.Unspecified - } - chatPreviewTitleText(color = color) } is ChatInfo.Group -> { val color = if (deleting) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 0c38b0c045..40d664a257 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -48,8 +48,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) } val smpWebPortServers = remember { mutableStateOf(currentCfgVal.smpWebPortServers) } - val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } - val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } + val networkTCPConnectTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.interactiveTimeout) } + val networkTCPConnectTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout.backgroundTimeout) } + val networkTCPTimeoutInteractive = remember { mutableStateOf(currentCfgVal.tcpTimeout.interactiveTimeout) } + val networkTCPTimeoutBackground = remember { mutableStateOf(currentCfgVal.tcpTimeout.backgroundTimeout) } val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } @@ -86,8 +88,14 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode = smpProxyMode.value, smpProxyFallback = smpProxyFallback.value, smpWebPortServers = smpWebPortServers.value, - tcpConnectTimeout = networkTCPConnectTimeout.value, - tcpTimeout = networkTCPTimeout.value, + tcpConnectTimeout = NetworkTimeout( + backgroundTimeout = networkTCPConnectTimeoutBackground.value, + interactiveTimeout = networkTCPConnectTimeoutInteractive.value + ) , + tcpTimeout = NetworkTimeout( + backgroundTimeout = networkTCPTimeoutBackground.value, + interactiveTimeout = networkTCPTimeoutInteractive.value + ), tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, rcvConcurrency = networkRcvConcurrency.value, tcpKeepAlive = tcpKeepAlive, @@ -101,8 +109,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode.value = cfg.smpProxyMode smpProxyFallback.value = cfg.smpProxyFallback smpWebPortServers.value = cfg.smpWebPortServers - networkTCPConnectTimeout.value = cfg.tcpConnectTimeout - networkTCPTimeout.value = cfg.tcpTimeout + networkTCPConnectTimeoutInteractive.value = cfg.tcpConnectTimeout.interactiveTimeout + networkTCPConnectTimeoutBackground.value = cfg.tcpConnectTimeout.backgroundTimeout + networkTCPTimeoutInteractive.value = cfg.tcpTimeout.interactiveTimeout + networkTCPTimeoutBackground.value = cfg.tcpTimeout.backgroundTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb networkRcvConcurrency.value = cfg.rcvConcurrency networkSMPPingInterval.value = cfg.smpPingInterval @@ -156,8 +166,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode = smpProxyMode, smpProxyFallback = smpProxyFallback, smpWebPortServers, - networkTCPConnectTimeout, - networkTCPTimeout, + networkTCPConnectTimeoutInteractive, + networkTCPConnectTimeoutBackground, + networkTCPTimeoutInteractive, + networkTCPTimeoutBackground, networkTCPTimeoutPerKb, networkRcvConcurrency, networkSMPPingInterval, @@ -189,8 +201,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U smpProxyMode: MutableState, smpProxyFallback: MutableState, smpWebPortServers: MutableState, - networkTCPConnectTimeout: MutableState, - networkTCPTimeout: MutableState, + networkTCPConnectTimeoutInteractive: MutableState, + networkTCPConnectTimeoutBackground: MutableState, + networkTCPTimeoutInteractive: MutableState, + networkTCPTimeoutBackground: MutableState, networkTCPTimeoutPerKb: MutableState, networkRcvConcurrency: MutableState, networkSMPPingInterval: MutableState, @@ -242,14 +256,26 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { SectionItemView { TimeoutSettingRow( - stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeoutInteractive, + listOf(10_000000, 15_000000, 20_000000, 30_000000), secondsLabel ) } SectionItemView { TimeoutSettingRow( - stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeout, - listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000, 30_000_000), secondsLabel + stringResource(MR.strings.network_option_tcp_connection_timeout_background), networkTCPConnectTimeoutBackground, + listOf(30_000000, 45_000000, 60_000000, 90_000000), secondsLabel + ) + } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout), networkTCPTimeoutInteractive, + listOf(5_000000, 7_000000, 10_000000, 15_000000, 20_000_000), secondsLabel + ) + } + SectionItemView { + TimeoutSettingRow( + stringResource(MR.strings.network_option_protocol_timeout_background), networkTCPTimeoutBackground, + listOf(15_000000, 20_000000, 30_000000, 45_000000, 60_000_000), secondsLabel ) } SectionItemView { @@ -555,8 +581,10 @@ fun PreviewAdvancedNetworkSettingsLayout() { smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, smpWebPortServers = remember { mutableStateOf(SMPWebPortServers.Preset) }, - networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, - networkTCPTimeout = remember { mutableStateOf(10_000000) }, + networkTCPConnectTimeoutInteractive = remember { mutableStateOf(15_000000) }, + networkTCPConnectTimeoutBackground = remember { mutableStateOf(45_000000) }, + networkTCPTimeoutInteractive = remember { mutableStateOf(10_000000) }, + networkTCPTimeoutBackground = remember { mutableStateOf(30_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, networkRcvConcurrency = remember { mutableStateOf(8) }, networkSMPPingInterval = remember { mutableStateOf(10_000000) }, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index b60c55c3b9..00419c6703 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -147,7 +147,9 @@ Please check your network connection with %1$s and try again. Server address is incompatible with network settings: %1$s. Server version is incompatible with your app: %1$s. + Private routing timeout Private routing error + No private routing session Error connecting to forwarding server %1$s. Please try later. Forwarding server address is incompatible with network settings: %1$s. Forwarding server version is incompatible with network settings: %1$s. @@ -870,6 +872,8 @@ Your profile + Can\'t change profile + To use another profile after connection attempt, delete the chat and use the link again. Scan code @@ -1974,7 +1978,9 @@ Reset to defaults sec TCP connection timeout + TCP connection bg timeout Protocol timeout + Protocol background timeout Protocol timeout per KB Receiving concurrency PING interval diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 1b427e075c..2a4d2de6c2 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.4-beta.3 -android.version_code=297 +android.version_name=6.4-beta.4 +android.version_code=300 android.bundle=false -desktop.version_name=6.4-beta.3 -desktop.version_code=107 +desktop.version_name=6.4-beta.4 +desktop.version_code=109 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/apps/simplex-chat/Server.hs b/apps/simplex-chat/Server.hs index 0906d14536..883c5cb473 100644 --- a/apps/simplex-chat/Server.hs +++ b/apps/simplex-chat/Server.hs @@ -13,7 +13,6 @@ module Server where import Control.Monad -import Control.Monad.Except import Control.Monad.Reader import Data.Aeson (FromJSON, ToJSON (..)) import qualified Data.Aeson as J @@ -127,7 +126,7 @@ runChatServer ChatServerConfig {chatPort, clientQSize} cc = do where sendError corrId e = atomically $ writeTBQueue sndQ $ ACR ChatSrvResponse {corrId, resp = CSRBody $ chatCmdError e} processCommand (corrId, cmd) = - response <$> runReaderT (runExceptT $ processChatCommand cmd) cc + response <$> runReaderT (execChatCommand' cmd 0) cc where response r = ChatSrvResponse {corrId = Just corrId, resp = CSRBody r} clientDisconnected _ = pure () diff --git a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md index 57c4f69981..c7087f1657 100644 --- a/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md +++ b/blog/20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md @@ -89,7 +89,7 @@ Matrix network does not provide connection privacy, as not only user identity ex **Server operator transparency** -Operator transparency means that network users know who operates the servers they use. +Operator transparency means that network users know who operates the servers they use. You may argue that when the operators are known, the servers data can be requested by the authorities. But such requests, in particular when multiple operators are used by all users, will follow a due legal process, and will not result in compromising the privacy of all users. @@ -139,8 +139,8 @@ Evgeny SimpleX Chat founder -[1] You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). +[1]: You can also to self-host your own SimpleX servers on [Flux decentralized cloud](https://home.runonflux.io/apps/marketplace?q=simplex). -[2] The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. +[2]: The probability of connection being de-anonymized and the number of random server choices follow this equation: `(1 - s ^ 2) ^ n = 1 - p`, where `s` is the share of attacker-controlled servers in the network, `n` is the number of random choices of entry and exit nodes for the circuit, and `p` is the probability of both entry and exit nodes, and the connection privacy being compromised. Substituting `0.02` (2%) for `s`, `0.5` (50%) for `p`, and solving this equation for `n` we obtain that `1733` random circuits have 50% probability of privacy being compromised. Also see [this presentation about Tor](https://ritter.vg/p/tor-v1.6.pdf), specifically the approximate calculations on page 76, and also [Tor project post](https://blog.torproject.org/announcing-vanguards-add-onion-services/) about the changes that made attack on hidden service anonymity harder, but still viable in case the it is used for a long time. diff --git a/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md b/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md new file mode 100644 index 0000000000..077d438e6b --- /dev/null +++ b/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.md @@ -0,0 +1,192 @@ +--- +layout: layouts/article.html +title: "SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4" +date: 2025-07-03 +preview: Now you can start talking to your contacts much faster, as soon as you scan the link. This technical post covers the technology that enabled this new user experience — short links and associated data of messaging queues. +image: images/20250703-connect1a.png +imageBottom: true +permalink: "/blog/20250703-simplex-network-protocol-extension-for-securely-connecting-people.html" +--- + +# SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4 + +**Published:** Jul 3, 2025 + +The mission of communication network is connecting people [1]. The process of connecting in SimpleX network is really secure — it is protected from server MITM attacks. But before this beta version connecting to contacts had a really bad user experience. + +## What was the problem? + +How did it work before: + +1. Your contact created a large link (1-time invitation or contact address [2]) and shared with you via another messenger, email, social profile or website. Sharing the link "out-of-band" (i.e., not via the server) is necessary for security. But it was not working well in some cases: + - the link was incorrectly changed by some applications, e.g. Telegram, because of link complexity, preventing people from connecting. + - some people were worried that the link "looks like malware". + - the QR code for the link was large, and sometimes difficult to scan. + - the link did not fit the size limit in social media profiles. +2. Once you received the link, you used it in the app. But when using the link you could not see who you were connecting to. The only choice you had is to share your current profile or to use incognito profile. And as people didn't know which profile will be shared with them, many were choosing to share incognito profile, making recognizing contacts more complex — you don't know who is who unless you attach aliases to incognito contacts. +3. Once you tap Connect, all you see is the line in the list of chats that said "Connecting via link". The process of connection required your contact to be online, and in some cases to approve the request, so you may had contact in "connecting" state for a really long time. + +So it is not surprising that a large number of people failed connecting to friends — either they refused to engage because of large and scary looking links, or their application made the link unusable, or they abandoned the process at step 3, deciding that the app is not working correctly. + +## Why can't we just use usernames? + +Many people asked — why don't you just use usernames or a link shortener for some really short links, as other networks and apps do. + +The problem is that usernames or very short links make e2e encryption security of your chats dependent on the servers. Unless the link you share contains enough randomness in it and is cryptographically linked to the encryption keys, the servers can substitute the e2e encryption keys and read all your communication without you knowing it. We see this risk as unacceptable. + +Mitigation against this "man-in-the-middle" attack by the server [3] is offered by Signal and other apps via security code verification [4], when you compare the numeric code in your app with your contact's app, but: +- most people do not verify security codes, and even if they do, they do not re-verify them every time security code changes, so their security is dependent on the server not being compromised, which is not a great security, +- the servers can still compromise the initial messages, where profile names are exchanged, before you had the chance to verify the security codes. + +When we design communication protocols for SimpleX network we always aim to protect you from the attack via your network operators — this is what sets SimpleX network design apart from many other communication networks and platforms. + +Even though you choose the servers that you trust, and they are bound by privacy policy, and we follow the best security practices to protect servers from any 3rd party attacks, there is still a possibility that servers may be compromised by some attackers, and unless your communications are not protected from the servers, they are not protected from whoever can compromise the servers [5]. + +## What is the new way to make connections? + +Before diving into the details of technology, let's walk though the new process of connecting to people that is introduced in [v6.4-beta.4](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.0-beta.4). + + + +1. As before, to connect you or your contact need to create a 1-time invitation or a contact address link. All the past problems of the long links are now solved: + - this link is correctly processed by all applications, as it has a simple structure, + - it is short and simple, e.g. the SimpleX Chat team address for support is: https://smp6.simplex.im/a#lrdvu2d8A1GumSmoKb2krQmtKhWXq-tyGpHuM7aMwsw + - the QR code is now much smaller, fits a standard business card, and is easy to scan from all devices, + - it fits in most social media profiles. + +While the link is short, it still contains 256 bits of key material with additional 192 bits of server-generated link ID for one-time invitation links, so the connection is as secure as before, and in case of one-time invitations it became more secure (see below). + + + +2. As before, you have to use this link in the app, either by pasting the link or scanning QR code. But now you instantly see the name of your contact or group you are connecting to, and from v6.4.1 that will be released this July you will also see a profile image (currently disabled for backward compatibility). + +3. Once you tap "Open new chat", the app will instantly open a conversation with your contact. As you now can see which profile is shared with you via the link, you can choose which of your profiles to use to connect. If your contact shared an incognito pseudonym, then you may also choose to connect incognito. But if your contact shared a real name, you may want to share your real name as well, making it easier for your contact to recognize you — the law of reciprocity in action! **In any case, your and your contact's profile names are inaccessible to messaging servers — they are e2e encrypted**. + +If you are connecting via a contact address you can also add a message to your request, making it more likely to be accepted when connecting to somebody you don't know well. And from v6.4.1 contact addresses can include a welcome message that you would see before connecting, right in the conversation. This way, you effectively become connected to your contact and start a secure conversation even before you tap "Connect" button. + +If you are connecting via a one-time invitation link, all you need to do is to tap "Connect", and then you can send messages straight after that, without waiting for your contact to be online — they will be securely received later. + +This new experience of connecting is very similar to commonly used messengers, but it protects your security. We hope that it will be much easier for the new users to connect to their friends. + +## What about security? + +We took a great care to design the protocol extension for the new experience of connecting in a way that not only preserves security at the same level as before, but also increases security of connecting via one-time invitation links. + +First, because all the keys are now included in encrypted link data on the server, and not in the link itself as before, we can include the keys for post-quantum (PQ) key exchange and make the first message sent via one-time link (your profile) encrypted with PQ e2e encryption. Previously, PQ encryption started only after the response from your contact. + +Second, whoever can observe the link is not able to determine which public keys are used in key exchange and what messaging queue address is used, and this data is removed from the server once the connection is established. Previously, the invitation link contained public keys and the actual queue address that could have been used for a long time, unless you rotated it. + +Third, if somebody retrieves the associated data of one-time invitation link they observed in transit, this link would become inaccessible for the intended recipient, so the recipient would know that the connection was potentially compromised, and would alert the contact that sent the link. + +## How does it work? + +In short, a new short link references a container with the encrypted data on the server that contains: +- the original full link that now include quantum-resistant keys that previously were not included because of their size, +- contact's or group's profile and conversation preferences, from v6.4.1 it will include profile images, +- also, it will include an optional contact's or group's welcome message from v6.4.1. + +Making user profile and welcome message included in the encrypted link data allows to start conversation as soon as you scan the link, as described in the previous section. + +### Design objectives and cryptographic primitives that achieve them + +This section is not a formal specification of the protocol, but an informal technical explanation of objectives we had for this design and how they were achieved. The technical details are available in [this RFC document](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-03-16-smp-queues.md). + +1. **Encrypted link data cannot be accessed by the server**. + +It means that while the client apps should use the link to derive both the link ID for the server and decryption key for the associated link data, the server should not be able to derive the link and decryption key from the link ID that it knows, and can't access the link data. + +This objective is achieved by using `secret_box` encryption of link data with the symmetric key derived from link URI, which is different from link ID known to the server. As it is a symmetric encryption, it is secure against quantum computers. + +2. **Allow changing encrypted link data without changing the link itself**. + +This is necessary to allow changes in user profile, chat preferences and welcome messages. + +This is possible via a specific server request that allows to change user-defined part of the link data to the link owner. Because the link is derived from fixed part of the link data, the link itself remains the same. + +3. **Prevent MITM attack on the link data by the server, even if the server obtained the link**. + +It means that the server should not be able to replace the associated link data even if it somehow obtained the link and can decrypt the data. + +This objective is achieved by deriving encryption key from the hash of the fixed part of the link data — if server changes the link data, it would be rejected by the client, as its hash won't match the link. Server also cannot replace the user-defined part of the link data, because it is signed and will be verified with the key included in the fixed part of link data. + +Clients use `HKDF` for key derivation, `SHA3-256` to compute hash of the fixed part of link data, and `ED25519` signature to sign user-defined link data. + +4. **Prevent undetectably accessing encrypted link data of one-time links**. + +This is explained in the previous section — if link observers retrieve the link data, the link will become inaccessible for the intended recipient. + +This objective is achieved because the link data of 1-time invitation link data can only be accessed with the server request that locks queue on the first access. Any subsequent access to the queue must uses the same authorization key (`ED25519`). + +5. **The link owner cannot include address of another queue in the link**. + +It means that the link cannot redirect the connecting party to another server or to another queue on the same server — the apps would reject the links that attempt to do it. While allowing redirects may be seen as higher security from the server, it would open the possibility of resource exhaustion attacks, as the server would not know if the links were actually used to connect or not, and when the link data can be removed. So we decided that preventing redirects is a better tradeoff. This cryptographically enforced association between link and queue allows to remove link data from the server once the connection is established, or once some time passes (e.g., 3 weeks for unused one-time invitation links). + +This objective is achieved by including queue ID and link data into the same server response. + +6. **Prevent link owner from being able to change the queue address and encryption keys in the link**. + +This quality prevents the MITM attack on e2e encryption via break-in attack on the client of the link owner. + +The server does not provide any API to change the fixed part of the link data. Also, changing fixed data would require changing the link, as otherwise the hash of the data won't match the link. + +7. **It should be impossible to check the existence of a messaging queue for one-time invitation links**. + +It means that any 3rd party that observed 1-time invitation link (e.g., by reading the message in the messenger or email where it was sent) must not be able to undetectably confirm whether this messaging queue still exists, by attempting to create another queue with the same link ID and the same link data. + +This objective is achieved by servers requiring that sender ID is derived (using `SHA3-384`) from request correlation ID, so an arbitrary sender ID cannot be used, and by generating link ID on the server — for 1-time invitation link, the link ID is included in the link in addition to link key, and is not derived from the link data. + +See the detailed [threat model](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2025-03-16-smp-queues.md#threat-model) for protocol extension supporting the new user experience of making connections. + +## Let us know what you think! + +We worked really hard to deliver this big change — the simplicity of user experience required to hide a lot of complexity under the hood. We really hope that it will help you to bring more of your friends to SimpleX network and to benefit from using secure communications. + +The stable versions v6.4 and v6.4.1 will be released this July, but you can already use the beta version available via [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app) (Android), [Test Flight](https://testflight.apple.com/join/DWuT2LQu) (iOS) and [GitHub](https://github.com/simplex-chat/simplex-chat/releases/tag/v6.4.0-beta.4) (Android and desktop). + +Big thank you to everybody who uses SimpleX network, even though the experience of connecting to people was complex before this release. + +With your help, SimpleX network should be able to get over the million active users now! + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-and-security-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge *thank you* to everybody who donated to SimpleX Chat! + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/#please-support-us-with-your-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder + +[1]: An interesting case study is the rise and fall of Nokia as the dominant supplier of mobile phones. The slogan "Connecting people" was created in 1992 by Ove Strandberg, an intern at Nokia, and as it was adopted as the core mission of the company, we saw it rise as the world's main mobile phone supplier. The fall of Nokia is usually attributed on iPhone success. But it may also be attributed to internal cultural changes, with Nokia's communications chief leaving in early 2000s, and Nokia failure to understand how the definition of "Connecting people" should evolve with time. + +[2]: One-time invitation can only be used once by the person you gave it too. Once your contact scans the one-time link, nobody else can connect to you via this link. Contact address can be used by multiple people, and even if you later delete the address, everybody who connected to you will remain connected. You can read more about the differences between one-time invitation links and contact addresses [here](../docs/guide/making-connections.md#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + +[3]: Read more about how man-in-the-middle attack works in our [post about e2e encryption properties](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#5-man-in-the-middle-attack-mitigated-by-two-factor-key-exchange). It also has the comparison of e2e encryption security in different messengers. + +[4]: SimpleX apps also allow security code verification, but it protects against different attack — the link substitution by the channel you use to pass it, not from the attacks by the servers — SimpleX servers cannot compromise e2e encryption. + +[5]: That is also why "securely scanning users' communications", also known as "Chat Control" is impossible — what communication operator can access, cyber-criminals will also access, and instead of reducing crime it would expose users to more crime. diff --git a/blog/README.md b/blog/README.md index 00a84eca6c..c8b464c810 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,6 +1,12 @@ # Blog -Mar 3, 2025 [SimpleX Chat v6.3: new user experience and safety in public groups](./20241014-simplex-network-v6-1-security-review-better-calls-user-experience.md) +Jul 3, 2025 [SimpleX network: new experience of connecting with people — available in SimpleX Chat v6.4-beta.4](./20250703-simplex-network-protocol-extension-for-securely-connecting-people.md) + +Now you can start talking to your contacts much faster, as soon as you scan the link. This technical post covers the technology that enabled this new user experience — short links and associated data of messaging queues. + +--- + +Mar 8, 2025 [SimpleX Chat v6.3: new user experience and safety in public groups](./20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html) What's new in v6.3: - preventing spam and abuse in public groups. @@ -12,13 +18,13 @@ Also, we added Catalan interface language to Android and desktop apps, thanks to The last but not the least - server builds are now reproducible! --- +--- Jan 14, 2025 [SimpleX network: large groups and privacy-preserving content moderation](./20250114-simplex-network-large-groups-privacy-preserving-content-moderation.md) This post explains how server operators can moderate end-to-end encrypted conversations without compromising user privacy or end-to-end encryption. --- +--- Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats and more with v6.2 of the apps](./20241210-simplex-network-v6-2-servers-by-flux-business-chats.md) @@ -27,7 +33,7 @@ Dec 10, 2024 [SimpleX network: preset servers operated by Flux, business chats a - Better user experience: open on the first unread, jump to quoted messages, see who reacted. - Improving notifications in iOS app. --- +--- Nov 25, 2024 [Servers operated by Flux - true privacy and decentralization for all users](./20241125-servers-operated-by-flux-true-privacy-and-decentralization-for-all-users.md) diff --git a/blog/images/20250703-card.jpg b/blog/images/20250703-card.jpg new file mode 100644 index 0000000000..937c70911d Binary files /dev/null and b/blog/images/20250703-card.jpg differ diff --git a/blog/images/20250703-connect1.png b/blog/images/20250703-connect1.png new file mode 100644 index 0000000000..18638a9b77 Binary files /dev/null and b/blog/images/20250703-connect1.png differ diff --git a/blog/images/20250703-connect1a.png b/blog/images/20250703-connect1a.png new file mode 100644 index 0000000000..0d7bb37ffd Binary files /dev/null and b/blog/images/20250703-connect1a.png differ diff --git a/blog/images/20250703-connect2.png b/blog/images/20250703-connect2.png new file mode 100644 index 0000000000..87ad2458bb Binary files /dev/null and b/blog/images/20250703-connect2.png differ diff --git a/cabal.project b/cabal.project index cd29f46132..b348bfa741 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: a46edd60f0e2460ec4a7d6fb371412f32e988357 + tag: 1062ccc5c3d915fc620a4c0080e82d28a401839b source-repository-package type: git diff --git a/libsimplex.dll.def b/libsimplex.dll.def index 592e6db4f2..2945d52e83 100644 --- a/libsimplex.dll.def +++ b/libsimplex.dll.def @@ -5,7 +5,9 @@ EXPORTS chat_migrate_init chat_close_store chat_send_cmd + chat_send_cmd_retry chat_send_remote_cmd + chat_send_remote_cmd_retry chat_recv_msg chat_recv_msg_wait chat_parse_markdown diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 2ff17a6ddf..0b390e147f 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,35 @@ + + https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html + +

New in v6.3.1-7:

+
    +
  • fix changing the user profile of the invitation link.
  • +
  • forward compatibility with short link data.
  • +
  • fixes
  • +
  • fixes mentions with trailing punctuation (e.g., hello @name!).
  • +
  • recognizes domain names as links (e.g., simplex.chat).
  • +
  • forward compatibility with "knocking" (a feature for group admins to review and to chat with the new members prior to admitting them to groups, it will be released in 6.4)
  • +
  • support for connecting via short connection links.
  • +
  • fix related to backward/forward compatibility of the app in some rare cases.
  • +
  • scrolling/navigation improvements.
  • +
  • faster onboarding (conditions and operators are combined to one screen).
  • +
+

New in v6.3.0:

+
    +
  • Mention members and get notified when mentioned.
  • +
  • Send private reports to moderators.
  • +
  • Delete, block and change role for multiple members at once
  • +
  • Faster sending messages and faster deletion.
  • +
  • Organize chats into lists to keep track of what's important.
  • +
  • Jump to found and forwarded messages.
  • +
  • Private media file names.
  • +
  • Message expiration in chats.
  • +
+
+
https://simplex.chat/blog/20250308-simplex-chat-v6-3-new-user-experience-safety-in-public-groups.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 79d0c22732..2037838194 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."a46edd60f0e2460ec4a7d6fb371412f32e988357" = "0vix4s9nfxrwiy56rc294s9ik7l9rar4zg2pvl1c4v914lsphybq"; + "https://github.com/simplex-chat/simplexmq.git"."1062ccc5c3d915fc620a4c0080e82d28a401839b" = "0r2m7sq1dibcvngg3k6dyja5sp046910wbgmks1arrxxyxzni6k3"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index e4810dad3b..f48f46a1f7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.0.5 +version: 6.4.0.5.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -108,6 +108,8 @@ library Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links + Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete + Simplex.Chat.Store.Postgres.Migrations.M20250704_groups_conn_link_prepared_connection else exposed-modules: Simplex.Chat.Archive @@ -241,6 +243,8 @@ library Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links + Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete + Simplex.Chat.Store.SQLite.Migrations.M20250704_groups_conn_link_prepared_connection other-modules: Paths_simplex_chat hs-source-dirs: diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index b1b9543723..0061b38022 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -768,6 +768,7 @@ data ChatEvent | CEvtGroupMemberSwitch {user :: User, groupInfo :: GroupInfo, member :: GroupMember, switchProgress :: SwitchProgress} | CEvtContactRatchetSync {user :: User, contact :: Contact, ratchetSyncProgress :: RatchetSyncProgress} | CEvtGroupMemberRatchetSync {user :: User, groupInfo :: GroupInfo, member :: GroupMember, ratchetSyncProgress :: RatchetSyncProgress} + | CEvtChatInfoUpdated {user :: User, chatInfo :: AChatInfo} | CEvtNewChatItems {user :: User, chatItems :: [AChatItem]} -- there is the same command response | CEvtChatItemsStatusesUpdated {user :: User, chatItems :: [AChatItem]} | CEvtChatItemUpdated {user :: User, chatItem :: AChatItem} -- there is the same command response diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index 865eb6a760..08c0a30c6d 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -65,10 +65,10 @@ runSimplexChat ChatOpts {maintenance} u cc chat waitEither_ a1 a2 sendChatCmdStr :: ChatController -> String -> IO (Either ChatError ChatResponse) -sendChatCmdStr cc s = runReaderT (execChatCommand Nothing . encodeUtf8 $ T.pack s) cc +sendChatCmdStr cc s = runReaderT (execChatCommand Nothing (encodeUtf8 $ T.pack s) 0) cc sendChatCmd :: ChatController -> ChatCommand -> IO (Either ChatError ChatResponse) -sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc +sendChatCmd cc cmd = runReaderT (execChatCommand' cmd 0) cc getSelectActiveUser :: DBStore -> IO (Maybe User) getSelectActiveUser st = do @@ -108,7 +108,7 @@ createActiveUser cc = do loop = do displayName <- T.pack <$> getWithPrompt "display name" let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} - execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case + execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) 0 `runReaderT` cc >>= \case Right (CRActiveUser user) -> pure user r -> printResponseEvent (Nothing, Nothing) (config cc) r >> loop diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 52fd48cfde..42499bd6a6 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -95,7 +95,7 @@ import Simplex.Messaging.Agent.Store.Interface (execSQL) import Simplex.Messaging.Agent.Store.Shared (upMigration) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations) -import Simplex.Messaging.Client (NetworkConfig (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), textToHostMode) import Simplex.Messaging.Compression (compressionLevel) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) @@ -263,7 +263,8 @@ updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, smpWebPortServers, tcpTimeout_, logTLSErrors} = let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ - cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ + cfg3 = maybe cfg2 (\t -> cfg2 {tcpTimeout = nt t, tcpConnectTimeout = nt ((t * 3) `div` 2)}) tcpTimeout_ + nt t = NetworkTimeout {backgroundTimeout = t * 3, interactiveTimeout = t} in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, smpWebPortServers, logTLSErrors} useServers :: Foldable f => RandomAgentServers -> [(Text, ServerOperator)] -> f UserOperatorServers -> (NonEmpty (ServerCfg 'PSMP), NonEmpty (ServerCfg 'PXFTP)) @@ -272,25 +273,27 @@ useServers as opDomains uss = xftp' = useServerCfgs SPXFTP as opDomains $ concatMap (servers' SPXFTP) uss in (smp', xftp') -execChatCommand :: Maybe RemoteHostId -> ByteString -> CM' (Either ChatError ChatResponse) -execChatCommand rh s = +execChatCommand :: Maybe RemoteHostId -> ByteString -> Int -> CM' (Either ChatError ChatResponse) +execChatCommand rh s retryNum = case parseChatCommand s of Left e -> pure $ chatCmdError e Right cmd -> case rh of Just rhId - | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s + | allowRemoteCommand cmd -> execRemoteCommand rhId cmd s retryNum | otherwise -> pure $ Left $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand _ -> do cc@ChatController {config = ChatConfig {chatHooks}} <- ask case preCmdHook chatHooks of - Just hook -> liftIO (hook cc cmd) >>= either pure execChatCommand' - Nothing -> execChatCommand' cmd + Just hook -> liftIO (hook cc cmd) >>= either pure (`execChatCommand'` retryNum) + Nothing -> execChatCommand' cmd retryNum -execChatCommand' :: ChatCommand -> CM' (Either ChatError ChatResponse) -execChatCommand' cmd = handleCommandError $ processChatCommand cmd +execChatCommand' :: ChatCommand -> Int -> CM' (Either ChatError ChatResponse) +execChatCommand' cmd retryNum = handleCommandError $ do + vr <- chatVersionRange + processChatCommand vr (NRMInteractive' retryNum) cmd -execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> CM' (Either ChatError ChatResponse) -execRemoteCommand rhId cmd s = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s +execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> Int -> CM' (Either ChatError ChatResponse) +execRemoteCommand rhId cmd s retryNum = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s retryNum handleCommandError :: CM ChatResponse -> CM' (Either ChatError ChatResponse) handleCommandError a = runExceptT a `E.catches` ioErrors @@ -304,13 +307,8 @@ parseChatCommand :: ByteString -> Either String ChatCommand parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace -- | Chat API commands interpreted in context of a local zone -processChatCommand :: ChatCommand -> CM ChatResponse -processChatCommand cmd = - chatVersionRange >>= (`processChatCommand'` cmd) -{-# INLINE processChatCommand #-} - -processChatCommand' :: VersionRangeChat -> ChatCommand -> CM ChatResponse -processChatCommand' vr = \case +processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> CM ChatResponse +processChatCommand vr nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser NewUser {profile, pastTimestamp} -> do forM_ profile $ \Profile {displayName} -> checkValidName displayName @@ -367,20 +365,20 @@ processChatCommand' vr = \case SetActiveUser uName viewPwd_ -> do tryChatError (withFastStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown - Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ + Right userId -> processChatCommand vr nm $ APISetActiveUser userId viewPwd_ SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ APISetUserContactReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserContactReceipts db user' settings ok user - SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings + SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserContactReceipts userId settings APISetUserGroupReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserGroupReceipts db user' settings ok user - SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings + SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserGroupReceipts userId settings APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -406,15 +404,15 @@ processChatCommand' vr = \case setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True} APIMuteUser userId' -> setUserNotifications userId' False APIUnmuteUser userId' -> setUserNotifications userId' True - HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIHideUser userId viewPwd - UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand $ APIUnhideUser userId viewPwd - MuteUser -> withUser $ \User {userId} -> processChatCommand $ APIMuteUser userId - UnmuteUser -> withUser $ \User {userId} -> processChatCommand $ APIUnmuteUser userId + HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIHideUser userId viewPwd + UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnhideUser userId viewPwd + MuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIMuteUser userId + UnmuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnmuteUser userId APIDeleteUser userId' delSMPQueues viewPwd_ -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' viewPwd_ checkDeleteChatUser user' - withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues + withChatLock "deleteUser" $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ StartChat {mainApp, enableSndFiles, largeLinkData} -> withUser' $ \_ -> do chatWriteVar useLargeLinkData largeLinkData @@ -474,7 +472,7 @@ processChatCommand' vr = \case ExportArchive -> do ts <- liftIO getCurrentTime let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" - processChatCommand $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing + processChatCommand vr nm $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing APIImportArchive cfg -> checkChatStopped $ do fileErrs <- lift $ importArchive cfg setStoreChanged @@ -586,7 +584,7 @@ processChatCommand' vr = \case ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage - processChatCommand $ APIReportMessage gId reportedItemId reportReason "" + processChatCommand vr nm $ APIReportMessage gId reportedItemId reportReason "" APIUpdateChatItem (ChatRef cType chatId scope) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do unless (null mentions) $ throwCmdError "mentions are not supported in this chat" @@ -993,7 +991,7 @@ processChatCommand' vr = \case let ext = takeExtension fileName pure $ prefix <> formattedDate <> ext APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user - UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId + UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId @@ -1070,7 +1068,7 @@ processChatCommand' vr = \case CTDirect -> do ct <- withFastStore $ \db -> getContact db vr user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct - withContactLock "deleteChat direct" chatId . procCmd $ + withContactLock "deleteChat direct" chatId $ case cdm of CDMFull notify -> do deleteCIFiles user filesInfo @@ -1091,7 +1089,7 @@ processChatCommand' vr = \case getContact db vr user chatId pure $ CRContactDeleted user ct' CDMMessages -> do - void $ processChatCommand $ APIClearChat cRef + void $ processChatCommand vr nm $ APIClearChat cRef withFastStore' $ \db -> setContactChatDeleted db user ct True pure $ CRContactDeleted user ct {chatDeleted = True} where @@ -1100,7 +1098,7 @@ processChatCommand' vr = \case when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) deleteAgentConnectionsAsync' contactConnIds doSendDel - CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do + CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync acId withFastStore' $ \db -> deletePendingContactConnection db userId chatId @@ -1112,7 +1110,7 @@ processChatCommand' vr = \case canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo - withGroupLock "deleteChat group" chatId . procCmd $ do + withGroupLock "deleteChat group" chatId $ do deleteCIFiles user filesInfo let doSendDel = memberActive membership && isOwner recipients = filter memberCurrentOrPending members @@ -1150,45 +1148,62 @@ processChatCommand' vr = \case pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) _ -> throwCmdError "not supported" APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do - (uclId, (ucl, gLinkInfo_)) <- withFastStore $ \db -> do - uclId <- getUserContactLinkIdByCReq db connReqId - uclGLinkInfo <- getUserContactLinkById db userId uclId - pure (uclId, uclGLinkInfo) - let UserContactLink {shortLinkDataSet, addressSettings} = ucl - when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" - withUserContactLock "acceptContact" uclId $ do - cReq@UserContactRequest {welcomeSharedMsgId} <- withFastStore $ \db -> getContactRequest db user connReqId - (ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito - let contactUsed = isNothing gLinkInfo_ - ct' <- withStore' $ \db -> do - updateContactAccepted db user ct contactUsed - conn' <- - if sqSecured - then conn {connStatus = ConnSndReady} <$ updateConnectionStatusFromTo db connId ConnNew ConnSndReady - else pure conn - pure ct {contactUsed, activeConn = Just conn'} - when sqSecured $ forM_ (autoReply addressSettings) $ \mc -> case welcomeSharedMsgId of - Just smId -> - void $ sendDirectContactMessage user ct' $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing - Nothing -> do - (msg, _) <- sendDirectContactMessage user ct' $ XMsgNew $ MCSimple $ extMsgContent mc Nothing - ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) - toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] - pure $ CRAcceptingContactRequest user ct' + uclData_ <- withFastStore $ \db -> do + uclId_ <- getUserContactLinkIdByCReq db connReqId + forM uclId_ $ \uclId -> do -- address may be deleted + uclGLinkInfo <- getUserContactLinkById db userId uclId + pure (uclId, uclGLinkInfo) + withContactRequestLock "acceptContact" connReqId $ case uclData_ of + Nothing -> do -- address was deleted + when incognito $ throwCmdError "incognito not allowed when address is not found" + cReq <- withFastStore $ \db -> getContactRequest db user connReqId + (ct, _sqSecured) <- acceptCReq user cReq True + pure $ CRAcceptingContactRequest user ct + Just (uclId, (ucl@UserContactLink {shortLinkDataSet}, gLinkInfo_)) -> do + when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data" + withUserContactLock "acceptContact" uclId $ do + cReq <- withFastStore $ \db -> getContactRequest db user connReqId + let contactUsed = isNothing gLinkInfo_ -- for redundancy, as group link requests are auto-accepted + (ct, sqSecured) <- acceptCReq user cReq contactUsed + when sqSecured $ sendWelcomeMsg user ct ucl cReq + pure $ CRAcceptingContactRequest user ct + where + acceptCReq user cReq contactUsed = do + (ct, conn, sqSecured) <- acceptContactRequest nm user cReq incognito + ct' <- withStore' $ \db -> do + updateContactAccepted db user ct contactUsed + conn' <- + if sqSecured + then updateConnectionStatusFromTo db conn ConnNew ConnSndReady + else pure conn + pure ct {contactUsed, activeConn = Just conn'} + pure (ct', sqSecured) + sendWelcomeMsg user ct ucl UserContactRequest {welcomeSharedMsgId} = + forM_ (autoReply $ addressSettings ucl) $ \mc -> case welcomeSharedMsgId of + Just smId -> + void $ sendDirectContactMessage user ct $ XMsgUpdate smId mc M.empty Nothing Nothing Nothing + Nothing -> do + (msg, _) <- sendDirectContactMessage user ct $ XMsgNew $ MCSimple $ extMsgContent mc Nothing + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] APIRejectContact connReqId -> withUser $ \user -> do - userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId - withUserContactLock "rejectContact" userContactLinkId $ do - (cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId}, ct_) <- - withFastStore $ \db -> do - cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId - ct_ <- forM contactId_ $ \contactId -> do - ct <- getContact db vr user contactId - deleteContact db user ct - pure ct - liftIO $ deleteContactRequest db user connReqId - pure (cReq, ct_) - withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected user cReq ct_ + uclId_ <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId + withContactRequestLock "rejectContact" connReqId $ case uclId_ of + Nothing -> rejectCReq user -- address was deleted + Just uclId -> withUserContactLock "rejectContact" uclId $ rejectCReq user + where + rejectCReq user = do + (cReq@UserContactRequest {agentInvitationId = AgentInvId invId}, ct_) <- + withFastStore $ \db -> do + cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId + ct_ <- forM contactId_ $ \contactId -> do + ct <- getContact db vr user contactId + deleteContact db user ct + pure ct + liftIO $ deleteContactRequest db user connReqId + pure (cReq, ct_) + withAgent (`rejectContact` invId) + pure $ CRContactRequestRejected user cReq ct_ APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withFastStore $ \db -> getContact db vr user contactId @@ -1213,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 @@ -1327,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 @@ -1359,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 @@ -1393,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 = @@ -1464,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 @@ -1484,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_ @@ -1660,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 @@ -1682,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 @@ -1731,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 @@ -1799,11 +1814,16 @@ processChatCommand' vr = \case gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db vr user gInfo hostMember newUser pure $ CRGroupUserChanged user gInfo newUser gInfo' APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do - Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId case preparedContact of Nothing -> throwCmdError "contact doesn't have link to connect" Just PreparedContact {connLinkToConnect = ACCL SCMInvitation ccLink} -> do - (_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) + (_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) `catchChatError` \e -> do + -- get updated contact, in case connection was started - in UI it would lock ability to change + -- user or incognito profile for contact, in case server received request while client got network error + ct' <- withFastStore $ \db -> getContact db vr user contactId + toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') + throwError e -- get updated contact with connection ct' <- withFastStore $ \db -> getContact db vr user contactId forM_ msgContent_ $ \mc -> do @@ -1819,15 +1839,21 @@ processChatCommand' vr = \case smId <- getSharedMsgId withFastStore' $ \db -> setRequestSharedMsgIdForContact db contactId smId pure (smId, mc) - connectViaContact user incognito ccLink welcomeSharedMsgId msg_ (Just $ ACCGContact contactId) >>= \case - CRSentInvitation {customUserProfile} -> do + r <- connectViaContact user (Just $ PCEContact ct) incognito ccLink welcomeSharedMsgId msg_ `catchChatError` \e -> do + -- get updated contact, in case connection was started - in UI it would lock ability to change + -- user or incognito profile for contact, in case server received request while client got network error + ct' <- withFastStore $ \db -> getContact db vr user contactId + toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') + throwError e + case r of + CVRSentInvitation _conn customUserProfile -> do -- get updated contact with connection ct' <- withFastStore $ \db -> getContact db vr user contactId forM_ msg_ $ \(sharedMsgId, mc) -> do ci <- createChatItem user (CDDirectSnd ct') False (CISndMsgContent mc) (Just sharedMsgId) Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToContact user ct' customUserProfile - cr -> pure cr + CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' APIConnectPreparedGroup groupId incognito msgContent_ -> withUser $ \user -> do (gInfo, hostMember) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getHostMember db vr user groupId case preparedGroup gInfo of @@ -1839,8 +1865,14 @@ processChatCommand' vr = \case smId <- getSharedMsgId withFastStore' $ \db -> setRequestSharedMsgIdForGroup db groupId smId pure (smId, mc) - connectViaContact user incognito connLinkToConnect welcomeSharedMsgId msg_ (Just $ ACCGGroup gInfo $ groupMemberId' hostMember) >>= \case - CRSentInvitation {customUserProfile} -> do + r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchChatError` \e -> do + -- get updated group info, in case connection was started (connLinkPreparedConnection) - in UI it would lock ability to change + -- user or incognito profile for group or business chat, in case server received request while client got network error + gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId + toView $ CEvtChatInfoUpdated user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing) + throwError e + case r of + CVRSentInvitation _conn customUserProfile -> do -- get updated group info (connLinkStartedConnection and incognito membership) gInfo' <- withFastStore $ \db -> do liftIO $ setPreparedGroupStartedConnection db groupId @@ -1849,13 +1881,16 @@ processChatCommand' vr = \case ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing toView $ CEvtNewChatItems user [ci] pure $ CRStartedConnectionToGroup user gInfo' customUserProfile - cr -> pure cr + CVRConnectedContact _ct -> throwChatError $ CEException "contact already exists when connecting to group" APIConnect userId incognito acl -> withUserId userId $ \user -> case acl of ACCL SCMInvitation ccLink -> do - (dbConnId, incognitoProfile) <- connectViaInvitation user incognito ccLink Nothing - pcc <- withFastStore $ \db -> getPendingContactConnection db userId dbConnId + (conn, incognitoProfile) <- connectViaInvitation user incognito ccLink Nothing + let pcc = mkPendingContactConnection conn $ Just ccLink pure $ CRSentConfirmation user pcc incognitoProfile - ACCL SCMContact ccLink -> connectViaContact user incognito ccLink Nothing Nothing Nothing + ACCL SCMContact ccLink -> + connectViaContact user Nothing incognito ccLink Nothing Nothing >>= \case + CVRConnectedContact ct -> pure $ CRContactAlreadyExists user ct + CVRSentInvitation conn incognitoProfile -> pure $ CRSentInvitation user (mkPendingContactConnection conn Nothing) incognitoProfile Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e connectWithPlan user incognito ccLink plan @@ -1878,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 @@ -1901,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 @@ -1917,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 @@ -1931,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 @@ -1960,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) _ -> @@ -1979,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 @@ -1992,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 @@ -2048,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 @@ -2081,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 @@ -2099,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 @@ -2113,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 @@ -2136,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 @@ -2210,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 @@ -2278,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 @@ -2324,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 @@ -2410,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 @@ -2434,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 @@ -2446,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 @@ -2468,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' @@ -2489,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 @@ -2528,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? @@ -2552,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 @@ -2569,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) @@ -2580,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 @@ -2603,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 @@ -2619,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" @@ -2785,6 +2820,7 @@ processChatCommand' vr = \case CLContact ctId -> "Contact " <> tshow ctId CLGroup gId -> "Group " <> tshow gId CLUserContact ucId -> "UserContact " <> tshow ucId + CLContactRequest crId -> "ContactRequest " <> tshow crId CLFile fId -> "File " <> tshow fId DebugEvent event -> toView event >> ok_ GetAgentSubsTotal userId -> withUserId userId $ \user -> do @@ -2826,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 @@ -2858,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 @@ -2889,136 +2922,100 @@ processChatCommand' vr = \case CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwCmdError "not supported" - connectViaInvitation :: User -> IncognitoEnabled -> CreatedLinkInvitation -> Maybe ContactId -> CM (Int64, Maybe Profile) + connectViaInvitation :: User -> IncognitoEnabled -> CreatedLinkInvitation -> Maybe ContactId -> CM (Connection, Maybe Profile) connectViaInvitation user@User {userId} incognito (CCLink cReq@(CRInvitationUri crData e2e) sLnk_) contactId_ = withInvitationLock "connect" (strEncode cReq) $ do subMode <- chatReadVar subscriptionMode - -- [incognito] generate profile to send - -- TODO [short links] if incognito profile was prepared on the previous attempt, it should be used instead of creating a new one - -- TODO [short links] for connection via prepared contacts we need to: - -- - potentially use different flow or pass contact as parameter here, - -- - prohibit changing user/incognito on the second attempt in the UI - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing False lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case Nothing -> throwChatError CEInvalidConnReq -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan Just (agentV, pqSup') -> do let chatV = agentToChatVersion agentV - dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case - Nothing -> joinNewConn chatV dm - Just (RcvDirectMsgConnection conn@Connection {connId = dbConnId, connStatus, contactConnInitiated} _ct_) - | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link - | connStatus == ConnPrepared -> joinPreparedConn dbConnId (aConnId conn) dm -- retrying join after error + Nothing -> joinNewConn chatV + Just (RcvDirectMsgConnection conn@Connection {connStatus, contactConnInitiated, customUserProfileId} _ct_) + | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV -- own connection link + | connStatus == ConnPrepared -> do -- retrying join after error + localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId + joinPreparedConn conn (fromLocalProfile <$> localIncognitoProfile) chatV Just ent -> throwCmdError $ "connection is not RcvDirectMsgConnection: " <> show (connEntityInfo ent) where - joinNewConn chatV dm = do + joinNewConn chatV = do + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ - createdAt <- liftIO getCurrentTime - (dbConnId, _) <- withFastStore' $ \db -> createDirectConnection_ db userId connId ccLink contactId_ ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' createdAt - joinPreparedConn dbConnId connId dm - joinPreparedConn dbConnId connId dm = do - (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode + conn <- withFastStore' $ \db -> createDirectConnection' db userId connId ccLink contactId_ ConnPrepared incognitoProfile subMode chatV pqSup' + joinPreparedConn conn incognitoProfile chatV + joinPreparedConn conn incognitoProfile chatV = do + let profileToSend = userProfileToSend user incognitoProfile Nothing False + dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + (sqSecured, _serviceId) <- withAgent $ \a -> joinConnection a nm (aUserId user) (aConnId conn) True cReq dm pqSup' subMode let newStatus = if sqSecured then ConnSndReady else ConnJoined - withFastStore' $ \db -> updateConnectionStatusFromTo db dbConnId ConnPrepared newStatus - pure (dbConnId, incognitoProfile) + conn' <- withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus + pure (conn', incognitoProfile) cReqs = ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Maybe AttachConnToContactOrGroup -> CM ChatResponse - connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ attachConnTo_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do + connectViaContact :: User -> Maybe PreparedChatEntity -> IncognitoEnabled -> CreatedLinkContact -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> CM ConnectViaContactResult + connectViaContact user@User {userId} preparedEntity_ incognito (CCLink cReq@(CRContactUri crData@ConnReqUriData {crClientData}) sLnk) welcomeSharedMsgId msg_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli cReqHash = ConnReqUriHash . C.sha256Hash . strEncode cReqHash1 = cReqHash $ CRContactUri crData {crScheme = SSSimplex} cReqHash2 = cReqHash $ CRContactUri crData {crScheme = simplexChat} - case groupLinkId of - -- contact address + -- groupLinkId is Nothing for business chats + when (isJust msg_ && isJust groupLinkId) $ throwChatError CEConnReqMessageProhibited + case preparedEntity_ of + Just (PCEContact ct@Contact {activeConn}) -> case activeConn of + Nothing -> connect' Nothing cReqHash1 Nothing + Just conn@Connection {connStatus, xContactId} -> case connStatus of + ConnPrepared -> joinPreparedConn' xContactId conn False + _ -> pure $ CVRConnectedContact ct + Just (PCEGroup _gInfo GroupMember {activeConn}) -> case activeConn of + Nothing -> connect' groupLinkId cReqHash1 Nothing + Just conn@Connection {connStatus, xContactId} -> case connStatus of + ConnPrepared -> joinPreparedConn' xContactId conn $ isJust groupLinkId + _ -> connect' groupLinkId cReqHash1 xContactId -- why not "already connected" for host member? Nothing -> withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case - (Just Contact {activeConn = Just conn@Connection {connStatus = ConnPrepared}}, Just (xContactId, _)) -> do - incognitoProfile <- joinPreparedConn' xContactId conn - pure $ CRSentInvitation user (toPCC conn) incognitoProfile - (Just contact, _) -> pure $ CRContactAlreadyExists user contact - (Nothing, Just (xContactId, Just conn@Connection {connId, connStatus = ConnPrepared})) -> do - incognitoProfile <- joinPreparedConn' xContactId conn - -- TODO [short links] align Connection and PendingContactConnection so it doesn't need to be read again - pcc <- withStore $ \db -> getPendingContactConnection db userId connId - pure $ CRSentInvitation user pcc incognitoProfile - (Nothing, xContactId_) -> procCmd $ do + Right ct@Contact {activeConn} -> case groupLinkId of + Nothing -> case activeConn of + Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn False + _ -> pure $ CVRConnectedContact ct + Just gLinkId -> + -- allow repeat contact request + -- TODO [short links] is this branch needed? it probably remained from the time we created host contact + connect' (Just gLinkId) cReqHash1 Nothing + Left conn_ -> case conn_ of + Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn $ isJust groupLinkId -- TODO [short links] this is executed on repeat request after success -- it probably should send the second message without creating the second connection? - xContactId <- mkXContactId $ fst <$> xContactId_ - connect' Nothing cReqHash1 xContactId False - -- group link - Just gLinkId -> do - -- TODO [short links] reset "connection started" column - when (isJust msg_) $ throwChatError CEConnReqMessageProhibited - withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case - (Just _contact, _) -> procCmd $ do - -- allow repeat contact request - -- TODO [short links] is this branch needed? it probably remained from the time we created host contact - newXContactId <- XContactId <$> drgRandomBytes 16 - connect' (Just gLinkId) cReqHash1 newXContactId True - (Nothing, Just (xContactId, Just conn@Connection {connStatus = ConnPrepared})) -> do - incognitoProfile <- joinPreparedConn' xContactId conn - pure $ CRSentInvitation user (toPCC conn) incognitoProfile - (_, xContactId_) -> procCmd $ do - -- TODO [short links] this is executed on repeat request after success - -- it probably should send the second message without creating the second connection? - xContactId <- mkXContactId $ fst <$> xContactId_ - connect' (Just gLinkId) cReqHash1 xContactId True + Just Connection {xContactId} -> connect' groupLinkId cReqHash1 xContactId + Nothing -> connect' groupLinkId cReqHash1 Nothing where - joinPreparedConn' xContactId conn@Connection {connId = dbConnId, agentConnId = AgentConnId connId, connChatVersion = chatV, customUserProfileId = pId_} = do - incognitoProfile <- getOrCreateIncognitoProfile - joinContact user dbConnId connId cReq incognitoProfile xContactId welcomeSharedMsgId msg_ False PQSupportOn chatV - pure incognitoProfile - where - getOrCreateIncognitoProfile - | incognito = - withStore' $ \db -> case pId_ of - Nothing -> newIncognitoProfile db - Just pId -> - runExceptT (getProfileById db userId pId) - >>= either (\_ -> newIncognitoProfile db) (pure . Just . fromLocalProfile) - | otherwise = do - when (isJust pId_) $ withStore' $ \db -> - deleteIncognitoConnectionProfile db userId conn - pure Nothing - newIncognitoProfile db = do - p <- generateRandomProfile - createdAt <- liftIO getCurrentTime - void $ createIncognitoProfile_ db userId createdAt p - pure $ Just p - toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, createdAt} = - PendingContactConnection - { pccConnId = connId, - pccAgentConnId = agentConnId, - pccConnStatus = connStatus, - viaContactUri = True, - viaUserContactLink, - groupLinkId, - customUserProfileId, - connLinkInv = Nothing, - localAlias = "", - createdAt, - updatedAt = createdAt - } + joinPreparedConn' xContactId_ conn@Connection {customUserProfileId} inGroup = do + when (incognito /= isJust customUserProfileId) $ throwCmdError "incognito mode is different from prepared connection" + xContactId <- mkXContactId xContactId_ + localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId + let incognitoProfile = fromLocalProfile <$> localIncognitoProfile + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup PQSupportOn + pure $ CVRSentInvitation conn' incognitoProfile mkXContactId = maybe (XContactId <$> drgRandomBytes 16) pure - connect' groupLinkId cReqHash xContactId inGroup = do - let pqSup = if inGroup then PQSupportOff else PQSupportOn + connect' groupLinkId cReqHash xContactId_ = do + let inGroup = isJust groupLinkId + pqSup = if inGroup then PQSupportOff else PQSupportOn (connId, chatV) <- prepareContact user cReq pqSup + xContactId <- mkXContactId xContactId_ -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk - conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' attachConnTo_ xContactId incognitoProfile groupLinkId subMode chatV pqSup - joinContact user pccConnId connId cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup chatV - pure $ CRSentInvitation user conn incognitoProfile + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId preparedEntity_ cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup + conn' <- joinContact user conn cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup + pure $ CVRSentInvitation conn' incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse - connectContactViaAddress user incognito ct (CCLink cReq shortLink) = + connectContactViaAddress user@User {userId} incognito ct@Contact {contactId} (CCLink cReq shortLink) = withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do newXContactId <- XContactId <$> drgRandomBytes 16 let pqSup = PQSupportOn @@ -3027,8 +3024,9 @@ processChatCommand' vr = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash shortLink newXContactId incognitoProfile subMode chatV pqSup - joinContact user pccConnId connId cReq incognitoProfile newXContactId Nothing Nothing False pqSup chatV + conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReqHash shortLink newXContactId incognitoProfile Nothing subMode chatV pqSup + void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing False pqSup + ct' <- withStore $ \db -> getContact db vr user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact user cReq pqSup = do @@ -3041,13 +3039,13 @@ processChatCommand' vr = \case let chatV = agentToChatVersion agentV connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) - joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Bool -> PQSupport -> VersionChat -> CM () - joinContact user pccConnId connId cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup chatV = do + joinContact :: User -> Connection -> ConnReqContact -> Maybe Profile -> XContactId -> Maybe SharedMsgId -> Maybe (SharedMsgId, MsgContent) -> Bool -> PQSupport -> CM Connection + joinContact user conn@Connection {connChatVersion = chatV} cReq incognitoProfile xContactId welcomeSharedMsgId msg_ inGroup pqSup = do 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) connId True cReq dm pqSup subMode - withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared ConnJoined + 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} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -3070,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 @@ -3123,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'}} @@ -3183,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 () @@ -3286,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 @@ -3378,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 @@ -3395,6 +3393,7 @@ processChatCommand' vr = \case let inv cReq = ACCL SCMInvitation $ CCLink cReq (Just l') liftIO (getConnectionEntityViaShortLink db vr user l') >>= \case Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing ent) + -- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l' invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do plan <- invitationRequestPlan user cReq contactSLinkData_ `catchChatError` (pure . CPError) @@ -3406,6 +3405,7 @@ processChatCommand' vr = \case CLShort l@(CSLContact _ ct _ _) -> do let l' = serverShortLink l con cReq = ACCL SCMContact $ CCLink cReq (Just l') + gPlan (cReq, g) = if memberRemoved (membership g) then Nothing else Just (con cReq, CPGroupLink (GLPKnown g)) case ct of CCTContact -> knownLinkPlans >>= \case @@ -3424,8 +3424,8 @@ processChatCommand' vr = \case Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink) Nothing -> getContactViaShortLinkToConnect db vr user l' >>= \case - Just (cReq, ct') -> pure $ Just (con cReq, CPContactAddress (CAPKnown ct')) - Nothing -> bimap con (CPGroupLink . GLPKnown) <$$> getGroupViaShortLinkToConnect db vr user l' + Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct')) + Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' CCTGroup -> knownLinkPlans >>= \case Just r -> pure r @@ -3438,8 +3438,7 @@ processChatCommand' vr = \case knownLinkPlans = withFastStore $ \db -> liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) - Nothing -> - bimap con (CPGroupLink . GLPKnown ) <$$> getGroupViaShortLinkToConnect db vr user l' + Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' CCTChannel -> throwCmdError "channel links are not supported in this version" connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan @@ -3447,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 @@ -3498,7 +3497,7 @@ processChatCommand' vr = \case | contactDeleted ct -> pure $ CPContactAddress (CAPOk contactSLinkData_) | otherwise -> pure $ CPContactAddress (CAPKnown ct) -- TODO [short links] RcvGroupMsgConnection branch is deprecated? (old group link protocol?) - Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo + Just (RcvGroupMsgConnection _ gInfo _) -> groupPlan gInfo Nothing Just _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection or RcvGroupMsgConnection" groupJoinRequestPlan :: User -> ConnReqContact -> Maybe GroupShortLinkData -> CM ConnectionPlan groupJoinRequestPlan user (CRContactUri crData) groupSLinkData_ = do @@ -3517,15 +3516,14 @@ processChatCommand' vr = \case | not (contactReady ct) && contactActive ct -> pure $ CPGroupLink (GLPConnectingProhibit gInfo_) | otherwise -> pure $ CPGroupLink (GLPOk groupSLinkData_) (Nothing, Just _) -> throwCmdError "found connection entity is not RcvDirectMsgConnection" - (Just gInfo, _) -> groupPlan gInfo - groupPlan :: GroupInfo -> CM ConnectionPlan - groupPlan gInfo@GroupInfo {membership} + (Just gInfo, _) -> groupPlan gInfo groupSLinkData_ + groupPlan :: GroupInfo -> Maybe GroupShortLinkData -> CM ConnectionPlan + groupPlan gInfo@GroupInfo {membership} groupSLinkData_ | memberStatus membership == GSMemRejected = pure $ CPGroupLink (GLPKnown gInfo) | not (memberActive membership) && not (memberRemoved membership) = pure $ CPGroupLink (GLPConnectingProhibit $ Just gInfo) | memberActive membership = pure $ CPGroupLink (GLPKnown gInfo) - -- TODO [short links] entity is already found - passing GroupShortLinkData doesn't make sense? - | otherwise = pure $ CPGroupLink (GLPOk Nothing) + | otherwise = pure $ CPGroupLink (GLPOk groupSLinkData_) contactCReqSchemas :: ConnReqUriData -> (ConnReqContact, ConnReqContact) contactCReqSchemas crData = ( CRContactUri crData {crScheme = SSSimplex}, @@ -3536,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 () @@ -3590,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) @@ -3887,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 @@ -3898,6 +3896,10 @@ processChatCommand' vr = \case gVar <- asks random liftIO $ SharedMsgId <$> encodedRandomBytes gVar 12 +data ConnectViaContactResult + = CVRConnectedContact Contact + | CVRSentInvitation Connection (Maybe Profile) + protocolServers :: UserProtocol p => SProtocolType p -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) -> ([Maybe ServerOperator], [UserServer 'PSMP], [UserServer 'PXFTP]) protocolServers p (operators, smpServers, xftpServers) = case p of SPSMP -> (operators, smpServers, []) @@ -4053,27 +4055,13 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do where addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) - RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') + RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (mkPendingContactConnection c Nothing) pcs in (cts, ucs, ms, sfts, rfts, pcs') RcvGroupMsgConnection c _g m -> let ms' = addConn c (toShortMember m c) ms in (cts, ucs, ms', sfts, rfts, pcs) SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) addConn :: Connection -> a -> Map ConnId a -> Map ConnId a addConn = M.insert . aConnId - toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = - PendingContactConnection - { pccConnId = connId, - pccAgentConnId = agentConnId, - pccConnStatus = connStatus, - viaContactUri = False, - viaUserContactLink, - groupLinkId, - customUserProfileId, - connLinkInv = Nothing, - localAlias, - createdAt, - updatedAt = createdAt - } toShortMember GroupMember {groupMemberId, groupId, localDisplayName} Connection {agentConnId} = ShortGroupMember { groupMemberId, diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index cef0f1bb8a..81c4f080f4 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -84,7 +84,7 @@ import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Client (NetworkConfig (..)) +import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) @@ -142,6 +142,10 @@ withUserContactLock :: Text -> Int64 -> CM a -> CM a withUserContactLock name = withEntityLock name . CLUserContact {-# INLINE withUserContactLock #-} +withContactRequestLock :: Text -> Int64 -> CM a -> CM a +withContactRequestLock name = withEntityLock name . CLContactRequest +{-# INLINE withContactRequestLock #-} + withFileLock :: Text -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} @@ -877,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 @@ -887,28 +891,28 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId = (ct, conn, incognitoProfile) <- case contactId_ of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' (ct, conn) <- withStore' $ \db -> - createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False + createContactFromRequest db user userContactLinkId_ connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False pure (ct, conn, incognitoProfile) Just contactId -> do ct <- withFastStore $ \db -> getContact db vr user contactId case contactConn ct of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup' + connId <- withAgent $ \a -> prepareConnectionToAccept a (aUserId user) True invId pqSup' currentTs <- liftIO getCurrentTime conn <- withStore' $ \db -> do forM_ xContactId $ \xcId -> setContactAcceptedXContactId db ct xcId - createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs + createAcceptedContactConn db user userContactLinkId_ contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs pure (ct {activeConn = Just conn} :: Contact, conn, incognitoProfile) Just conn@Connection {customUserProfileId} -> do incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId pure (ct, conn, ExistingIncognito <$> incognitoProfile) - let profileToSend = profileToSendOnAccept user incognitoProfile False + let profileToSend = userProfileToSend' user incognitoProfile (Just ct) False dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend -- TODO [certs rcv] - (ct,conn,) . fst <$> withAgent (\a -> acceptContact a (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 @@ -918,14 +922,14 @@ acceptContactRequestAsync UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId, pqSupport = cReqPQSup} incognitoProfile = do subMode <- chatReadVar subscriptionMode - let profileToSend = profileToSendOnAccept user incognitoProfile False + let profileToSend = userProfileToSend' user incognitoProfile (Just ct) False vr <- chatVersionRange let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV currentTs <- liftIO getCurrentTime withStore $ \db -> do forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId - Connection {connId} <- liftIO $ createAcceptedContactConn db user uclId contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs + Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId getContact db vr user contactId @@ -947,7 +951,7 @@ acceptGroupJoinRequestAsync (groupMemberId, memberId) <- withStore $ \db -> createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile cReqXContactId_ welcomeMsgId_ gLinkMemRole initialStatus currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo - let Profile {displayName} = profileToSendOnAccept user incognitoProfile True + let Profile {displayName} = userProfileToSend' user incognitoProfile Nothing True GroupMember {memberRole = userRole, memberId = userMemberId} = membership msg = XGrpLinkInv $ @@ -1006,7 +1010,7 @@ acceptBusinessJoinRequestAsync clientMember@GroupMember {groupMemberId, memberId} UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId} = do vr <- chatVersionRange - let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True + let userProfile@Profile {displayName, preferences} = userProfileToSend' user Nothing Nothing True -- TODO [short links] take groupPreferences from group info groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences msg = @@ -2194,7 +2198,7 @@ agentAcceptContactAsync :: MsgEncodingI e => User -> Bool -> InvitationId -> Cha agentAcceptContactAsync user enableNtfs invId msg subMode pqSup chatV = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact dm <- encodeConnInfoPQ pqSup chatV msg - connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm pqSup subMode + connId <- withAgent $ \a -> acceptContactAsync a (aUserId user) (aCorrId cmdId) enableNtfs invId dm pqSup subMode pure (cmdId, connId) deleteAgentConnectionAsync :: ConnId -> CM () diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 4f82823118..31bf0b743f 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -849,8 +849,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (groupFeatureAllowed SGFHistory gInfo'' && not memberIsCustomer) $ sendHistory user gInfo'' m' where sendXGrpLinkMem gInfo'' = do - let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo'' - profileToSend = profileToSendOnAccept user profileMode True + let incognitoProfile = ExistingIncognito <$> incognitoMembershipProfile gInfo'' + profileToSend = userProfileToSend' user incognitoProfile Nothing True void $ sendDirectMemberMessage conn (XGrpLinkMem profileToSend) groupId _ -> do unless (memberPending m) $ withStore' $ \db -> updateGroupMemberStatus db userId m GSMemConnected diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 194fc1bb06..bb1f45fc67 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -97,8 +97,12 @@ foreign export ccall "chat_reopen_store" cChatReopenStore :: StablePtr ChatContr foreign export ccall "chat_send_cmd" cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString +foreign export ccall "chat_send_cmd_retry" cChatSendCmdRetry :: StablePtr ChatController -> CString -> CInt -> IO CJSONString + foreign export ccall "chat_send_remote_cmd" cChatSendRemoteCmd :: StablePtr ChatController -> CInt -> CString -> IO CJSONString +foreign export ccall "chat_send_remote_cmd_retry" cChatSendRemoteCmdRetry :: StablePtr ChatController -> CInt -> CString -> CInt -> IO CJSONString + foreign export ccall "chat_recv_msg" cChatRecvMsg :: StablePtr ChatController -> IO CJSONString foreign export ccall "chat_recv_msg_wait" cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString @@ -155,20 +159,30 @@ cChatReopenStore cPtr = do c <- deRefStablePtr cPtr newCAString =<< chatReopenStore c --- | send command to chat (same syntax as in terminal for now) +-- | send command to chat cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString -cChatSendCmd cPtr cCmd = do +cChatSendCmd cPtr cCmd = cChatSendCmdRetry cPtr cCmd 0 + +-- | send command to chat with retry count +cChatSendCmdRetry :: StablePtr ChatController -> CString -> CInt -> IO CJSONString +cChatSendCmdRetry cPtr cCmd cRetryNum = do c <- deRefStablePtr cPtr cmd <- B.packCString cCmd - newCStringFromLazyBS =<< chatSendCmd c cmd + newCStringFromLazyBS =<< chatSendRemoteCmdRetry c Nothing cmd (fromIntegral cRetryNum) +{-# INLINE cChatSendCmdRetry #-} --- | send command to chat (same syntax as in terminal for now) +-- | send remote command to chat cChatSendRemoteCmd :: StablePtr ChatController -> CInt -> CString -> IO CJSONString -cChatSendRemoteCmd cPtr cRemoteHostId cCmd = do +cChatSendRemoteCmd cPtr cRhId cCmd = cChatSendRemoteCmdRetry cPtr cRhId cCmd 0 + +-- | send remote command to chat with retry count +cChatSendRemoteCmdRetry :: StablePtr ChatController -> CInt -> CString -> CInt -> IO CJSONString +cChatSendRemoteCmdRetry cPtr cRemoteHostId cCmd cRetryNum = do c <- deRefStablePtr cPtr cmd <- B.packCString cCmd let rhId = Just $ fromIntegral cRemoteHostId - newCStringFromLazyBS =<< chatSendRemoteCmd c rhId cmd + newCStringFromLazyBS =<< chatSendRemoteCmdRetry c rhId cmd (fromIntegral cRetryNum) +{-# INLINE cChatSendRemoteCmdRetry #-} -- | receive message from chat (blocking) cChatRecvMsg :: StablePtr ChatController -> IO CJSONString @@ -294,10 +308,11 @@ handleErr :: IO () -> IO String handleErr a = (a $> "") `catch` (pure . show @SomeException) chatSendCmd :: ChatController -> B.ByteString -> IO JSONByteString -chatSendCmd cc = chatSendRemoteCmd cc Nothing +chatSendCmd cc cmd = chatSendRemoteCmdRetry cc Nothing cmd 0 +{-# INLINE chatSendCmd #-} -chatSendRemoteCmd :: ChatController -> Maybe RemoteHostId -> B.ByteString -> IO JSONByteString -chatSendRemoteCmd cc rh s = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s) cc +chatSendRemoteCmdRetry :: ChatController -> Maybe RemoteHostId -> B.ByteString -> Int -> IO JSONByteString +chatSendRemoteCmdRetry cc rh s retryNum = J.encode . eitherToResult rh <$> runReaderT (execChatCommand rh s retryNum) cc chatRecvMsg :: ChatController -> IO JSONByteString chatRecvMsg ChatController {outputQ} = J.encode . uncurry eitherToResult <$> readChatResponse @@ -312,6 +327,7 @@ chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) chatParseMarkdown :: ByteString -> JSONByteString chatParseMarkdown = J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 +{-# INLINE chatParseMarkdown #-} chatParseServer :: ByteString -> JSONByteString chatParseServer = J.encode . toServerAddress . strDecode diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index aa741df09b..f9427d6ab2 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 4, 0, 5] +minRemoteCtrlVersion = AppVersion [6, 4, 0, 5, 1] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 0, 5] +minRemoteHostVersion = AppVersion [6, 4, 0, 5, 1] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version @@ -366,8 +366,8 @@ getRemoteFile rhId rf = do createDirectoryIfMissing True dir liftRH rhId $ remoteGetFile c dir rf -processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> CM ChatResponse -processRemoteCommand remoteHostId c cmd s = case cmd of +processRemoteCommand :: RemoteHostId -> RemoteHostClient -> ChatCommand -> ByteString -> Int -> CM ChatResponse +processRemoteCommand remoteHostId c cmd s retryNum = case cmd of SendFile chatName f -> sendFile "/f" chatName f SendImage chatName f -> sendFile "/img" chatName f _ -> chatRemoteSend s @@ -380,7 +380,7 @@ processRemoteCommand remoteHostId c cmd s = case cmd of cryptoFileStr CryptoFile {filePath, cryptoArgs} = maybe "" (\(CFArgs key nonce) -> "key=" <> strEncode key <> " nonce=" <> strEncode nonce <> " ") cryptoArgs <> encodeUtf8 (T.pack filePath) - chatRemoteSend = either throwError pure <=< liftRH remoteHostId . remoteSend c + chatRemoteSend cmd' = either throwError pure =<< liftRH remoteHostId (remoteSend c cmd' retryNum) liftRH :: RemoteHostId -> ExceptT RemoteProtocolError IO a -> CM a liftRH rhId = liftError (ChatErrorRemoteHost (RHId rhId) . RHEProtocolError) @@ -497,8 +497,8 @@ parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo parseCtrlAppInfo ctrlAppInfo = do liftEitherWith (const $ ChatErrorRemoteCtrl RCEBadInvitation) $ JT.parseEither J.parseJSON ctrlAppInfo -handleRemoteCommand :: (ByteString -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () -handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do +handleRemoteCommand :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> RemoteCrypto -> TBQueue (Either ChatError ChatEvent) -> HTTP2Request -> CM' () +handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do logDebug "handleRemoteCommand" liftIO (tryRemoteError' parseRequest) >>= \case Right (rfKN, getNext, rc) -> do @@ -514,7 +514,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque replyError = reply . RRChatResponse . RRError processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () processCommand user rfKN getNext = \case - RCSend {command} -> lift $ handleSend execChatCommand command >>= reply + RCSend {command, retryNumber} -> lift $ handleSend execCC command retryNumber >>= reply RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile rfKN fileName fileSize fileDigest getNext >>= reply RCGetFile {file} -> handleGetFile user file replyWith @@ -550,12 +550,12 @@ tryRemoteError' :: ExceptT RemoteProtocolError IO a -> IO (Either RemoteProtocol tryRemoteError' = tryAllErrors' (RPEException . tshow) {-# INLINE tryRemoteError' #-} -handleSend :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM' RemoteResponse -handleSend execChatCommand command = do +handleSend :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> Int -> CM' RemoteResponse +handleSend execCC command retryNum = do logDebug $ "Send: " <> tshow command - -- execChatCommand checks for remote-allowed commands - -- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper - RRChatResponse . eitherToResult <$> execChatCommand (encodeUtf8 command) + -- execCC checks for remote-allowed commands + -- convert errors thrown in execCC into error responses to prevent aborting the protocol wrapper + RRChatResponse . eitherToResult <$> execCC (encodeUtf8 command) retryNum handleRecv :: Int -> TBQueue (Either ChatError ChatEvent) -> IO RemoteResponse handleRecv time events = do @@ -615,8 +615,8 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} -- | Take a look at emoji of tlsunique, commit pairing, and start session server -verifyRemoteCtrlSession :: (ByteString -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo -verifyRemoteCtrlSession execChatCommand sessCode' = do +verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo +verifyRemoteCtrlSession execCC sessCode' = do (sseq, client, ctrlName, sessionCode, vars) <- chatReadVar remoteCtrlSession >>= \case Nothing -> throwError $ ChatErrorRemoteCtrl RCEInactive @@ -631,7 +631,7 @@ verifyRemoteCtrlSession execChatCommand sessCode' = do remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls cc <- ask - http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execChatCommand encryption remoteOutputQ req `runReaderT` cc + http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execCC encryption remoteOutputQ req `runReaderT` cc void . forkIO $ monitor sseq http2Server updateRemoteCtrlSession sseq $ \case RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ} diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index 207eade665..d8155db7d6 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -56,7 +56,7 @@ import System.FilePath (takeFileName, ()) import UnliftIO data RemoteCommand - = RCSend {command :: Text} -- TODO maybe ChatCommand here? + = RCSend {command :: Text, retryNumber :: Int} | RCRecv {wait :: Int} -- this wait should be less than HTTP timeout | -- local file encryption is determined by the host, but can be overridden for videos RCStoreFile {fileName :: String, fileSize :: Word32, fileDigest :: FileDigest} -- requires attachment @@ -133,9 +133,9 @@ closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClien -- ** Commands -remoteSend :: RemoteHostClient -> ByteString -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) -remoteSend c cmd = - sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd} >>= \case +remoteSend :: RemoteHostClient -> ByteString -> Int -> ExceptT RemoteProtocolError IO (Either ChatError ChatResponse) +remoteSend c cmd retryNumber = + sendRemoteCommand' c Nothing RCSend {command = decodeUtf8 cmd, retryNumber} >>= \case RRChatResponse cr -> pure $ resultToEither cr r -> badResponse r diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 29c9ad29e2..3334653a2c 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -95,7 +95,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version @@ -140,7 +140,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index a56c89102b..ee68de5af2 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -113,7 +113,7 @@ createOrUpdateContactRequest cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct @@ -143,12 +143,11 @@ createOrUpdateContactRequest SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.xcontact_id = ? @@ -203,7 +202,7 @@ createOrUpdateContactRequest ct <- getContact db vr user contactId pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct) createBusinessChat = do - let Profile {preferences = userPreferences} = profileToSendOnAccept user Nothing True + let Profile {preferences = userPreferences} = userProfileToSend' user Nothing Nothing True groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs userPreferences (gInfo@GroupInfo {groupId}, clientMember) <- createBusinessRequestGroup db vr gVar user cReqChatVRange profile profileId ldn groupPreferences diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index fe0b1f64c7..f142da4553 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -26,12 +26,11 @@ module Simplex.Chat.Store.Direct -- * Contacts and connections functions getPendingContactConnection, deletePendingContactConnection, - createDirectConnection_, + createDirectConnection', createDirectConnection, createIncognitoProfile, createConnReqConnection, setPreparedGroupStartedConnection, - createAddressContactConnection, getProfileById, getConnReqContactXContactId, createPreparedContact, @@ -113,6 +112,7 @@ import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport) +import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Protocol (SubscriptionMode (..)) #if defined(dbPostgres) import Database.PostgreSQL.Simple (Only (..), Query, (:.) (..)) @@ -154,16 +154,11 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) -createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash sLnk xContactId incognitoProfile subMode chatV pqSup = do - PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk (Just $ ACCGContact contactId) xContactId incognitoProfile Nothing subMode chatV pqSup - (pccConnId,) <$> getContact db vr user contactId - -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> Maybe AttachConnToContactOrGroup -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash sLnk attachConnTo_ xContactId incognitoProfile groupLinkId subMode chatV pqSup = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> Maybe PreparedChatEntity -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection +createConnReqConnection db userId acId preparedEntity_ cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do currentTs <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile - let pccConnStatus = ConnPrepared + let connStatus = ConnPrepared DB.execute db [sql| @@ -174,23 +169,56 @@ createConnReqConnection db userId acId cReqHash sLnk attachConnTo_ xContactId in created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, pccConnStatus, connType, BI True) + ( (userId, acId, connStatus, connType, BI True) :. (cReqHash, sLnk, contactId_, groupMemberId_) :. (xContactId, customUserProfileId, BI (isJust groupLinkId), groupLinkId) :. (currentTs, currentTs, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) - pccConnId <- insertedRowId db - case attachConnTo_ of - Just (ACCGGroup gInfo _gmId) -> updatePreparedGroup gInfo pccConnId customUserProfileId currentTs + connId <- insertedRowId db + case preparedEntity_ of + Just (PCEGroup gInfo _) -> updatePreparedGroup gInfo customUserProfileId currentTs _ -> pure () - pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt = currentTs, updatedAt = currentTs} + pure + Connection + { connId, + agentConnId = AgentConnId acId, + connChatVersion = chatV, + -- TODO (proposed): + -- - add agent version 8 for short links + -- - update agentToChatVersion to convert 8 to 16 + -- - return and correctly set peer's range from link (via connRequestPQSupport) + peerChatVRange = chatInitialVRange, -- this is 1-1 + connLevel = 0, + viaContact = Nothing, + viaUserContactLink = Nothing, + viaGroupLink = isJust groupLinkId, + groupLinkId, + xContactId = Just xContactId, + customUserProfileId, + connType, + connStatus, + contactConnInitiated = True, + localAlias = "", + entityId, + connectionCode = Nothing, + pqSupport = pqSup, + pqEncryption = CR.pqSupportToEnc pqSup, + pqSndEnabled = Nothing, + pqRcvEnabled = Nothing, + authErrCounter = 0, + quotaErrCounter = 0, + createdAt = currentTs + } where - (connType, contactId_, groupMemberId_) = case attachConnTo_ of - Just (ACCGContact ctId) -> (ConnContact, Just ctId, Nothing) - Just (ACCGGroup _gInfo gmId) -> (ConnMember, Nothing, Just gmId) - Nothing -> (ConnContact, Nothing, Nothing) - updatePreparedGroup GroupInfo {groupId, membership} pccConnId customUserProfileId currentTs = do - setViaGroupLinkHash db groupId pccConnId + (connType, contactId_, groupMemberId_, entityId) = case preparedEntity_ of + Just (PCEContact Contact {contactId}) -> (ConnContact, Just contactId, Nothing, Just contactId) + Just (PCEGroup _ GroupMember {groupMemberId}) -> (ConnMember, Nothing, Just groupMemberId, Just groupMemberId) + Nothing -> (ConnContact, Nothing, Nothing, Nothing) + updatePreparedGroup GroupInfo {groupId, membership} customUserProfileId currentTs = do + DB.execute + db + "UPDATE groups SET via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ?" + (cReqHash, BI True, currentTs, groupId) when (isJust customUserProfileId) $ DB.execute db @@ -205,20 +233,17 @@ setPreparedGroupStartedConnection db groupId = do "UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?" (BI True, currentTs, groupId) -getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe Contact, Maybe (XContactId, Maybe Connection)) -getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = do - getContactByConnReqHash db vr user cReqHash1 cReqHash2 >>= \case - Just (xContactId_, ct@Contact {activeConn}) -> pure (Just ct, (,activeConn) <$> xContactId_) - Nothing -> (Nothing,) <$> getConnectionXContactId +getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Either (Maybe Connection) Contact) +getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = + getContactByConnReqHash db vr user cReqHash1 cReqHash2 >>= maybe (Left <$> getConnection) (pure . Right) where - getConnectionXContactId :: IO (Maybe (XContactId, Maybe Connection)) - getConnectionXContactId = - maybeFirstRow toConnectionAndXContactId $ + getConnection :: IO (Maybe Connection) + getConnection = + maybeFirstRow (toConnection vr) $ DB.query db [sql| - SELECT xcontact_id, - connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections @@ -227,24 +252,21 @@ getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = do LIMIT 1 |] (userId, cReqHash1, userId, cReqHash2) - toConnectionAndXContactId :: Only XContactId :. ConnectionRow -> (XContactId, Maybe Connection) - toConnectionAndXContactId (Only xContactId_ :. connRow) = (xContactId_, Just $ toConnection vr connRow) -getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe (Maybe XContactId, Contact)) +getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe Contact) getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do - r <- - maybeFirstRow toContactAndXContactId $ + ct <- + maybeFirstRow (toContact vr user []) $ DB.query db [sql| SELECT - c.xcontact_id, -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct @@ -258,18 +280,47 @@ getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do LIMIT 1 |] (userId, cReqHash1, userId, cReqHash2, CSActive) - mapM (traverse $ addDirectChatTags db) r - where - toContactAndXContactId :: Only (Maybe XContactId) :. (ContactRow :. MaybeConnectionRow) -> (Maybe XContactId, Contact) - toContactAndXContactId (Only xContactId_ :. ctRow) = (xContactId_, toContact vr user [] ctRow) + mapM (addDirectChatTags db) ct + +createDirectConnection' :: DB.Connection -> UserId -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO Connection +createDirectConnection' db userId acId ccLink contactId_ connStatus incognitoProfile subMode chatV pqSup = do + createdAt <- getCurrentTime + (connId, customUserProfileId, contactConnInitiated) <- createDirectConnection_ db userId acId ccLink contactId_ connStatus incognitoProfile subMode chatV pqSup createdAt + pure + Connection + { connId, + agentConnId = AgentConnId acId, + connChatVersion = chatV, + peerChatVRange = chatInitialVRange, -- see comment in createConnReqConnection + connLevel = 0, + viaContact = Nothing, + viaUserContactLink = Nothing, + viaGroupLink = False, + groupLinkId = Nothing, + xContactId = Nothing, + customUserProfileId, + connType = ConnContact, + connStatus, + contactConnInitiated, + localAlias = "", + entityId = contactId_, + connectionCode = Nothing, + pqSupport = pqSup, + pqEncryption = CR.pqSupportToEnc pqSup, + pqSndEnabled = Nothing, + pqRcvEnabled = Nothing, + authErrCounter = 0, + quotaErrCounter = 0, + createdAt + } createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection createDirectConnection db User {userId} acId ccLink contactId_ pccConnStatus incognitoProfile subMode chatV pqSup = do createdAt <- getCurrentTime - (pccConnId, customUserProfileId) <- createDirectConnection_ db userId acId ccLink contactId_ pccConnStatus incognitoProfile subMode chatV pqSup createdAt + (pccConnId, customUserProfileId, _) <- createDirectConnection_ db userId acId ccLink contactId_ pccConnStatus incognitoProfile subMode chatV pqSup createdAt pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connLinkInv = Just ccLink, localAlias = "", createdAt, updatedAt = createdAt} -createDirectConnection_ :: DB.Connection -> UserId -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> UTCTime -> IO (Int64, Maybe Int64) +createDirectConnection_ :: DB.Connection -> UserId -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> UTCTime -> IO (Int64, Maybe Int64, Bool) createDirectConnection_ db userId acId (CCLink cReq shortLinkInv) contactId_ pccConnStatus incognitoProfile subMode chatV pqSup createdAt = do customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let contactConnInitiated = pccConnStatus == ConnNew @@ -285,7 +336,7 @@ createDirectConnection_ db userId acId (CCLink cReq shortLinkInv) contactId_ pcc :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) dbConnId <- insertedRowId db - pure (dbConnId, customUserProfileId) + pure (dbConnId, customUserProfileId, contactConnInitiated) createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 createIncognitoProfile db User {userId} p = do @@ -708,7 +759,7 @@ getUserContacts db vr user@User {userId} = do contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts -getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64 +getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO (Maybe Int64) getUserContactLinkIdByCReq db contactRequestId = ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $ DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId) @@ -734,12 +785,11 @@ contactRequestQuery = SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) |] @@ -774,8 +824,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId, userId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createContactFromRequest :: DB.Connection -> User -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) -createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do +createContactFromRequest :: DB.Connection -> User -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection) +createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId_ agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do currentTs <- getCurrentTime let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences DB.execute @@ -784,7 +834,7 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc (userId, localDisplayName, profileId, BI True, userPreferences, currentTs, currentTs, currentTs, xContactId, BI contactUsed) contactId <- insertedRowId db DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName) - conn <- createAcceptedContactConn db user uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs + conn <- createAcceptedContactConn db user uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn ct = Contact @@ -813,12 +863,12 @@ createContactFromRequest db user@User {userId, profile = LocalProfile {preferenc } pure (ct, conn) -createAcceptedContactConn :: DB.Connection -> User -> Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection -createAcceptedContactConn db User {userId} uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do +createAcceptedContactConn :: DB.Connection -> User -> Maybe Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection +createAcceptedContactConn db User {userId} uclId_ contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do customUserProfileId <- forM incognitoProfile $ \case NewIncognito p -> createIncognitoProfile_ db userId currentTs p ExistingIncognito LocalProfile {profileId = pId} -> pure pId - createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just uclId) customUserProfileId 0 currentTs subMode pqSup + createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing uclId_ customUserProfileId 0 currentTs subMode pqSup updateContactAccepted :: DB.Connection -> User -> Contact -> Bool -> IO () updateContactAccepted db User {userId} Contact {contactId} contactUsed = @@ -857,7 +907,7 @@ getContact_ db vr user@User {userId} contactId deleted = do cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct @@ -910,7 +960,7 @@ getContactConnections db vr userId Contact {contactId} = DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -928,7 +978,7 @@ getConnectionById db vr User {userId} connId = ExceptT $ do DB.query db [sql| - SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version @@ -969,11 +1019,11 @@ updateConnectionStatus :: DB.Connection -> Connection -> ConnStatus -> IO () updateConnectionStatus db Connection {connId} = updateConnectionStatus_ db connId {-# INLINE updateConnectionStatus #-} -updateConnectionStatusFromTo :: DB.Connection -> Int64 -> ConnStatus -> ConnStatus -> IO () -updateConnectionStatusFromTo db connId fromStatus toStatus = do +updateConnectionStatusFromTo :: DB.Connection -> Connection -> ConnStatus -> ConnStatus -> IO Connection +updateConnectionStatusFromTo db conn@Connection {connId} fromStatus toStatus = do maybeFirstRow fromOnly (DB.query db "SELECT conn_status FROM connections WHERE connection_id = ?" (Only connId)) >>= \case - Just status | status == fromStatus -> updateConnectionStatus_ db connId toStatus - _ -> pure () + Just status | status == fromStatus -> updateConnectionStatus_ db connId toStatus $> conn {connStatus = toStatus} + _ -> pure conn updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO () updateConnectionStatus_ db connId connStatus = do diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index e797f08f54..51cbcd6b61 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -217,7 +217,7 @@ getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -939,7 +939,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1011,7 +1011,7 @@ groupMemberQuery = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -1821,7 +1821,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -1836,7 +1836,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -2464,7 +2464,7 @@ createMemberContact db "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ?" (contactId, currentTs, memberContactProfileId) - DB.execute + DB.execute -- why do we insert conn_req_inv here? how is it used? db [sql| INSERT INTO connections ( @@ -2489,6 +2489,7 @@ createMemberContact viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, + xContactId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, @@ -2623,6 +2624,7 @@ createMemberContactConn_ viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, + xContactId = Nothing, customUserProfileId, connLevel, connStatus = ConnJoined, diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index b84e3fe4ec..8c7c02d709 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -1051,12 +1051,11 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 7edaf796e7..02e2378d2c 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -9,6 +9,8 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links import Simplex.Chat.Store.Postgres.Migrations.M20250512_member_admission import Simplex.Chat.Store.Postgres.Migrations.M20250513_group_scope import Simplex.Chat.Store.Postgres.Migrations.M20250526_short_links +import Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete +import Simplex.Chat.Store.Postgres.Migrations.M20250704_groups_conn_link_prepared_connection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -17,7 +19,9 @@ schemaMigrations = ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope), - ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links) + ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links), + ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete), + ("20250704_groups_conn_link_prepared_connection", m20250704_groups_conn_link_prepared_connection, Just down_m20250704_groups_conn_link_prepared_connection) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs new file mode 100644 index 0000000000..b3868c82d7 --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250702_contact_requests_remove_cascade_delete.hs @@ -0,0 +1,39 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250702_contact_requests_remove_cascade_delete where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250702_contact_requests_remove_cascade_delete :: Text +m20250702_contact_requests_remove_cascade_delete = + T.pack + [r| +ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey; + +ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id DROP NOT NULL; + +ALTER TABLE contact_requests + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey + FOREIGN KEY (user_contact_link_id) + REFERENCES user_contact_links(user_contact_link_id) + ON UPDATE CASCADE + ON DELETE SET NULL; +|] + +down_m20250702_contact_requests_remove_cascade_delete :: Text +down_m20250702_contact_requests_remove_cascade_delete = + T.pack + [r| +ALTER TABLE contact_requests DROP CONSTRAINT contact_requests_user_contact_link_id_fkey; + +ALTER TABLE contact_requests ALTER COLUMN user_contact_link_id SET NOT NULL; + +ALTER TABLE contact_requests + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey + FOREIGN KEY (user_contact_link_id) + REFERENCES user_contact_links(user_contact_link_id) + ON UPDATE CASCADE + ON DELETE CASCADE; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20250704_groups_conn_link_prepared_connection.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20250704_groups_conn_link_prepared_connection.hs new file mode 100644 index 0000000000..dae6cd60ad --- /dev/null +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20250704_groups_conn_link_prepared_connection.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.Postgres.Migrations.M20250704_groups_conn_link_prepared_connection where + +import Data.Text (Text) +import qualified Data.Text as T +import Text.RawString.QQ (r) + +m20250704_groups_conn_link_prepared_connection :: Text +m20250704_groups_conn_link_prepared_connection = + T.pack + [r| +ALTER TABLE groups ADD COLUMN conn_link_prepared_connection SMALLINT NOT NULL DEFAULT 0; +|] + +down_m20250704_groups_conn_link_prepared_connection :: Text +down_m20250704_groups_conn_link_prepared_connection = + T.pack + [r| +ALTER TABLE groups DROP COLUMN conn_link_prepared_connection; +|] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 377d923256..4115c8a491 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -349,7 +349,7 @@ ALTER TABLE test_chat_schema.contact_profiles ALTER COLUMN contact_profile_id AD CREATE TABLE test_chat_schema.contact_requests ( contact_request_id bigint NOT NULL, - user_contact_link_id bigint NOT NULL, + user_contact_link_id bigint, agent_invitation_id bytea NOT NULL, contact_profile_id bigint, local_display_name text NOT NULL, @@ -645,7 +645,8 @@ CREATE TABLE test_chat_schema.groups ( conn_short_link_to_connect bytea, conn_link_started_connection smallint DEFAULT 0 NOT NULL, welcome_shared_msg_id bytea, - request_shared_msg_id bytea + request_shared_msg_id bytea, + conn_link_prepared_connection smallint DEFAULT 0 NOT NULL ); @@ -2313,7 +2314,7 @@ ALTER TABLE ONLY test_chat_schema.contact_requests ALTER TABLE ONLY test_chat_schema.contact_requests - ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE CASCADE; + ADD CONSTRAINT contact_requests_user_contact_link_id_fkey FOREIGN KEY (user_contact_link_id) REFERENCES test_chat_schema.user_contact_links(user_contact_link_id) ON UPDATE CASCADE ON DELETE SET NULL; @@ -2689,3 +2690,6 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links ALTER TABLE ONLY test_chat_schema.xftp_file_descriptions ADD CONSTRAINT xftp_file_descriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE; + + + diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index aaca217626..7fca9fed3f 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -370,7 +370,7 @@ getUserAddressConnection db vr User {userId} = do DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -386,7 +386,7 @@ getUserContactLinks db vr User {userId} = <$> DB.query db [sql| - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 14ee9a2567..ed78c8092a 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -132,6 +132,8 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links import Simplex.Chat.Store.SQLite.Migrations.M20250512_member_admission import Simplex.Chat.Store.SQLite.Migrations.M20250513_group_scope import Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links +import Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete +import Simplex.Chat.Store.SQLite.Migrations.M20250704_groups_conn_link_prepared_connection import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -263,7 +265,9 @@ schemaMigrations = ("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links), ("20250512_member_admission", m20250512_member_admission, Just down_m20250512_member_admission), ("20250513_group_scope", m20250513_group_scope, Just down_m20250513_group_scope), - ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links) + ("20250526_short_links", m20250526_short_links, Just down_m20250526_short_links), + ("20250702_contact_requests_remove_cascade_delete", m20250702_contact_requests_remove_cascade_delete, Just down_m20250702_contact_requests_remove_cascade_delete), + ("20250704_groups_conn_link_prepared_connection", m20250704_groups_conn_link_prepared_connection, Just down_m20250704_groups_conn_link_prepared_connection) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs new file mode 100644 index 0000000000..c758a962cd --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250702_contact_requests_remove_cascade_delete.hs @@ -0,0 +1,46 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250702_contact_requests_remove_cascade_delete where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250702_contact_requests_remove_cascade_delete :: Query +m20250702_contact_requests_remove_cascade_delete = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace( + replace( + sql, + 'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links', + 'user_contact_link_id INTEGER REFERENCES user_contact_links' + ), + 'ON UPDATE CASCADE ON DELETE CASCADE,', + 'ON UPDATE CASCADE ON DELETE SET NULL,' + ) +WHERE name = 'contact_requests' AND type = 'table'; + +PRAGMA writable_schema=0; +|] + +down_m20250702_contact_requests_remove_cascade_delete :: Query +down_m20250702_contact_requests_remove_cascade_delete = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = replace( + replace( + sql, + 'ON UPDATE CASCADE ON DELETE SET NULL,', + 'ON UPDATE CASCADE ON DELETE CASCADE,' + ), + 'user_contact_link_id INTEGER REFERENCES user_contact_links', + 'user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links' + ) +WHERE name = 'contact_requests' AND type = 'table'; + +PRAGMA writable_schema=0; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250704_groups_conn_link_prepared_connection.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250704_groups_conn_link_prepared_connection.hs new file mode 100644 index 0000000000..e8f559969a --- /dev/null +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250704_groups_conn_link_prepared_connection.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Store.SQLite.Migrations.M20250704_groups_conn_link_prepared_connection where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20250704_groups_conn_link_prepared_connection :: Query +m20250704_groups_conn_link_prepared_connection = + [sql| +ALTER TABLE groups ADD COLUMN conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0; +|] + +down_m20250704_groups_conn_link_prepared_connection :: Query +down_m20250704_groups_conn_link_prepared_connection = + [sql| +ALTER TABLE groups DROP COLUMN conn_link_prepared_connection; +|] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index a85ba4a4cb..ee717e71f5 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -274,7 +274,7 @@ Plan: Query: INSERT INTO conn_invitations - (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); + (invitation_id, contact_conn_id, cr_invitation, recipient_conn_info, accepted) VALUES (?, ?, ?, ?, 0); Plan: @@ -822,7 +822,7 @@ Query: DELETE FROM commands WHERE command_id = ? Plan: SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) -Query: DELETE FROM conn_invitations WHERE contact_conn_id = ? AND invitation_id = ? +Query: DELETE FROM conn_invitations WHERE invitation_id = ? Plan: SEARCH conn_invitations USING PRIMARY KEY (invitation_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 7a2d03117b..2ff3164c5d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -63,7 +63,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -200,7 +200,7 @@ Query: cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct @@ -398,12 +398,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.xcontact_id = ? @@ -412,7 +411,6 @@ Query: Plan: SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT COUNT(1) @@ -608,7 +606,7 @@ Plan: SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) Query: - SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version @@ -618,6 +616,22 @@ Query: Plan: SEARCH connections USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) +Query: + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, + contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, + conn_chat_version, peer_chat_min_version, peer_chat_max_version + FROM connections + WHERE (user_id = ? AND via_contact_uri_hash = ?) + OR (user_id = ? AND via_contact_uri_hash = ?) + LIMIT 1 + +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +INDEX 2 +SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) + Query: SELECT ct.contact_id FROM group_members m @@ -712,23 +726,6 @@ SEARCH m USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH g USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN SEARCH h USING INDEX idx_sent_probe_hashes_sent_probe_id (sent_probe_id=?) -Query: - SELECT xcontact_id, - connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, - contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, - conn_chat_version, peer_chat_min_version, peer_chat_max_version - FROM connections - WHERE (user_id = ? AND via_contact_uri_hash = ?) - OR (user_id = ? AND via_contact_uri_hash = ?) - LIMIT 1 - -Plan: -MULTI-INDEX OR -INDEX 1 -SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) -INDEX 2 -SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) - Query: UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND item_status = ? AND chat_item_id = ? @@ -933,13 +930,43 @@ SEARCH i USING INTEGER PRIMARY KEY (rowid=?) SEARCH f USING INDEX idx_files_chat_item_id (chat_item_id=?) LEFT-JOIN SEARCH ri USING COVERING INDEX idx_chat_items_direct_shared_msg_id (user_id=? AND contact_id=? AND shared_msg_id=?) LEFT-JOIN +Query: + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, + ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, + c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id + JOIN connections c ON c.contact_id = ct.contact_id + WHERE + ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR + (c.user_id = ? AND c.via_contact_uri_hash = ?) + ) AND ct.contact_status = ? AND ct.deleted = 0 + ORDER BY c.created_at DESC + LIMIT 1 + +Plan: +MULTI-INDEX OR +INDEX 1 +SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +INDEX 2 +SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) +SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) +SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) +USE TEMP B-TREE FOR ORDER BY + Query: SELECT -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -954,7 +981,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -984,43 +1011,12 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN CORRELATED SCALAR SUBQUERY 1 SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) -Query: - SELECT - c.xcontact_id, - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, - ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, - c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version - FROM contacts ct - JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id - JOIN connections c ON c.contact_id = ct.contact_id - WHERE - ( (c.user_id = ? AND c.via_contact_uri_hash = ?) OR - (c.user_id = ? AND c.via_contact_uri_hash = ?) - ) AND ct.contact_status = ? AND ct.deleted = 0 - ORDER BY c.created_at DESC - LIMIT 1 - -Plan: -MULTI-INDEX OR -INDEX 1 -SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) -INDEX 2 -SEARCH c USING INDEX idx_connections_via_contact_uri_hash (user_id=? AND via_contact_uri_hash=?) -SEARCH ct USING INTEGER PRIMARY KEY (rowid=?) -SEARCH cp USING INTEGER PRIMARY KEY (rowid=?) -USE TEMP B-TREE FOR ORDER BY - Query: SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1086,7 +1082,7 @@ SEARCH connections USING INDEX idx_connections_via_contact_uri_hash (user_id=? A USE TEMP B-TREE FOR ORDER BY Query: - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -1532,7 +1528,7 @@ Query: cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.welcome_shared_msg_id, ct.request_shared_msg_id, ct.contact_request_id, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct @@ -1631,12 +1627,11 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? @@ -1655,18 +1650,16 @@ Plan: SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=? AND updated_at?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id JOIN user_contact_links uc ON uc.user_contact_link_id = cr.user_contact_link_id WHERE cr.user_id = ? @@ -1715,7 +1706,6 @@ Plan: SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?) SEARCH cr USING INDEX idx_contact_requests_updated_at (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT @@ -2932,7 +2922,7 @@ Plan: SEARCH chat_items USING COVERING INDEX idx_chat_items_notes (user_id=? AND note_folder_id=? AND item_status=?) Query: - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -2945,7 +2935,7 @@ SEARCH uc USING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -2958,7 +2948,7 @@ SEARCH uc USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: - SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, @@ -3145,7 +3135,7 @@ Plan: SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, + SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, xcontact_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version @@ -4692,7 +4682,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -4718,7 +4708,7 @@ Query: g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -4742,35 +4732,31 @@ Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.business_group_id = ? Plan: SEARCH cr USING INDEX idx_contact_requests_business_group_id (business_group_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.contact_id, cr.business_group_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, + cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, cr.pq_support, cr.welcome_shared_msg_id, cr.request_shared_msg_id, p.preferences, cr.created_at, cr.updated_at, cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr - JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) WHERE cr.user_id = ? AND cr.contact_request_id = ? Plan: SEARCH cr USING INTEGER PRIMARY KEY (rowid=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) -SEARCH c USING INDEX idx_connections_user_contact_link_id (user_contact_link_id=?) Query: SELECT @@ -4778,7 +4764,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4811,7 +4797,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4836,7 +4822,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4861,7 +4847,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4886,7 +4872,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4911,7 +4897,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -4936,7 +4922,7 @@ Query: m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, m.created_at, m.updated_at, m.support_chat_ts, m.support_chat_items_unread, m.support_chat_items_member_attention, m.support_chat_items_mentions, m.support_chat_last_msg_from_member_ts, - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.xcontact_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version @@ -6242,6 +6228,10 @@ Query: UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND g Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: UPDATE groups SET via_group_link_uri_hash = ?, conn_link_prepared_connection = ?, updated_at = ? WHERE group_id = ? +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE note_folders SET chat_ts = ? WHERE user_id = ? AND note_folder_id = ? Plan: SEARCH note_folders USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 6cf5a8f630..913a99b6ec 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -145,7 +145,8 @@ CREATE TABLE groups( conn_short_link_to_connect BLOB, conn_link_started_connection INTEGER NOT NULL DEFAULT 0, welcome_shared_msg_id BLOB, - request_shared_msg_id BLOB, -- received + request_shared_msg_id BLOB, + conn_link_prepared_connection INTEGER NOT NULL DEFAULT 0, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -344,8 +345,8 @@ CREATE TABLE user_contact_links( ); CREATE TABLE contact_requests( contact_request_id INTEGER PRIMARY KEY, - user_contact_link_id INTEGER NOT NULL REFERENCES user_contact_links - ON UPDATE CASCADE ON DELETE CASCADE, + user_contact_link_id INTEGER REFERENCES user_contact_links + ON UPDATE CASCADE ON DELETE SET NULL, agent_invitation_id BLOB NOT NULL, contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 4bcfa10361..90246f0710 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -64,6 +64,7 @@ data ChatLockEntity | CLContact ContactId | CLGroup GroupId | CLUserContact Int64 + | CLContactRequest Int64 | CLFile Int64 deriving (Eq, Ord) @@ -196,12 +197,12 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, BoolInt, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, BoolInt, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, BoolInt, Maybe GroupLinkId, Maybe XContactId) :. (Maybe Int64, ConnStatus, ConnType, BoolInt, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe BoolInt, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe BoolInt, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe BoolInt, Maybe GroupLinkId, Maybe XContactId) :. (Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe BoolInt, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) toConnection :: VersionRangeChat -> ConnectionRow -> Connection -toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, BI contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) = +toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, BI contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) = Connection { connId, agentConnId = AgentConnId acId, @@ -212,6 +213,7 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI vi viaUserContactLink, viaGroupLink, groupLinkId, + xContactId, customUserProfileId, connStatus, connType, @@ -237,8 +239,8 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI vi entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection -toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) = - Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) +toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) = + Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) toMaybeConnection _ _ = Nothing createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection @@ -272,7 +274,8 @@ createConnection_ db userId connType entityId acId connStatus connChatVersion pe viaContact, viaUserContactLink, viaGroupLink, - groupLinkId = Nothing, + groupLinkId = Nothing, -- should it be set to viaLinkGroupId + xContactId = Nothing, customUserProfileId, connLevel, connStatus, @@ -431,21 +434,6 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] (userId, profileId, userId, profileId, userId, profileId) -deleteIncognitoConnectionProfile :: DB.Connection -> UserId -> Connection -> IO () -deleteIncognitoConnectionProfile db userId Connection {connId, customUserProfileId} = - forM_ customUserProfileId $ \profileId -> do - DB.execute db "UPDATE connections SET custom_user_profile_id = NULL WHERE connection_id = ?" (Only connId) - DB.execute - db - [sql| - DELETE FROM contact_profiles - WHERE user_id = ? AND contact_profile_id = ? - AND NOT EXISTS (SELECT 1 FROM contacts WHERE contact_profile_id = ?) - AND NOT EXISTS (SELECT 1 FROM contact_requests WHERE contact_profile_id = ?) - AND NOT EXISTS (SELECT 1 FROM group_members WHERE contact_profile_id = ? OR member_profile_id = ?) - |] - (userId, profileId, profileId, profileId, profileId, profileId) - type PreparedContactRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe SharedMsgId, Maybe SharedMsgId) type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. PreparedContactRow :. (Maybe Int64, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) @@ -488,13 +476,13 @@ getProfileById db userId profileId = toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Int64) :. (AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Maybe GroupId, Maybe Int64) :. (Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe SharedMsgId, Maybe SharedMsgId, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId) :. (agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_) :. (profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, preferences, createdAt, updatedAt, minVer, maxVer)) = do let profile = Profile {displayName, fullName, image, contactLink, preferences} cReqChatVRange = fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer - in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} + in UserContactRequest {contactRequestId, agentInvitationId, contactId_, businessGroupId_, userContactLinkId_, cReqChatVRange, localDisplayName, profileId, profile, xContactId, pqSupport, welcomeSharedMsgId, requestSharedMsgId, createdAt, updatedAt} userQuery :: Query userQuery = @@ -620,7 +608,7 @@ safeDeleteLDN db User {userId} localDisplayName = do |] (userId, localDisplayName, userId) -type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, Maybe SharedMsgId, Maybe SharedMsgId) +type PreparedGroupRow = (Maybe ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, Maybe SharedMsgId, Maybe SharedMsgId) type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) @@ -640,8 +628,8 @@ toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, toPreparedGroup :: PreparedGroupRow -> Maybe PreparedGroup toPreparedGroup = \case - (Just fullLink, shortLink_, BI connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId) -> - Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId} + (Just fullLink, shortLink_, BI connLinkPreparedConnection, BI connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId) -> + Just PreparedGroup {connLinkToConnect = CCLink fullLink shortLink_, connLinkPreparedConnection, connLinkStartedConnection, welcomeSharedMsgId, requestSharedMsgId} _ -> Nothing toGroupMember :: Int64 -> GroupMemberRow -> GroupMember @@ -676,7 +664,7 @@ groupInfoQuery = g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, - g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, + g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.conn_link_prepared_connection, g.conn_link_started_connection, g.welcome_shared_msg_id, g.request_shared_msg_id, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 7c52f59a50..b7eebd141a 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -14,7 +14,6 @@ module Simplex.Chat.Terminal.Input where import Control.Applicative (optional, (<|>)) import Control.Concurrent (forkFinally, forkIO, killThread, mkWeakThreadId, threadDelay) import Control.Monad -import Control.Monad.Except import Control.Monad.Reader import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (second) @@ -63,7 +62,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do cmd = parseChatCommand bs rh' = if either (const False) allowRemoteCommand cmd then rh else Nothing unless (isMessage cmd) $ echo s - r <- runReaderT (execChatCommand rh' bs) cc + r <- execChatCommand rh' bs 0 `runReaderT` cc case r of Right r' -> processResp cmd rh r' Left _ -> when (isMessage cmd) $ echo s @@ -150,7 +149,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do sendUpdatedLiveMessage :: ChatController -> String -> LiveMessage -> Bool -> IO (Either ChatError ChatResponse) sendUpdatedLiveMessage cc sentMsg LiveMessage {chatName, chatItemId} live = do let cmd = UpdateLiveMessage chatName chatItemId live $ T.pack sentMsg - runExceptT (processChatCommand cmd) `runReaderT` cc + execChatCommand' cmd 0 `runReaderT` cc runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index a43cad05b4..5731be7b0b 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -15,7 +15,6 @@ import Control.Concurrent (ThreadId) import Control.Logger.Simple import Control.Monad import Control.Monad.Catch (MonadMask) -import Control.Monad.Except import Control.Monad.Reader import Data.List (intercalate) import Data.Text (Text) @@ -23,7 +22,7 @@ import qualified Data.Text as T import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Simplex.Chat.Controller -import Simplex.Chat.Library.Commands (execChatCommand, processChatCommand) +import Simplex.Chat.Library.Commands (execChatCommand, execChatCommand') import Simplex.Chat.Markdown import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent (CIContent (..), SMsgDirection (..)) @@ -166,11 +165,11 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha (True, CISRcvNew) -> do let itemId = chatItemId' ci chatRef = chatInfoToRef chat - void $ runReaderT (runExceptT $ processChatCommand (APIChatItemsRead chatRef [itemId])) cc + void $ runReaderT (execChatCommand' (APIChatItemsRead chatRef [itemId]) 0) cc _ -> pure () logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s getRemoteUser rhId = - runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case + runReaderT (execChatCommand (Just rhId) "/user" 0) cc >>= \case Right CRActiveUser {user} -> updateRemoteUser ct user rhId cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct) diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index eb61988f8a..739f5943b9 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -347,8 +347,7 @@ data UserContactRequest = UserContactRequest agentInvitationId :: AgentInvId, contactId_ :: Maybe ContactId, businessGroupId_ :: Maybe GroupId, - userContactLinkId :: Int64, - agentContactConnId :: AgentConnId, -- connection id of user contact + userContactLinkId_ :: Maybe Int64, cReqChatVRange :: VersionRangeChat, localDisplayName :: ContactName, profileId :: Int64, @@ -495,6 +494,7 @@ instance ToField BusinessChatType where toField = toField . textEncode data PreparedGroup = PreparedGroup { connLinkToConnect :: CreatedLinkContact, + connLinkPreparedConnection :: Bool, connLinkStartedConnection :: Bool, welcomeSharedMsgId :: Maybe SharedMsgId, -- it is stored only for business chats, and only if welcome message is specified requestSharedMsgId :: Maybe SharedMsgId @@ -511,7 +511,7 @@ data GroupSummary = GroupSummary data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember] -data AttachConnToContactOrGroup = ACCGContact ContactId | ACCGGroup GroupInfo GroupMemberId +data PreparedChatEntity = PCEContact Contact | PCEGroup {groupInfo :: GroupInfo, hostMember :: GroupMember} contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId) contactAndGroupIds = \case @@ -647,10 +647,10 @@ redactedMemberProfile Profile {displayName, fullName, image} = data IncognitoProfile = NewIncognito Profile | ExistingIncognito LocalProfile -profileToSendOnAccept :: User -> Maybe IncognitoProfile -> Bool -> Profile -profileToSendOnAccept user ip = userProfileToSend user (getIncognitoProfile <$> ip) Nothing +userProfileToSend' :: User -> Maybe IncognitoProfile -> Maybe Contact -> Bool -> Profile +userProfileToSend' user ip = userProfileToSend user (fromIncognitoProfile <$> ip) where - getIncognitoProfile = \case + fromIncognitoProfile = \case NewIncognito p -> p ExistingIncognito lp -> fromLocalProfile lp @@ -1556,6 +1556,7 @@ data Connection = Connection viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" viaGroupLink :: Bool, -- whether contact connected via group link groupLinkId :: Maybe GroupLinkId, + xContactId :: Maybe XContactId, customUserProfileId :: Maybe Int64, connType :: ConnType, connStatus :: ConnStatus, @@ -1619,7 +1620,7 @@ data PendingContactConnection = PendingContactConnection { pccConnId :: Int64, pccAgentConnId :: AgentConnId, pccConnStatus :: ConnStatus, - viaContactUri :: Bool, + viaContactUri :: Bool, -- whether connection was created via contact request to a contact link viaUserContactLink :: Maybe Int64, groupLinkId :: Maybe GroupLinkId, customUserProfileId :: Maybe Int64, @@ -1630,6 +1631,22 @@ data PendingContactConnection = PendingContactConnection } deriving (Eq, Show) +mkPendingContactConnection :: Connection -> Maybe CreatedLinkInvitation -> PendingContactConnection +mkPendingContactConnection Connection {connId, agentConnId, connStatus, xContactId, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} connLinkInv = + PendingContactConnection + { pccConnId = connId, + pccAgentConnId = agentConnId, + pccConnStatus = connStatus, + viaContactUri = isJust xContactId, + viaUserContactLink, + groupLinkId, + customUserProfileId, + connLinkInv, + localAlias, + createdAt, + updatedAt = createdAt + } + aConnId' :: PendingContactConnection -> ConnId aConnId' PendingContactConnection {pccAgentConnId = AgentConnId cId} = cId @@ -1713,12 +1730,6 @@ instance TextEncoding ConnType where ConnRcvFile -> "rcv_file" ConnUserContact -> "user_contact" -data NewConnection = NewConnection - { agentConnId :: ByteString, - connLevel :: Int, - viaConn :: Maybe Int64 - } - data GroupMemberIntro = GroupMemberIntro { introId :: Int64, reMember :: GroupMember, diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 5a09a7b2ce..fe28775c50 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -386,6 +386,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} CEvtGroupMemberSwitch u g m progress -> ttyUser u $ viewGroupMemberSwitch g m progress CEvtContactRatchetSync u ct progress -> ttyUser u $ viewContactRatchetSync ct progress CEvtGroupMemberRatchetSync u g m progress -> ttyUser u $ viewGroupMemberRatchetSync g m progress + CEvtChatInfoUpdated _ _ -> [] CEvtNewChatItems u chatItems -> viewChatItems ttyUser unmuted u chatItems ts tz CEvtChatItemsStatusesUpdated u chatItems | length chatItems <= 20 -> @@ -1894,6 +1895,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] ILPKnown ct | nextConnectPrepared ct -> [invLink ("known prepared contact " <> ttyContact' ct)] + | contactDeleted ct -> [invLink ("known deleted contact " <> ttyContact' ct)] | otherwise -> [ invLink ("known contact " <> ttyContact' ct), "use " <> ttyToContact' ct <> highlight' "" <> " to send messages" @@ -1902,7 +1904,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case invLink = ("invitation link: " <>) invOrBiz = \case Just ContactShortLinkData {business} - | business -> ("business link: " <>) + | business -> ("business address: " <>) _ -> ("invitation link: " <>) CPContactAddress cap -> case cap of CAPOk contactSLinkData -> [addrOrBiz contactSLinkData "ok to connect"] <> [viewJSON contactSLinkData | testView] @@ -1920,7 +1922,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case ctAddr = ("contact address: " <>) addrOrBiz = \case Just ContactShortLinkData {business} - | business -> ("business link: " <>) + | business -> ("business address: " <>) _ -> ("contact address: " <>) CPGroupLink glp -> case glp of GLPOk groupSLinkData -> [grpLink "ok to connect"] <> [viewJSON groupSLinkData | testView] @@ -1928,19 +1930,28 @@ viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] GLPConnectingProhibit (Just g) -> connecting g - GLPKnown g@GroupInfo {preparedGroup} -> case preparedGroup of - Just PreparedGroup {connLinkStartedConnection} - | connLinkStartedConnection -> connecting g - | otherwise -> [knownGroup "prepared "] - Nothing -> - [ knownGroup "", - "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" - ] + GLPKnown g@GroupInfo {preparedGroup, membership = m} -> case preparedGroup of + Just PreparedGroup {connLinkStartedConnection} -> case memberStatus m of + GSMemUnknown + | connLinkStartedConnection -> connecting g + | otherwise -> [knownGroup "prepared "] + GSMemAccepted -> connecting g + _ + | memberRemoved m -> [knownGroup "deleted "] -- it should not get here, as this plan is returned as GLPOk + | otherwise -> knownActive + _ -> knownActive where - knownGroup prepared = grpOrBiz g <> " link: known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g + knownActive = + [ knownGroup "", + "use " <> ttyToGroup g Nothing <> highlight' "" <> " to send messages" + ] + knownGroup prepared = grpOrBizLink g <> ": known " <> prepared <> grpOrBiz g <> " " <> ttyGroup' g where - connecting g = [grpOrBiz g <> " link: connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] + connecting g = [grpOrBizLink g <> ": connecting to " <> grpOrBiz g <> " " <> ttyGroup' g] grpLink = ("group link: " <>) + grpOrBizLink GroupInfo {businessChat} = case businessChat of + Just _ -> "business address" + Nothing -> "group link" grpOrBiz GroupInfo {businessChat} = case businessChat of Just _ -> "business" Nothing -> "group" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 0785495429..12a63d2333 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -21,7 +21,6 @@ import Control.Logger.Simple (LogLevel (..)) import Control.Monad import Control.Monad.Except import Control.Monad.Reader -import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) import Data.List (dropWhileEnd, find) import Data.Maybe (isNothing) @@ -47,7 +46,7 @@ import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval -import Simplex.Messaging.Agent.Store.Interface (DBOpts (..), closeDBStore) +import Simplex.Messaging.Agent.Store.Interface (closeDBStore) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..), MigrationError) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Client (ProtocolClientConfig (..)) @@ -68,7 +67,9 @@ import System.Terminal.Internal (VirtualTerminal (..), VirtualTerminalSettings ( import System.Timeout (timeout) import Test.Hspec (Expectation, HasCallStack, shouldReturn) #if defined(dbPostgres) +import qualified Data.ByteString.Char8 as B import Database.PostgreSQL.Simple (ConnectInfo (..), defaultConnectInfo) +import Simplex.Messaging.Agent.Store.Interface (DBOpts (..)) #else import Data.ByteArray (ScrubbedBytes) import qualified Data.Map.Strict as M @@ -311,7 +312,7 @@ startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user t <- withVirtualTerminal termSettings pure ct <- newChatTerminal t opts cc <- newChatController db (Just user) cfg opts False - void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc + void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") 0 `runReaderT` cc chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry termQ <- newTQueueIO diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 63668e6df1..d4ef4674bb 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -34,6 +34,7 @@ import Simplex.Chat.Types (VersionRangeChat, authErrDisableCount, sameVerificati import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.DB as DB +import Simplex.Messaging.Client (NetworkTimeout (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport @@ -313,6 +314,7 @@ testRetryConnectingClientTimeout ps = do withSmpServer' serverCfg' $ do withTestChatCfgOpts ps cfg' opts' "alice" $ \alice -> do withTestChatCfgOpts ps cfg' opts' "bob" $ \bob -> do + threadDelay 250000 bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" _sLinkData <- getTermLine bob @@ -352,7 +354,7 @@ testRetryConnectingClientTimeout ps = do }, presetServers = let def@PresetServers {netCfg} = presetServers testCfg - in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = 10}} + in def {netCfg = (netCfg :: NetworkConfig) {tcpTimeout = NetworkTimeout 10 10}} } opts' = testOpts diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a5f29b00a4..eaaabf5b15 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -48,10 +48,7 @@ chatProfileTests = do it "deduplicate contact requests" testDeduplicateContactRequests it "deduplicate contact requests with profile change" testDeduplicateContactRequestsProfileChange it "reject contact and delete contact link" testRejectContactAndDeleteUserContact - -- TODO [short links] fix address deletion: - -- TODO - either alert user that N chats will be deleted and delete contact request contacts and business chats - -- TODO - or allow to accept contact requests for deleted address (remove cascade deletes, rework agent) - xit "delete connection requests when contact link deleted" testDeleteConnectionRequests + it "keep connection requests when contact link deleted" testKeepConnectionRequests it "connected contact works when contact link deleted" testContactLinkDeletedConnectedContactWorks -- TODO [short links] test auto-reply with current version, with connecting client not preparing contact it "auto-reply message" testAutoReplyMessage @@ -114,9 +111,9 @@ chatProfileTests = do it "should plan and connect via one-time invitation" testPlanShortLinkInvitation it "should connect via contact address" testShortLinkContactAddress it "should join group" testShortLinkJoinGroup - mapSubject (\params -> params {largeLinkData = True} :: TestParams) $ + aroundWith (. (\params -> params {largeLinkData = True} :: TestParams)) $ describe "short links with attached data (largeLinkData = True)" $ shortLinkTests True - mapSubject (\params -> params {largeLinkData = False} :: TestParams) $ + aroundWith (. (\params -> params {largeLinkData = False} :: TestParams)) $ describe "short links with attached data (largeLinkData = False)" $ shortLinkTests False shortLinkTests :: Bool -> SpecWith TestParams @@ -673,8 +670,8 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr cath ##> ("/c " <> cLink) cath <## "error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" -testDeleteConnectionRequests :: HasCallStack => TestParams -> IO () -testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ +testKeepConnectionRequests :: HasCallStack => TestParams -> IO () +testKeepConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/ad" cLink <- getContactLink alice True @@ -687,14 +684,52 @@ testDeleteConnectionRequests = testChat3 aliceProfile bobProfile cathProfile $ alice <## "Your chat address is deleted - accepted contacts will remain connected." alice <## "To create a new chat address use /ad" + -- can accept and reject requests after address deletion + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + + alice ##> "/rc cath" + alice <## "cath: contact request rejected" + + alice @@@ [("@bob", "hey")] + + -- bob's request to new address uses different name alice ##> "/ad" cLink' <- getContactLink alice True + bob ##> ("/c " <> cLink') - -- same names are used here, as they were released at /da - alice <#? bob + bob <## "connection request sent!" + alice <## "bob_1 (Bob) wants to connect to you!" + alice <## "to accept: /ac bob_1" + alice <## "to reject: /rc bob_1 (the sender will NOT be notified)" + + alice ##> "/ac bob_1" + alice <## "bob_1 (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice_1 (Alice): contact is connected") + (alice <## "bob_1 (Bob): contact is connected") + + alice #> "@bob_1 hi" + bob <# "alice_1> hi" + bob #> "@alice_1 hey" + alice <# "bob_1> hey" + cath ##> ("/c " <> cLink') alice <#? cath + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + alice <##> cath + + alice @@@ [("@cath", "hey"), ("@bob_1", "hey"), ("@bob", "hey")] + testContactLinkDeletedConnectedContactWorks :: HasCallStack => TestParams -> IO () testContactLinkDeletedConnectedContactWorks = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -784,7 +819,7 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice bob <## "#biz: joining the group..." -- the next command can be prone to race conditions bob ##> ("/_connect plan 1 " <> cLink) - bob <## "business link: connecting to business #biz" + bob <## "business address: connecting to business #biz" biz <## "#bob: bob_1 joined the group" bob <## "#biz: you joined the group" biz #> "#bob hi" @@ -792,7 +827,7 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice bob #> "#biz hello" biz <# "#bob bob_1> hello" bob ##> ("/_connect plan 1 " <> cLink) - bob <## "business link: known business #biz" + bob <## "business address: known business #biz" bob <## "use #biz to send messages" connectUsers biz alice biz <##> alice @@ -2954,6 +2989,14 @@ testShortLinkInvitationPrepareContact ps@TestParams {largeLinkData} = testChatCf (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") alice <##> bob + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "invitation link: known contact alice" + bob <## "use @alice to send messages" + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "invitation link: known deleted contact alice" testShortLinkInvitationImage :: HasCallStack => TestParams -> IO () testShortLinkInvitationImage ps@TestParams {largeLinkData} = testChatCfg2 testCfg {largeLinkData} aliceProfile bobProfile test ps @@ -3082,6 +3125,15 @@ testShortLinkAddressPrepareContact ps@TestParams {largeLinkData} = testChatCfg2 (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") alice <##> bob + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "contact address: ok to connect" + void $ getTermLine bob testShortLinkDeletedInvitation :: HasCallStack => TestParams -> IO () testShortLinkDeletedInvitation ps@TestParams {largeLinkData} = testChatCfg2 testCfg {largeLinkData} aliceProfile bobProfile test ps @@ -3253,19 +3305,19 @@ testShortLinkAddressPrepareBusiness ps@TestParams {largeLinkData} = testChatCfg3 biz ##> "/auto_accept on business" biz <## "auto_accept on, business" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "business link: ok to connect" + bob <## "business address: ok to connect" contactSLinkData <- getTermLine bob bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) bob <## "#biz: group is prepared" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "business link: known prepared business #biz" + bob <## "business address: known prepared business #biz" bob ##> "/_connect group #1" bob <## "#biz: connection started" biz <## "#bob (Bob): accepting business address request..." bob <## "#biz: joining the group..." -- the next command can be prone to race conditions bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "business link: connecting to business #biz" + bob <## "business address: connecting to business #biz" biz <## "#bob: bob_1 joined the group" bob <## "#biz: you joined the group" biz #> "#bob hi" @@ -3298,6 +3350,18 @@ testShortLinkAddressPrepareBusiness ps@TestParams {largeLinkData} = testChatCfg3 concurrently_ (alice <# "#bob bob_1> hey there") (biz <# "#bob bob_1> hey there") + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "business address: known business #biz" + bob <## "use #biz to send messages" + biz ##> "/d #bob" + biz <## "#bob: you deleted the group" + alice <## "#bob: biz deleted the group" + alice <## "use /d #bob to delete the local copy of the group" + bob <## "#biz: biz_1 deleted the group" + bob <## "use /d #biz to delete the local copy of the group" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "business address: ok to connect" + void $ getTermLine bob testBusinessAddressRequestMessage :: HasCallStack => TestParams -> IO () testBusinessAddressRequestMessage ps@TestParams {largeLinkData} = testChatCfg3 testCfg {largeLinkData} businessProfile aliceProfile {fullName = "Alice @ Biz"} bobProfile test ps @@ -3310,7 +3374,7 @@ testBusinessAddressRequestMessage ps@TestParams {largeLinkData} = testChatCfg3 t biz <## "auto reply:" biz <## "Welcome!" bob ##> ("/_connect plan 1 " <> shortLink) - bob <## "business link: ok to connect" + bob <## "business address: ok to connect" contactSLinkData <- getTermLine bob bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) bob <## "#biz: group is prepared" @@ -3380,6 +3444,17 @@ testShortLinkPrepareGroup ps@TestParams {largeLinkData} = testChatCfg3 testCfg { [alice, cath] *<# "#team bob> 2" cath #> "#team 3" [alice, bob] *<# "#team cath> 3" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + bob ##> "/l #team" + bob <## "#team: you left the group" + bob <## "use /d #team to delete the group" + alice <## "#team: bob left the group" + cath <## "#team: bob left the group" + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "group link: ok to connect" + void $ getTermLine bob testShortLinkPrepareGroupReject :: HasCallStack => TestParams -> IO () testShortLinkPrepareGroupReject ps@TestParams {largeLinkData} = testChatCfg3 cfg {largeLinkData} aliceProfile bobProfile cathProfile test ps diff --git a/tests/Test.hs b/tests/Test.hs index b8e2c4a4c0..cbe2efd281 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -9,7 +9,6 @@ import ChatClient import ChatTests import ChatTests.DBUtils import ChatTests.Utils (xdescribe'') -import Control.Exception (bracket_) import Control.Logger.Simple import Data.Time.Clock.System import JSONTests @@ -24,6 +23,7 @@ import UnliftIO.Temporary (withTempDirectory) import ValidNames import ViewTests #if defined(dbPostgres) +import Control.Exception (bracket_) import PostgresSchemaDump import Simplex.Chat.Store.Postgres.Migrations (migrations) import Simplex.Messaging.Agent.Store.Postgres.Util (createDBAndUserIfNotExists, dropAllSchemasExceptSystem, dropDatabaseAndUser)