From c2348098943c907144f25b4ba25fbcf112057e00 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:00:24 +0700 Subject: [PATCH 01/14] ios: improvement of chat item context menu (#3981) * ios: improvement of chat item context menu * rename --------- Co-authored-by: Evgeny Poberezkin --- .../Views/Chat/ChatItem/CIImageView.swift | 4 ++-- .../Views/Chat/ChatItem/CIVideoView.swift | 4 ++-- apps/ios/Shared/Views/Chat/ChatView.swift | 4 +++- .../Shared/Views/Helpers/ContextMenu.swift | 24 +++++++++++++++---- .../Shared/Views/Helpers/DetermineWidth.swift | 20 ++++++++++++++++ apps/ios/SimpleXChat/ChatTypes.swift | 8 +++++++ 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index c3e4805bf3..16974147c8 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -70,14 +70,14 @@ struct CIImageView: View { } private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : img.imageData == nil ? .infinity : maxWidth + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth DispatchQueue.main.async { imgWidth = w } return ZStack(alignment: .topTrailing) { if img.imageData == nil { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) + .frame(width: w) } else { SwiftyGif(image: img) .frame(width: w, height: w * img.size.height / img.size.width) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index ff208fe58a..a3918e17bc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -243,13 +243,13 @@ struct CIVideoView: View { } private func imageView(_ img: UIImage) -> some View { - let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity + let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { Image(uiImage: img) .resizable() .scaledToFit() - .frame(maxWidth: w) + .frame(width: w) loadingIndicator() } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 33bd46e393..cd2aa55bc3 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -514,6 +514,7 @@ struct ChatView: View { chat: chat, chatItem: ci, maxWidth: maxWidth, + itemWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, chatView: self @@ -526,6 +527,7 @@ struct ChatView: View { @ObservedObject var chat: Chat var chatItem: ChatItem var maxWidth: CGFloat + @State var itemWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? var chatView: ChatView @@ -654,7 +656,7 @@ struct ChatView: View { playbackState: $playbackState, playbackTime: $playbackTime ) - .uiKitContextMenu(maxWidth: maxWidth, menu: uiMenu, allowMenu: $allowMenu) + .uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu) .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions(ci) diff --git a/apps/ios/Shared/Views/Helpers/ContextMenu.swift b/apps/ios/Shared/Views/Helpers/ContextMenu.swift index 3b82d6eb95..9504d919ef 100644 --- a/apps/ios/Shared/Views/Helpers/ContextMenu.swift +++ b/apps/ios/Shared/Views/Helpers/ContextMenu.swift @@ -11,11 +11,20 @@ import UIKit import SwiftUI extension View { - func uiKitContextMenu(maxWidth: CGFloat, menu: Binding, allowMenu: Binding) -> some View { + func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding, menu: Binding, allowMenu: Binding) -> some View { Group { if allowMenu.wrappedValue { - InteractionView(content: self, maxWidth: maxWidth, menu: menu) - .fixedSize(horizontal: true, vertical: false) + if hasImageOrVideo { + InteractionView(content: + self.environmentObject(ChatModel.shared) + .overlay(DetermineWidthImageVideoItem()) + .onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 } + , maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) + .frame(maxWidth: itemWidth.wrappedValue) + } else { + InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) + .fixedSize(horizontal: true, vertical: false) + } } else { self } @@ -31,13 +40,14 @@ private class HostingViewHolder: UIView { struct InteractionView: UIViewRepresentable { let content: Content var maxWidth: CGFloat + var itemWidth: Binding @Binding var menu: UIMenu func makeUIView(context: Context) -> UIView { let view = HostingViewHolder() - view.contentSize = CGSizeMake(maxWidth, .infinity) view.backgroundColor = .clear let hostView = UIHostingController(rootView: content) + view.contentSize = hostView.view.intrinsicContentSize hostView.view.translatesAutoresizingMaskIntoConstraints = false let constraints = [ hostView.view.topAnchor.constraint(equalTo: view.topAnchor), @@ -57,7 +67,11 @@ struct InteractionView: UIViewRepresentable { } func updateUIView(_ uiView: UIView, context: Context) { - (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(maxWidth, .infinity)) + let was = (uiView as! HostingViewHolder).contentSize + (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity)) + if was != (uiView as! HostingViewHolder).contentSize { + uiView.invalidateIntrinsicContentSize() + } } func makeCoordinator() -> Coordinator { diff --git a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift index d2a0aaab1d..b05ab17089 100644 --- a/apps/ios/Shared/Views/Helpers/DetermineWidth.swift +++ b/apps/ios/Shared/Views/Helpers/DetermineWidth.swift @@ -21,6 +21,19 @@ struct DetermineWidth: View { } } +struct DetermineWidthImageVideoItem: View { + typealias Key = MaximumWidthImageVideoPreferenceKey + var body: some View { + GeometryReader { proxy in + Color.clear + .preference( + key: MaximumWidthImageVideoPreferenceKey.self, + value: proxy.size.width + ) + } + } +} + struct MaximumWidthPreferenceKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { @@ -28,6 +41,13 @@ struct MaximumWidthPreferenceKey: PreferenceKey { } } +struct MaximumWidthImageVideoPreferenceKey: PreferenceKey { + static var defaultValue: CGFloat = 0 + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = max(value, nextValue()) + } +} + struct DetermineWidth_Previews: PreviewProvider { static var previews: some View { DetermineWidth() diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 5e1c0ac538..ed62b5c9ac 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3209,6 +3209,14 @@ public enum MsgContent: Equatable { } } + public var isImageOrVideo: Bool { + switch self { + case .image: true + case .video: true + default: false + } + } + var cmdString: String { "json \(encodeJSON(self))" } From b8ee2af5b762f361a8e72c86213790c7d8338d91 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 3 Apr 2024 00:57:23 +0700 Subject: [PATCH 02/14] deskop: show window icon (#3983) * deskop: show window icon * size --- .../resources/MR/images/ic_simplex.svg | 44 +++++++++++++++++++ .../kotlin/chat/simplex/common/DesktopApp.kt | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg new file mode 100644 index 0000000000..d7019f3645 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_simplex.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 44073aa990..7b7762eefc 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -21,6 +21,7 @@ import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.views.TerminalView import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import java.awt.event.WindowEvent @@ -103,7 +104,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { simplexWindowState.windowState = windowState // Reload all strings in all @Composable's after language change at runtime if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { - Window(state = windowState, onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { + Window(state = windowState, icon = painterResource(MR.images.ic_simplex), onCloseRequest = { closedByError.value = false; exitApplication() }, onKeyEvent = { if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { simplexWindowState.backstack.lastOrNull()?.invoke() != null } else { From 28fbc1cd84b1a85c6b6d6cfb4fd70d90d98a680c Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 3 Apr 2024 02:05:14 +0700 Subject: [PATCH 03/14] desktop: correct height of a window (#3982) --- .../views/chat/item/CIVideoView.desktop.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt index 8dac39199f..2c063b5888 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt @@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import java.awt.Window @Composable actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) { @@ -23,14 +25,15 @@ actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLon } } +/* +* This function doesn't take into account multi-window environment. In case more windows will be used, modify the code +* */ @Composable -actual fun LocalWindowWidth(): Dp { - return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } - /*val density = LocalDensity.current - var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) } - SideEffect { - if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) - width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } +actual fun LocalWindowWidth(): Dp = with(LocalDensity.current) { + val windows = java.awt.Window.getWindows() + if (windows.size == 1) { + (windows.getOrNull(0)?.width ?: 0).toDp() + } else { + simplexWindowState.windowState.size.width } - return width.also { println("LALAL $it") }*/ } From 97a37634ef150beec96df6e2f8824d1e3d4f216f Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 3 Apr 2024 01:47:01 +0100 Subject: [PATCH 04/14] ios: 5.6.1 build 205 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 64 +++++++++++----------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 9825bf08ff..2a40cb41cb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -111,6 +111,11 @@ 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; + 5CC932E12BBC94DC008A1EB6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */; }; + 5CC932E22BBC94DC008A1EB6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */; }; + 5CC932E32BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */; }; + 5CC932E42BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */; }; + 5CC932E52BBC94DC008A1EB6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932E02BBC94DC008A1EB6 /* libffi.a */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; @@ -139,11 +144,6 @@ 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */; }; 5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */; }; - 5CF898622BB984E400EE33B6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF8985D2BB984E400EE33B6 /* libgmpxx.a */; }; - 5CF898632BB984E400EE33B6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF8985E2BB984E400EE33B6 /* libffi.a */; }; - 5CF898642BB984E400EE33B6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF8985F2BB984E400EE33B6 /* libgmp.a */; }; - 5CF898652BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF898602BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */; }; - 5CF898662BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CF898612BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; 5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; }; @@ -402,6 +402,11 @@ 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; + 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a"; sourceTree = ""; }; + 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a"; sourceTree = ""; }; + 5CC932E02BBC94DC008A1EB6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = ""; }; @@ -431,11 +436,6 @@ 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; - 5CF8985D2BB984E400EE33B6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CF8985E2BB984E400EE33B6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CF8985F2BB984E400EE33B6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CF898602BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a"; sourceTree = ""; }; - 5CF898612BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a"; sourceTree = ""; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = ""; }; @@ -521,13 +521,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CF898652BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a in Frameworks */, - 5CF898622BB984E400EE33B6 /* libgmpxx.a in Frameworks */, - 5CF898642BB984E400EE33B6 /* libgmp.a in Frameworks */, + 5CC932E32BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a in Frameworks */, + 5CC932E42BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a in Frameworks */, + 5CC932E22BBC94DC008A1EB6 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 5CC932E52BBC94DC008A1EB6 /* libffi.a in Frameworks */, + 5CC932E12BBC94DC008A1EB6 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CF898632BB984E400EE33B6 /* libffi.a in Frameworks */, - 5CF898662BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -590,11 +590,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CF8985E2BB984E400EE33B6 /* libffi.a */, - 5CF8985F2BB984E400EE33B6 /* libgmp.a */, - 5CF8985D2BB984E400EE33B6 /* libgmpxx.a */, - 5CF898612BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv-ghc9.6.3.a */, - 5CF898602BB984E400EE33B6 /* libHSsimplex-chat-5.6.0.4-FOF2McwHkk1EIlP5UNozOv.a */, + 5CC932E02BBC94DC008A1EB6 /* libffi.a */, + 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */, + 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */, + 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */, + 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */, ); path = Libraries; sourceTree = ""; @@ -1536,7 +1536,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1561,7 +1561,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1585,7 +1585,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1610,7 +1610,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1671,7 +1671,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1686,7 +1686,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1708,7 +1708,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1723,7 +1723,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1745,7 +1745,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1771,7 +1771,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1796,7 +1796,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 204; + CURRENT_PROJECT_VERSION = 205; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1822,7 +1822,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.6; + MARKETING_VERSION = 5.6.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; From 2bd1a82b7d1f7add8ad7fac6a846f39c0bda678e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 3 Apr 2024 10:47:38 +0100 Subject: [PATCH 05/14] core: revert "deps: switch to base64 via simplexmq (#3957)" (#3985) * Revert "deps: switch to base64 via simplexmq (#3957)" This reverts commit d65137882bfd822d4af4aaf98871f1dd98efebde. * update simplexmq --- cabal.project | 8 +------- package.yaml | 1 + scripts/nix/sha256map.nix | 3 +-- simplex-chat.cabal | 7 +++++++ src/Simplex/Chat.hs | 4 ++-- src/Simplex/Chat/Messages.hs | 2 +- src/Simplex/Chat/Mobile.hs | 2 +- src/Simplex/Chat/Mobile/WebRTC.hs | 2 +- src/Simplex/Chat/Remote.hs | 2 +- src/Simplex/Chat/Store/Shared.hs | 2 +- tests/ChatTests/Utils.hs | 2 +- tests/WebRTCTests.hs | 6 +++--- 12 files changed, 21 insertions(+), 20 deletions(-) diff --git a/cabal.project b/cabal.project index 12879f4c76..486cd97691 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: bfd532e833aff36754ef766f4e021f0079a7f83c + tag: 84b8c8417b73af5ee347af6e098d9b3c9190549d source-repository-package type: git @@ -34,12 +34,6 @@ source-repository-package location: https://github.com/simplex-chat/aeson.git tag: aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b --- old bs/text compat for 8.10 -source-repository-package - type: git - location: https://github.com/simplex-chat/base64.git - tag: 2d77b6dbcaffc00570a70be8694049f3710e7c94 - source-repository-package type: git location: https://github.com/simplex-chat/haskell-terminal.git diff --git a/package.yaml b/package.yaml index 1f891df76b..22fe922174 100644 --- a/package.yaml +++ b/package.yaml @@ -18,6 +18,7 @@ dependencies: - async == 2.2.* - attoparsec == 0.14.* - base >= 4.7 && < 5 + - base64-bytestring >= 1.0 && < 1.3 - composition == 1.0.* - constraints >= 0.12 && < 0.14 - containers == 0.6.* diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 9942f3f4a5..51e503b63c 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,10 +1,9 @@ { - "https://github.com/simplex-chat/simplexmq.git"."bfd532e833aff36754ef766f4e021f0079a7f83c" = "1xxcdadllimk2hgzz6aggvywr14zm2h0l62c92yvnyvps9j49gdx"; + "https://github.com/simplex-chat/simplexmq.git"."84b8c8417b73af5ee347af6e098d9b3c9190549d" = "1aaqj83ym2h21py6qqzigzsgplsr5k651q92zilqhmd44h4fwxrm"; "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"; "https://github.com/simplex-chat/aeson.git"."aab7b5a14d6c5ea64c64dcaee418de1bb00dcc2b" = "0jz7kda8gai893vyvj96fy962ncv8dcsx71fbddyy8zrvc88jfrr"; - "https://github.com/simplex-chat/base64.git"."2d77b6dbcaffc00570a70be8694049f3710e7c94" = "0zdskk67fzqrrx1i29s3shp7fh9c0krmq5h6hq03qx0n3xy2m44b"; "https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn"; "https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 3ab530e2d9..17ea518309 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -190,6 +190,7 @@ library , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -251,6 +252,7 @@ executable simplex-bot , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -313,6 +315,7 @@ executable simplex-bot-advanced , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -378,6 +381,7 @@ executable simplex-broadcast-bot , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -441,6 +445,7 @@ executable simplex-chat , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -510,6 +515,7 @@ executable simplex-directory-service , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* @@ -604,6 +610,7 @@ test-suite simplex-chat-test , async ==2.2.* , attoparsec ==0.14.* , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 , composition ==1.0.* , constraints >=0.12 && <0.14 , containers ==0.6.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 2367294489..7948875180 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -29,6 +29,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Bifunctor (bimap, first, second) import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteArray as BA +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -103,9 +104,8 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..), pattern IKNoPQ, pattern IKPQOff, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff, pattern PQSupportOn) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding -import Simplex.Messaging.Encoding.Base64 (base64P) -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (base64P) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index c24de3b2e1..b29543cf74 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -24,6 +24,7 @@ import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace) import Data.Int (Int64) @@ -47,7 +48,6 @@ import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 9bc31fa2c7..5883c6042c 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -17,6 +17,7 @@ import qualified Data.Aeson.TH as JQ import Data.Bifunctor (first) import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteArray as BA +import qualified Data.ByteString.Base64.URL as U import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB @@ -49,7 +50,6 @@ import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index b69203b651..537388b18b 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -17,6 +17,7 @@ import Data.Bifunctor (bimap) import qualified Data.ByteArray as BA import Data.ByteString (ByteString) import qualified Data.ByteString as B +import qualified Data.ByteString.Base64.URL as U import Data.Either (fromLeft) import Data.Word (Word8) import Foreign.C (CInt, CString, newCAString) @@ -25,7 +26,6 @@ import Foreign.StablePtr import Simplex.Chat.Controller (ChatController (..)) import Simplex.Chat.Mobile.Shared import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import UnliftIO (atomically) cChatEncryptMedia :: StablePtr ChatController -> CString -> Ptr Word8 -> CInt -> IO CString diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 416b88599c..819c1cd670 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -22,6 +22,7 @@ import Crypto.Random (getRandomBytes) import qualified Data.Aeson as J import qualified Data.Aeson.Types as JT import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64.URL as B64U import Data.ByteString.Builder (Builder) import qualified Data.ByteString.Char8 as B import Data.Functor (($>)) @@ -55,7 +56,6 @@ import Simplex.Messaging.Agent import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF -import qualified Simplex.Messaging.Encoding.Base64.URL as B64U import Simplex.Messaging.Encoding.String (StrEncoding (..)) import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport (TLS, closeConnection, tlsUniq) diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index dc75ad50e9..88540134fe 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -18,6 +18,7 @@ import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) import qualified Data.Aeson.TH as J +import qualified Data.ByteString.Base64 as B64 import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) @@ -38,7 +39,6 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 13812cbaaa..3b0748e7d0 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -14,6 +14,7 @@ import Control.Concurrent.STM import Control.Monad (unless, when) import Control.Monad.Except (runExceptT) import Data.ByteString (ByteString) +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B import Data.Char (isDigit) import Data.List (isPrefixOf, isSuffixOf) @@ -34,7 +35,6 @@ import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff, pattern PQEncOn, pattern PQSupportOff) -import qualified Simplex.Messaging.Encoding.Base64 as B64 import Simplex.Messaging.Encoding.String import Simplex.Messaging.Version import System.Directory (doesFileExist) diff --git a/tests/WebRTCTests.hs b/tests/WebRTCTests.hs index f2372acc2f..a473afef36 100644 --- a/tests/WebRTCTests.hs +++ b/tests/WebRTCTests.hs @@ -4,12 +4,12 @@ module WebRTCTests where import Control.Monad.Except import Crypto.Random (getRandomBytes) +import qualified Data.ByteString.Base64.URL as U import qualified Data.ByteString.Char8 as B import Foreign.StablePtr import Simplex.Chat.Mobile import Simplex.Chat.Mobile.WebRTC import qualified Simplex.Messaging.Crypto as C -import qualified Simplex.Messaging.Encoding.Base64.URL as U import System.FilePath (()) import Test.Hspec @@ -36,8 +36,8 @@ webRTCTests = describe "WebRTC crypto" $ do cc <- newStablePtr c let key = B.replicate 32 '#' frame <- (<> B.replicate reservedSize '\NUL') <$> getRandomBytes 100 - runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0" - runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid base64 encoding near offset: 0" + runExceptT (chatEncryptMedia cc key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" + runExceptT (chatDecryptMedia key frame) `shouldReturn` Left "invalid key: invalid character at offset: 0" it "should fail on invalid auth tag" $ \tmp -> do Right c <- chatMigrateInit (tmp "1") "" "yesUp" cc <- newStablePtr c From ea862a8f340bc25509784c0a7bb80ab46161b550 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Wed, 3 Apr 2024 11:43:42 +0100 Subject: [PATCH 06/14] core: 5.6.1.1 (simplexmq 5.6.2.1) --- cabal.project | 2 +- package.yaml | 2 +- scripts/nix/sha256map.nix | 2 +- simplex-chat.cabal | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cabal.project b/cabal.project index 486cd97691..62115c136b 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: 84b8c8417b73af5ee347af6e098d9b3c9190549d + tag: 6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd source-repository-package type: git diff --git a/package.yaml b/package.yaml index 22fe922174..3e187c5653 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.6.1.0 +version: 5.6.1.1 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 51e503b63c..d2701dc45f 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."84b8c8417b73af5ee347af6e098d9b3c9190549d" = "1aaqj83ym2h21py6qqzigzsgplsr5k651q92zilqhmd44h4fwxrm"; + "https://github.com/simplex-chat/simplexmq.git"."6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd" = "08l00ay1ibz7skhlpfjp6z2821zpfd0kxplwdm6zc63m2f6za7cv"; "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 17ea518309..fb0635abad 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: 5.6.1.0 +version: 5.6.1.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat From 9b28ae6d9e8db2390dbc2e9028ef18e26f77ed6e Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:58:32 +0700 Subject: [PATCH 07/14] desktop: remote connection host/port fix (#3987) --- .../common/views/remote/ConnectMobileView.kt | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index bd1be525be..e13b86258d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -38,9 +38,7 @@ import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.runBlocking @Composable fun ConnectMobileView() { @@ -269,12 +267,20 @@ fun AddingMobileDevice(showTitle: Boolean, staleQrCode: MutableState, c var cachedR by remember { mutableStateOf(null) } val customAddress = rememberSaveable { mutableStateOf(null) } val customPort = rememberSaveable { mutableStateOf(null) } + var userChangedAddress by rememberSaveable { mutableStateOf(false) } + var userChangedPort by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { + if (customAddress.value != cachedR.address && cachedR != null) { + userChangedAddress = true + } + if (customPort.value != cachedR.port && cachedR != null) { + userChangedPort = true + } val r = chatModel.controller.startRemoteHost( rhId = null, multicast = controller.appPrefs.offerRemoteMulticast.get(), - address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_, - port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ + address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_, + port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ) if (r != null) { cachedR = r @@ -343,12 +349,20 @@ private fun showConnectMobileDevice(rh: RemoteHostInfo, connecting: MutableState var cachedR by remember { mutableStateOf(null) } val customAddress = rememberSaveable { mutableStateOf(null) } val customPort = rememberSaveable { mutableStateOf(null) } + var userChangedAddress by rememberSaveable { mutableStateOf(false) } + var userChangedPort by rememberSaveable { mutableStateOf(false) } val startRemoteHost = suspend { + if (customAddress.value != cachedR.address && cachedR != null) { + userChangedAddress = true + } + if (customPort.value != cachedR.port && cachedR != null) { + userChangedPort = true + } val r = chatModel.controller.startRemoteHost( rhId = rh.remoteHostId, multicast = controller.appPrefs.offerRemoteMulticast.get(), - address = if (customAddress.value?.address != cachedR.address?.address) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, - port = if (customPort.value != cachedR.port) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ + address = if (customAddress.value != null && userChangedAddress) customAddress.value else cachedR.rh?.bindAddress_ ?: rh.bindAddress_, + port = if (customPort.value != null && userChangedPort) customPort.value else cachedR.rh?.bindPort_ ?: rh.bindPort_ ) if (r != null) { cachedR = r From d2de81100d4066d80ba1b35c6f4cc2e08576458a Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:11:04 +0700 Subject: [PATCH 08/14] android: workaround of pager's bug (#3988) --- .../kotlin/chat/simplex/common/views/chat/ChatView.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 c70511f847..702dd9fec5 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 @@ -1392,11 +1392,12 @@ private fun providerForGallery( return null } - var initialIndex = Int.MAX_VALUE / 2 + // Pager has a bug with overflowing when total pages is around Int.MAX_VALUE. Using smaller value + var initialIndex = 10000 / 2 var initialChatId = cItemId return object: ImageGalleryProvider { override val initialIndex: Int = initialIndex - override val totalMediaSize = mutableStateOf(Int.MAX_VALUE) + override val totalMediaSize = mutableStateOf(10000) override fun getMedia(index: Int): ProviderMedia? { val internalIndex = initialIndex - index val item = item(internalIndex, initialChatId)?.second ?: return null From 14c279d1e0eff5d4825c059ef0ebff9413b65d65 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Wed, 3 Apr 2024 21:15:44 +0700 Subject: [PATCH 09/14] android: possibly, prevent TooManyRequests exception (#3989) * android: possibly, prevent TooManyRequests exception * new line --- .../views/chat/item/CIImageView.android.kt | 3 +-- .../chat/item/ImageFullScreenView.android.kt | 27 ++++++++++--------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index 28c00ec018..c606e9acb0 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import chat.simplex.common.helpers.toUri import chat.simplex.common.model.CIFile import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.ModalManager @@ -15,7 +14,6 @@ import coil.compose.rememberAsyncImagePainter import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.request.ImageRequest -import java.net.URI @Composable actual fun SimpleAndAnimatedImageView( @@ -43,6 +41,7 @@ actual fun SimpleAndAnimatedImageView( } private val imageLoader = ImageLoader.Builder(androidAppContext) + .networkObserverEnabled(false) .components { if (SDK_INT >= 28) { add(ImageDecoderDecoder.Factory()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt index d4efdc3e59..dad8872012 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt @@ -3,7 +3,7 @@ package chat.simplex.common.views.chat.item import android.os.Build import android.view.View import androidx.compose.foundation.Image -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.BitmapPainter @@ -11,8 +11,8 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.isVisible -import chat.simplex.common.helpers.toUri import chat.simplex.common.platform.VideoPlayer +import chat.simplex.common.platform.androidAppContext import chat.simplex.res.MR import coil.ImageLoader import coil.compose.rememberAsyncImagePainter @@ -23,21 +23,11 @@ import coil.size.Size import com.google.android.exoplayer2.ui.AspectRatioFrameLayout import com.google.android.exoplayer2.ui.StyledPlayerView import dev.icerock.moko.resources.compose.stringResource -import java.net.URI @Composable actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { - // I'm making a new instance of imageLoader here because if I use one instance in multiple places + // I'm using a new private instance of imageLoader here because if I use one instance in multiple places // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want - val imageLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() Image( rememberAsyncImagePainter( ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(), @@ -73,3 +63,14 @@ actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: ( modifier ) } + +private val imageLoader = ImageLoader.Builder(androidAppContext) + .networkObserverEnabled(false) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() From ea6c5bfb0bc5276cf666ed9f0c96ae1613583aca Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 4 Apr 2024 08:58:24 +0100 Subject: [PATCH 10/14] 5.6.1: ios 206, android 193, desktop 36 --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 52 +++++++++++----------- apps/multiplatform/gradle.properties | 8 ++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2a40cb41cb..6443c21b9e 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -111,11 +111,11 @@ 5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */; }; 5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */; }; 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */; }; - 5CC932E12BBC94DC008A1EB6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */; }; - 5CC932E22BBC94DC008A1EB6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */; }; - 5CC932E32BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */; }; - 5CC932E42BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */; }; - 5CC932E52BBC94DC008A1EB6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932E02BBC94DC008A1EB6 /* libffi.a */; }; + 5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */; }; + 5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */; }; + 5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */; }; + 5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */; }; + 5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CC932F42BBDD9F9008A1EB6 /* libffi.a */; }; 5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; }; 5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CD67B8D2B0E858A00C510B1 /* hs_init.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */ = {isa = PBXBuildFile; fileRef = 5CD67B8E2B0E858A00C510B1 /* hs_init.c */; }; @@ -402,11 +402,11 @@ 5CC2C0FB2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 5CC2C0FE2809BF11000C35E3 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = "ru.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIRcvDecryptionError.swift; sourceTree = ""; }; - 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a"; sourceTree = ""; }; - 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a"; sourceTree = ""; }; - 5CC932E02BBC94DC008A1EB6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a"; sourceTree = ""; }; + 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a"; sourceTree = ""; }; + 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5CC932F42BBDD9F9008A1EB6 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = ""; }; 5CD67B8D2B0E858A00C510B1 /* hs_init.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hs_init.h; sourceTree = ""; }; 5CD67B8E2B0E858A00C510B1 /* hs_init.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = hs_init.c; sourceTree = ""; }; @@ -521,12 +521,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CC932E32BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a in Frameworks */, - 5CC932E42BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a in Frameworks */, - 5CC932E22BBC94DC008A1EB6 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CC932E52BBC94DC008A1EB6 /* libffi.a in Frameworks */, - 5CC932E12BBC94DC008A1EB6 /* libgmp.a in Frameworks */, + 5CC932F72BBDD9FA008A1EB6 /* libgmpxx.a in Frameworks */, + 5CC932F62BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a in Frameworks */, + 5CC932F52BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a in Frameworks */, + 5CC932F92BBDD9FA008A1EB6 /* libffi.a in Frameworks */, + 5CC932F82BBDD9FA008A1EB6 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -590,11 +590,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CC932E02BBC94DC008A1EB6 /* libffi.a */, - 5CC932DC2BBC94DC008A1EB6 /* libgmp.a */, - 5CC932DD2BBC94DC008A1EB6 /* libgmpxx.a */, - 5CC932DE2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr-ghc9.6.3.a */, - 5CC932DF2BBC94DC008A1EB6 /* libHSsimplex-chat-5.6.1.0-1MvnzcJ9TtTKuJWF1wc9Tr.a */, + 5CC932F42BBDD9F9008A1EB6 /* libffi.a */, + 5CC932F32BBDD9F9008A1EB6 /* libgmp.a */, + 5CC932F22BBDD9F9008A1EB6 /* libgmpxx.a */, + 5CC932F02BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto-ghc9.6.3.a */, + 5CC932F12BBDD9F9008A1EB6 /* libHSsimplex-chat-5.6.1.1-KSjuZv7tE7cHRWd28F1Zto.a */, ); path = Libraries; sourceTree = ""; @@ -1536,7 +1536,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1585,7 +1585,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1671,7 +1671,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1708,7 +1708,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1745,7 +1745,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1796,7 +1796,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 205; + CURRENT_PROJECT_VERSION = 206; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 1bfac36a28..fbbab0ffab 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,11 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.6 -android.version_code=191 +android.version_name=5.6.1 +android.version_code=193 -desktop.version_name=5.6 -desktop.version_code=35 +desktop.version_name=5.6.1 +desktop.version_code=36 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 From 5f9710e4bb2ba643888fefb220e46dad5d621c44 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 4 Apr 2024 13:17:12 +0100 Subject: [PATCH 11/14] blog: post --- ...y-i-joined-simplex-chat-esraa-al-shafei.md | 38 ++++++++++++++++++ blog/README.md | 8 ++++ blog/images/20240404-esraa.png | Bin 0 -> 11788 bytes blog/images/20240404-messsaging-apps.png | Bin 0 -> 186897 bytes .../src/_includes/blog_previews/20240404.html | 8 ++++ 5 files changed, 54 insertions(+) create mode 100644 blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md create mode 100644 blog/images/20240404-esraa.png create mode 100644 blog/images/20240404-messsaging-apps.png create mode 100644 website/src/_includes/blog_previews/20240404.html diff --git a/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md b/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md new file mode 100644 index 0000000000..e554db5102 --- /dev/null +++ b/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.md @@ -0,0 +1,38 @@ +--- +layout: layouts/article.html +title: "Why I joined SimpleX Chat - by Esra'a al Shafei" +date: 2024-04-04 +previewBody: blog_previews/20240404.html +image: images/20240404-esraa.png +permalink: "/blog/20240404-why-i-joined-simplex-chat-esraa-al-shafei.html" +--- + +# Why I joined SimpleX Chat + +**Published:** Apr 4, 2024 + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity. + +The nonprofit world has been my primary focus for decades. My team and I ran the platforms at Majal.org with an extremely limited budget. We had to navigate many complexities and challenges that shadow the nonprofit model. And because we worked primarily in creating applications and tools, a recurring theme has been financial sustainability. Being a Bahrain-based entity for most of these years meant that the many communities we served were not in a position to provide contributions and we were not eligible for most foundation grants. This drastically limited our growth and the reliability of our apps. When we failed to raise sufficient funds or meet our target budgets, we often had to shutter certain applications, sometimes after spending more than 10 years building them. + +With secure and private messaging, the stakes are even graver. Any failure to commit and resource/fund ongoing development, security patches, etc means lives can be at risk. I still believe in nonprofit models, and it’s why I continue to serve them through various volunteer roles. I do also believe that there is room for a mixture of models that, in the case of something as unique as SimpleX Chat, can serve as a fully open and transparent public interest technology while also having a profitable values-aligned company that can keep the lights on to continue developing, expanding, and improving the protocol, network and their reach. + +I’m no stranger to writing about some VC models being [corrupt](https://mastodon.social/@alshafei/112125959080515656). Frankly, I also hold the view that some tech VCs are amongst the [most complicit](https://responsiblestatecraft.org/defense-tech/) in egregious war crimes worldwide, or enabling the [intrusive surveillance](https://mastodon.social/@alshafei/112140566088322925) we’re fighting against. So being part of a VC-funded venture is not a decision I take lightly. However, I have been following SimpleX Chat’s growth since early 2022 when I first met Evgeny at the Mozilla Festival. I appreciated the drive and Evgeny’s firm refusal to settle for the current models of private messaging. We share the belief that messaging is something we need to keep improving and that we must continue pushing its boundaries to make it even more private, secure, usable for groups, and, most importantly - fully decentralized. This is a major undertaking, and it requires funding to achieve. Candidly, I did worry about funding and sustainability because, at the time, SimpleX was still primarily funded by user contributions. + +But even knowing this, I scrutinized SimpleX Chat for taking VC funding ($350K) from Village Global and questioned the individuals featured on its frontpage. I had to speak with Evgeny directly to learn who exactly from this fund was involved, how much power they wielded, if any, and if this changes the ethos of the company - all of which he is already making public. It was only after these discussions that I was comfortable to take a leap of faith and continue to use the app and vouch for its current and future offerings. It required me to question my own views on whether a VC-funded company can actually have major positive contributions to privacy as well as the open ecosystem. + + + +The web has a long history of [trading privacy](https://www.engadget.com/from-its-start-gmail-conditioned-us-to-trade-privacy-for-free-services-120009741.html) for “free” services. Traditionally, these services have also been centralized, closed-source, non-transparent, and profit-oriented. The companies behind these apps and services became prolific because of their disregard of privacy rights, which normalized lucrative surveillance capitalism. There is such an extensive global monopoly that in Africa, only 1 of the 5 biggest messaging apps in Africa isn't owned by Meta, notoriously known for spying not just through its own apps but even through [its competitors](https://qz.com/project-ghostbusters-facebook-meta-wiretap-snapchat-1851366814), – relentless, massive data harvesting that stretches far beyond its own walled gardens: + +Some of the world’s top engineers often go to these companies because of the benefits and financial opportunities. We can question their ethics all day long, but we also need to question if the web would look significantly different if there were as many opportunities at privacy-first companies with purpose and strong, proven moral boundaries, set up in a way that can guarantee operational independence from any shareholders and VCs. + +SimpleX could have taken the route of other companies in the privacy space, whether it’s Skiff which rushed to take a large amount of [VC money](https://techcrunch.com/2022/03/30/skiff-series-a-encrypted-workspaces/) only to [shutter its doors](https://www.techradar.com/computing/cyber-security/skiff-gets-bought-by-notion-raising-privacy-concerns) after an acquisition, leaving its users hanging with many unanswered questions, or giving up control of the company, which would puts its future solely in the hands of VCs with majority ownership. SimpleX aims to prevent this, and in fact has left money on the table to ensure that it does not occur. Had it not been for this information, I would not have joined, and I would have remained a user of the product, albeit a very cautious one, constantly wondering whether it will be sold or corrupted. + +It’s worth noting that some private foundations operate on the VC model in supporting nonprofits, either by requiring Board seats or requesting that their funding be used towards very specific objectives not always in alignment with the organization’s values and mission. It’s also worth noting that [some nonprofits](https://www.engadget.com/2019-05-31-sex-lies-and-surveillance-fosta-privacy.html) actually operate on the models of surveillance and censorship. Therefore, whether an organization or company is VC-backed or a nonprofit should not be the sole factor in deciding whether or not it is trustworthy. Actions are important, with full transparency being one of the most critical factors, and being fully open source being another to attract valid criticisms and audits to ensure any product or protocol lives up to its privacy and security promise. SimpleX Chat prides itself on being both transparent and open, on top of also being fully decentralized. If you’re new to it and eager to know more, you can start with [this overview](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md). + +Another important consideration is that the SimpleX network does have a plan that would rely on users' payments for specific or tailored services, and not on some other sources of revenue or funds (ads, etc.). Building anything that users would be willing to pay for requires substantially more time and resources, hence the VC route to establish a business model that doesn’t translate to the user being the product. But any business services need to be separate from SimpleX as a public interest technology. As outlined in this [recent post](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md), I’ll be using my background in nonprofit governance structures to ensure that the SimpleX network protocols evolve under the stewardship of nonprofit entities in various jurisdictions, so that its continued evolution aligns more closely with the vision of community-driven, independent and decentralized governance. This would help create a necessary balance between different structures, in the same way many tech nonprofits also have for-profit subsidiaries to attract fee-for-service agreements to sustain their operations. + +In summary: My decision to join Simplex Chat, despite my deep-rooted beliefs and skepticism towards VC funding, reflects a broader realization: that the fight for privacy, security, and decentralization in today’s web is multifaceted and sometimes requires us to depart from our comfort zones to explore sustainable paths for continuous growth and impact so that open source privacy tools and protocols are no longer “niche”, but universally accessible standards. As long as nothing in this journey compromises our moral principles and integrity, this will remain a very worthwhile goal to pursue. diff --git a/blog/README.md b/blog/README.md index 7f27c46c76..cf93260676 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,13 @@ # Blog +Apr 4. 2024 [Why I joined SimpleX Chat](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md) + +_By [Esra'a al Shafei](https://mastodon.social/@alshafei)_ + +Transitioning from a lifelong career dedicated to nonprofits, including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, my decision to join SimpleX Chat may come as a surprise to some. But, as I step into this new chapter, I want to share the insights and convictions that have guided me here, shedding light on what I think sets SimpleX Chat apart and why this move feels like an essential learning opportunity. + +--- + Mar 23, 2024 [SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration](./20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) SimpleX network: deliver real privacy via a profitable business and non-profit protocol governance: diff --git a/blog/images/20240404-esraa.png b/blog/images/20240404-esraa.png new file mode 100644 index 0000000000000000000000000000000000000000..baee24202340fdc8a9ae78613d88ac2432fcd933 GIT binary patch literal 11788 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�lpp4wal(ybGYx{LN zWU>0;G$|G2K*T{eUwOV5Cz=UoNg8$~T&@K<>_n!^2O@OcA{ZQ-i)-}CqG zO~IC&Cry9qI4+dk<+1Uz6l7E%%S8yL&u?f%?D(Q%1#Ogr3}vcNLDx10TMG&y0A(|Q zyod=s)XX%=IjQcsb8S<&LkkPLm0s4Bc8jN}V@aIHtr5iKr#OXJuKwH6Ywsq zaj!0Ss;0?}`rm8?`Fv>l^LjSD`BHQ*J4Ue$L-oO^KbL~EGzT(X`Qgiv;nREVz5MC}L*Z3^;vrYROlR zKRn#*0by|S-69K@1v)6MD%=cXCYEu8dtp z6WTo9c6CbMKkkkSCz%l!)yVNHjN)R}$QgU_?o9cB=Q#h*&7kWJ_^P%bzT{=CLEn)9Zp+5pMm>pOMO}p&az}hxuj3 zT#0?|52w=w_?YW~(ZpeEN&{>=jLK<{X7Z@2n4`*$QNdxCk?r?QWLM+6;$uQ{p^$2_ zgWopiTsS6pGSYzLR`)F0#0LSC+Q8&8HVHwi70;+M!kHb~GP7OP!)y0b^`51#Hc2_D zRuOpSCCw%g*H=;vDjse77IurA1!w%;@wEIRuO~zNnWe2I^d4GS%v(B#AO98})yi&l zNUKTRxCUM~XNz|a8W?(v#C^x!%t6$8tdz(crshX8mxS`5?lz0Mt@4kY%)^tznLPXN z-Xdw~Iyj`x=%q~zzqTF>LHk~kc_T}zPtCRZ@@ZPwf`6sE@^|My0I#xOKbVKg`d$_% zNB3K32b7zv<%Q=m>b@M!4}TqDKz6}l{!$2|ot!0D_vieRUhXsMs;Oh?`$m;5#g7np zILT+L%;=wbm{i7ri%aC+Up9&NTPnrz=ypSm{btltycPE%yL_Z?5iLZDBcCU3dwzf4 z{#!fMv__C>F zSiPg}{I=Lkh4{?|;Rt2LC=LsB;u9F8p2jWYJp7Z6kxrO4i`Mvs9U@Pw4b376tv+3E z$VXHv-+0u3xcsxTnh;{@pM=PK{$zb622GK+e8Ri6g2+{#^6FPy$xkPAn1&n9h5FU<$+b)B zoz(^zI~b!jQ(rvK&iYcP@exqpj|WsToof)yIPak#iit8nXaf`&IK zecY5@efoVCn0!CmBO5;g34eLIHYRzt>lxy@aP^DcqCxHathhok6b{w@YL0&yc+;BIE?s|kqfB(3?vWa5PO zS@nK`5Asjj%tYJb6$4?6U4IuJh>LF)triFT)O|nuQ61DFSnrPeTl43cP*~~T^|KxF zbr28QCr5YB;f&Sco=^z+^43`bYZDfz&C4bSae0TPbR*>I z&3U2M(xl3yoAI_*{eenQyMF#J%OXJ+kp}b`oxT%$3ohMw-82Wx(K5-WlCqTj@+A`d zGekgY7IsIN)*fE(s96-9oO&LDZjpEfavoZfKQwm!1Y0hA|r z&#&(;Q!y0?f%&1dVJ+gx;2(ywwRtb`ZXBiH`#417mqsdZua+_((?f{^}V`lnbr-USPCR^Llr=m zr6VH)n*LUpZVY_xw?=J8gQE?h0&tc=bupExeBC$};K zMHFT;kBp|Ltm`R+6Mg)~TSph9HrpZZU)&S=!Dk&L4UOP^dW8%?a%HrP3Z;=HRJzyA zo(_XYGYBil~QprQX?#n1n@66yc%s7rF0w;vBQ+y?#tMcE*24Sn@Dsy0#o18VA1 AHvj+t literal 0 HcmV?d00001 diff --git a/blog/images/20240404-messsaging-apps.png b/blog/images/20240404-messsaging-apps.png new file mode 100644 index 0000000000000000000000000000000000000000..6081a468a1cd3837d9aecbb31d8f8f1a60602200 GIT binary patch literal 186897 zcmeFZWmKF^(lCrOxLdFpoZt{3z~Js07y`lFnSsFI?(QVGhTsr_yL%u=LU5M^CpdiE z_wGKsXYWV$&-d@0IdiVQy34w%PIq-z_e5%_$>U%?!$d$pz)@6?(Lz8#BSJtxh62!@ zYHW&ye?2`=tR+10r{zf`1C~xw?#ntvy6c7MCs{+k`#u3 z{`4e#`WfaS{u43}=^v12M0v>nC?iAvK$6s!R#bd?YMVnXEuCOC&Tb6#yiXD!IoRqz z+#sqd!sgD7oTe7eW|o|ujxK+oAb>rEpNfu_Zl*v_M+YaEu%{^9pAf=N)+K~yz>(#}vzpa3T~CpVoKCJ+b&LoKX?wPfV}j{fu}N@wHd<|53+<>BGM>A}b8 z47KLs5fT#O;^yVz<>h#S;DEuM+)O<=oM7~SBk~VAGL|rNsI7~et+NyG54xsi&hBoa zbaa0(`mfL5*{{E`1OGux zSi{!S(g7l4>-ePAC(*=s`1$$4fAaZD(?2Es8|o{VB~;qk@d?vS?4Ne|JMzCa{yXBI zJoW#{^W{t4e`WbEE&q-w?d;$T)pjv8w-n>~JIcRQ{R{LTJr-7n+CIr<`iIK@0RKzf z-{HYrf7s(+?DIF{|5^KFeKAZh*ME7E7-meaX&(ZD1cIWBq>d-z&-U=#SF$Y+tqjbF zib|i}Q!xh1kdV}G6v3?2GGLQO%lR2eb_$#`qhQbY^$!QbIZ;mUb;VvVN3u4H)q9| zG3EaT^bd3(ehqIoQTP8&@lRkIQbabQccFvg&;A3tx=fz9|ErLH=RZzoVkuRBd?o+C z^yeRr&=$~)@v&b)^15z=@dp2le{{s`S z5rNVFC+qwRvp_3?8s2<;&%46^z^W#eK|B8eYx*zjNV-ffu;Tq+ApKvCV)CS?W2*n# zp-fQT$##u|SgoDJ|KB8Ip7fN1{2#zc)ITZz3p@B>>OXM4CntsdA8!ArV*j6${y!)E ze@^;;jy?Z>jHGT?20^Rgw0<9s4n;2)wI9FJ{wYQ$3W`(@qYyKNHS47^`K!zPO8ny+ z8#acchXc=wqX!-+*1wOaS8%#x!9tZQBa|Phqk2*H^VKBOB5&D+_hzfIsrmeJzRpz& zAj##$AZ&i0E!T@asxo9fY%YB>)vUZz;CuU9mn9|ebj3dzP$O3Lrd`+Pa>)ZV47DO2 zms;4<_YTP#+1&U3=3+g{V(oIilS|nBka*HZfbJR<_j9YozfBlj)bguwtVA7;54XUx z$!rm__t$g3iM1-UnOPBq-Q!|Gn1N|5$lo;lV87q5cLgH%=X2qG#7U2)xm-5Drg~ZP zfx<}pe1EA`cHY*}+qdnHQGQhoh`{5uH(k6wv{u+JIo$l!I2V_(=N@m-7~9tc;`(@h ziSFvfys$1D)3p?VP;`1?J{Zl_Af}e}Qs3wC{-QvAr9)QLQT3#&3q7jpP3r?)E^?4g z=iAEF=7Z_}=zYPJ7o5K=b*739<_qNGX-sXVuI3GHv35RS?MxngKX7ne>j_OzUwLz_ zrZnLl73URqK-YFO3;WhC>P@<&BRJ{d_)3G+*LOc<;Ks!D=g^er@=xCt-~O6+rv0nk z!Ry!6jFWMWvoywoe;Krom8Jhs7H4#yc+I0k?O}aG|4DU+9Z8&ngowu>)%(euc5jmQ zVualvo&Hmu{+(Bg#rEkRWxCw;i?YRhORmn7E&J^sZL)or>Jh8=VfFNT^y(&407G;bhn z#|LE51ch?CZNyZ|zpBXj1lK1(0ifQF_S>k&#~Z8KpM}N!YtrGpcK01(m$trOE%5F7 zS$|(R6I!O<&TDsTyPk~oq7>$X`6{;Ghu`U|i;tW>G-;Qr$^MF1>#qp6CSw6Q4|?TV zS4F3Dk}}8VvV3;_uKRPbLt1H~x4$fzIoi47S|I1E2Hp8={g0uk!YEt560!~EI4&=x zvM;2l{rid1@==UO{u(}4hqwJXOZ)XHSnXaH&#fjh-+<40VEtqIe9_C77f#XpJ~)2~ zj=EMDkUUf}T~5}E&F16Y)aI`=m|Guo>8>6ftL(Dqxi^=a&kE?seBZRyjw<_F+F$ZX zeMO9*4%m5Ysef>|)Go4rw^f9H1;mMT0fhN z9!=+5`9h7&S~8xYc}~kMvHZ+;^Zxr@nEC5d)Jg3Xzgx%Ji|Fxw!r{UTUaD?BglfQk z`ziI>{AOm}PE!>x0tH7fE?6ae*!yE;3I=(lPlsvYz)ht_9`F$WlM%I^$ilb|*LzMw zAON%-{%}M7SB1s`7I`dt8tnC=>len>vTM9|swfSnqnuqdj~MYnD-Aw}Z2d4-7c&~m z5s_a5qrIRTb*T|@1-hQ>>MTTvO03v^;B zGLkyP0u&_rD71L^RHc2R#>=HR_pLWeaKQ

i2P!9@uqDp2tQIJ)7BYVj=8-PMR; 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

*}&}hE=2e(a8A9|a}rW4 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 F3$XU; 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]p6yEXt}#e>5h}Uq#|YkkEQP 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

GIJ&Q~m5)w6poScBO)&~zYz-HfY=^^yb){PN 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 literal 0 HcmV?d00001 diff --git a/website/src/_includes/blog_previews/20240404.html b/website/src/_includes/blog_previews/20240404.html new file mode 100644 index 0000000000..b2407b3ab2 --- /dev/null +++ b/website/src/_includes/blog_previews/20240404.html @@ -0,0 +1,8 @@ +By Esra'a al Shafei + +

Transitioning from a lifelong career dedicated to nonprofits, +including Board roles at organizations like the Wikimedia Foundation, Access Now and Tor, +my decision to join SimpleX Chat may come as a surprise to some. +But, as I step into this new chapter, I want to share the insights and convictions +that have guided me here, shedding light on what I think sets SimpleX Chat apart +and why this move feels like an essential learning opportunity.

From a725d2efacc395bb43cd51fc233bdb9d2bbe6567 Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:18:33 +0700 Subject: [PATCH 12/14] desktop (windows): fix build (#3990) --- apps/multiplatform/common/build.gradle.kts | 4 +--- apps/multiplatform/desktop/build.gradle.kts | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 4cc5ced0d1..9a7b7fb2e3 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -12,9 +12,7 @@ version = extra["android.version_name"] as String kotlin { androidTarget() - jvm("desktop") { - jvmToolchain(11) - } + jvm("desktop") applyDefaultHierarchyTemplate() sourceSets { all { diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index c3dd9bb9b0..401c2938d5 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -12,10 +12,7 @@ version = extra["desktop.version_name"] as String kotlin { - jvm { - jvmToolchain(11) - withJava() - } + jvm() sourceSets { val jvmMain by getting { dependencies { @@ -151,7 +148,7 @@ cmake { tasks.named("clean") { dependsOn("cmakeClean") } -tasks.named("compileJava") { +tasks.named("compileKotlinJvm") { dependsOn("cmakeBuildAndCopy") } afterEvaluate { From 069395c2a0bd3c8793182bd7433e36ba290a8864 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:24:42 +0400 Subject: [PATCH 13/14] core: entity locks (#3962) * core: entity locks * more locks * update sha256map * add delay * clean up * empty * fix tests * empty * empty * more delays * empty * comment delays * Revert "comment delays" This reverts commit 4245b545fba4dc20d2bc7ce08c0cd46907722fc6. * Revert "Revert "comment delays"" This reverts commit f803386945abe6a3e1d9d94e31e9ed9af2061330. * take lock in the beginning of processing loop * empty * empty * remove lock * rework file locks * empty * fix * empty * add connection locks * empty * fix test * empty * remove commented delays * add to debug locks * update * refactor * refactor --------- Co-authored-by: Evgeny Poberezkin --- cabal.project | 2 +- scripts/nix/sha256map.nix | 2 +- src/Simplex/Chat.hs | 349 ++++++++++++++------------ src/Simplex/Chat/Controller.hs | 15 +- src/Simplex/Chat/Protocol.hs | 2 +- src/Simplex/Chat/Store.hs | 1 + src/Simplex/Chat/Store/Connections.hs | 29 ++- src/Simplex/Chat/Store/Files.hs | 6 +- src/Simplex/Chat/Store/Shared.hs | 9 + src/Simplex/Chat/View.hs | 5 +- tests/ChatTests/Files.hs | 3 +- tests/ChatTests/Profiles.hs | 10 +- 12 files changed, 255 insertions(+), 178 deletions(-) diff --git a/cabal.project b/cabal.project index 62115c136b..4e8cfd22ee 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: 6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd + tag: 791368c7beb3996ab4a10f25dbf8cad1e289b413 source-repository-package type: git diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index d2701dc45f..a671f28751 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."6bc4f6c94e11f59604b0d9c576e62e01bc08b4cd" = "08l00ay1ibz7skhlpfjp6z2821zpfd0kxplwdm6zc63m2f6za7cv"; + "https://github.com/simplex-chat/simplexmq.git"."791368c7beb3996ab4a10f25dbf8cad1e289b413" = "0wbxk69lv6h17b5rdqskxwhc1wfvn1zi8q4a4w57qfzkzyaxkymk"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 7948875180..c62fdfb8bd 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -88,9 +88,9 @@ import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescr import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) -import Simplex.Messaging.Agent.Lock +import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) @@ -227,6 +227,7 @@ newChatController connNetworkStatuses <- atomically TM.empty subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO + entityLocks <- atomically TM.empty sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty @@ -263,6 +264,7 @@ newChatController connNetworkStatuses, subscriptionMode, chatLock, + entityLocks, sndFiles, rcvFiles, currentCalls, @@ -310,6 +312,40 @@ newChatController userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') +withChatLock :: String -> CM a -> CM a +withChatLock name action = asks chatLock >>= \l -> withLock l name action + +withEntityLock :: String -> ChatLockEntity -> CM a -> CM a +withEntityLock name entity action = do + chatLock <- asks chatLock + ls <- asks entityLocks + atomically $ unlessM (isEmptyTMVar chatLock) retry + withLockMap ls entity name action + +withInvitationLock :: String -> ByteString -> CM a -> CM a +withInvitationLock name = withEntityLock name . CLInvitation +{-# INLINE withInvitationLock #-} + +withConnectionLock :: String -> Int64 -> CM a -> CM a +withConnectionLock name = withEntityLock name . CLConnection +{-# INLINE withConnectionLock #-} + +withContactLock :: String -> ContactId -> CM a -> CM a +withContactLock name = withEntityLock name . CLContact +{-# INLINE withContactLock #-} + +withGroupLock :: String -> GroupId -> CM a -> CM a +withGroupLock name = withEntityLock name . CLGroup +{-# INLINE withGroupLock #-} + +withUserContactLock :: String -> Int64 -> CM a -> CM a +withUserContactLock name = withEntityLock name . CLUserContact +{-# INLINE withUserContactLock #-} + +withFileLock :: String -> Int64 -> CM a -> CM a +withFileLock name = withEntityLock name . CLFile +{-# INLINE withFileLock #-} + activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) activeAgentServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) @@ -669,8 +705,8 @@ processChatCommand' vr = \case memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses _ -> pure Nothing pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} - APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> withChatLock "sendMessage" $ case cType of - CTDirect -> do + APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user -> case cType of + CTDirect -> withContactLock "sendMessage" chatId $ do ct@Contact {contactId, contactUsed} <- withStore $ \db -> getContact db vr user chatId assertDirectAllowed user MDSnd ct XMsgNew_ unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct @@ -707,7 +743,7 @@ processChatCommand' vr = \case quoteData ChatItem {content = CISndMsgContent qmc} = pure (qmc, CIQDirectSnd, True) quoteData ChatItem {content = CIRcvMsgContent qmc} = pure (qmc, CIQDirectRcv, False) quoteData _ = throwChatError CEInvalidQuote - CTGroup -> do + CTGroup -> withGroupLock "sendMessage" chatId $ do g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user chatId assertUserGroupRole gInfo GRAuthor send g @@ -767,8 +803,8 @@ processChatCommand' vr = \case pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing Nothing False createdAt Nothing createdAt pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci - APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of - CTDirect -> do + APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of + CTDirect -> withContactLock "updateChatItem" chatId $ do ct@Contact {contactId} <- withStore $ \db -> getContact db vr user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId @@ -790,7 +826,7 @@ processChatCommand' vr = \case else pure $ CRChatItemNotChanged user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate - CTGroup -> do + CTGroup -> withGroupLock "updateChatItem" chatId $ do Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId assertUserGroupRole gInfo GRAuthor cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId @@ -825,8 +861,8 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> withChatLock "deleteChatItem" $ case cType of - CTDirect -> do + APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> case cType of + CTDirect -> withContactLock "deleteChatItem" chatId $ do (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}}) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId case (mode, msgDir, itemSharedMsgId, editable) of (CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False @@ -837,7 +873,7 @@ processChatCommand' vr = \case then deleteDirectCI user ct ci True False else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete - CTGroup -> do + CTGroup -> withGroupLock "deleteChatItem" chatId $ do Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, editable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId case (mode, msgDir, itemSharedMsgId, editable) of @@ -852,7 +888,7 @@ processChatCommand' vr = \case deleteLocalCI user nf ci True False CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withChatLock "deleteChatItem" $ do + APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withStore $ \db -> getGroupChatItem db user gId itemId case (chatDir, itemSharedMsgId) of @@ -862,44 +898,46 @@ processChatCommand' vr = \case (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId delGroupChatItem user gInfo ci msgId (Just membership) (_, _) -> throwChatError CEInvalidChatItemDelete - APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> withChatLock "chatItemReaction" $ case cType of + APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> - withStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case - (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (featureAllowed SCFReactions forUser ct) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setDirectReaction db ct itemSharedMId True reaction add msgId createdAt - liftIO $ getDirectCIReactions db ct itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + withContactLock "chatItemReaction" chatId $ + withStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case + (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do + unless (featureAllowed SCFReactions forUser ct) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True + checkReactionAllowed rs + (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add + createdAt <- liftIO getCurrentTime + reactions <- withStore' $ \db -> do + setDirectReaction db ct itemSharedMId True reaction add msgId createdAt + liftIO $ getDirectCIReactions db ct itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTDirect SMDSnd (DirectChat ct) $ CIReaction CIDirectSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" CTGroup -> - withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case - (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do - unless (groupFeatureAllowed SGFReactions g) $ - throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) - unless (ciReactionAllowed ci) $ - throwChatError (CECommandError "reaction not allowed - chat item has no content") - let GroupMember {memberId = itemMemberId} = chatItemMember g ci - rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True - checkReactionAllowed rs - (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) - createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do - setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt - liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId - let ci' = CChatItem md ci {reactions} - r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction - pure $ CRChatItemReaction user add r - _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" + withGroupLock "chatItemReaction" chatId $ + withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case + (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do + unless (groupFeatureAllowed SGFReactions g) $ + throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) + unless (ciReactionAllowed ci) $ + throwChatError (CECommandError "reaction not allowed - chat item has no content") + let GroupMember {memberId = itemMemberId} = chatItemMember g ci + rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True + checkReactionAllowed rs + (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + createdAt <- liftIO getCurrentTime + reactions <- withStore' $ \db -> do + setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt + liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId + let ci' = CChatItem md ci {reactions} + r = ACIReaction SCTGroup SMDSnd (GroupChat g) $ CIReaction CIGroupSnd ci' createdAt reaction + pure $ CRChatItemReaction user add r + _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" CTLocal -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" @@ -959,7 +997,7 @@ processChatCommand' vr = \case CTDirect -> do ct <- withStore $ \db -> getContact db vr user chatId filesInfo <- withStore' $ \db -> getContactFileInfo db user ct - withChatLock "deleteChat direct" . procCmd $ do + withContactLock "deleteChat direct" chatId . procCmd $ do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo let doSendDel = contactReady ct && contactActive ct && notify @@ -971,7 +1009,7 @@ processChatCommand' vr = \case withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct withStore $ \db -> deleteContact db user ct pure $ CRContactDeleted user ct - CTContactConnection -> withChatLock "deleteChat contactConnection" . procCmd $ do + CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync user acId withStore' $ \db -> deletePendingContactConnection db userId chatId @@ -983,7 +1021,7 @@ processChatCommand' vr = \case canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - withChatLock "deleteChat group" . procCmd $ do + withGroupLock "deleteChat group" chatId . procCmd $ do cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo let doSendDel = memberActive membership && isOwner @@ -1038,28 +1076,29 @@ processChatCommand' vr = \case CTLocal -> do nf <- withStore $ \db -> getNoteFolder db user chatId filesInfo <- withStore' $ \db -> getNoteFolderFileInfo db user nf - withChatLock "clearChat local" . procCmd $ do - deleteFilesLocally filesInfo - withStore' $ \db -> deleteNoteFolderFiles db userId nf - withStore' $ \db -> deleteNoteFolderCIs db user nf - pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) + deleteFilesLocally filesInfo + withStore' $ \db -> deleteNoteFolderFiles db userId nf + withStore' $ \db -> deleteNoteFolderCIs db user nf + pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do + APIAcceptContact incognito connReqId -> withUser $ \_ -> do (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withStore $ \db -> getContactRequest' db connReqId - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId - let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl - -- [incognito] generate profile to send, create connection with incognito profile - incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing - ct <- acceptContactRequest user cReq incognitoProfile contactUsed - pure $ CRAcceptingContactRequest user ct - APIRejectContact connReqId -> withUser $ \user -> withChatLock "rejectContact" $ do - cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- + withUserContactLock "acceptContact" userContactLinkId $ do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl + -- [incognito] generate profile to send, create connection with incognito profile + incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing + ct <- acceptContactRequest user cReq incognitoProfile contactUsed + pure $ CRAcceptingContactRequest user ct + APIRejectContact connReqId -> withUser $ \user -> do + cReq@UserContactRequest {userContactLinkId, agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- withStore $ \db -> getContactRequest db user connReqId `storeFinally` liftIO (deleteContactRequest db user connReqId) - withAgent $ \a -> rejectContact a connId invId - pure $ CRContactRequestRejected user cReq + withUserContactLock "rejectContact" userContactLinkId $ do + withAgent $ \a -> rejectContact a connId invId + pure $ CRContactRequestRejected user cReq APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withStore $ \db -> getContact db vr user contactId @@ -1067,7 +1106,7 @@ processChatCommand' vr = \case if featureAllowed SCFCalls forUser ct then do calls <- asks currentCalls - withChatLock "sendCallInvitation" $ do + withContactLock "sendCallInvitation" contactId $ do g <- asks random callId <- atomically $ CallId <$> C.randomBytes 16 g dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing @@ -1192,12 +1231,11 @@ processChatCommand' vr = \case toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ - withChatLock "setUserSMPServers" $ do - withStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers - ok user + APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do + withStore $ \db -> overwriteProtocolServers db user servers + cfg <- asks config + lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers + ok user SetUserProtoServers serversConfig -> withUser $ \User {userId} -> processChatCommand $ APISetUserProtoServers userId serversConfig APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> @@ -1300,7 +1338,7 @@ processChatCommand' vr = \case connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRGroupMemberSwitchAborted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive - APISyncContactRatchet contactId force -> withUser $ \user -> withChatLock "syncContactRatchet" $ do + APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do ct <- withStore $ \db -> getContact db vr user contactId case contactConn ct of Just conn@Connection {pqSupport} -> do @@ -1308,7 +1346,7 @@ processChatCommand' vr = \case createInternalChatItem user (CDDirectSnd ct) (CISndConnEvent $ SCERatchetSync rss Nothing) Nothing pure $ CRContactRatchetSyncStarted user ct cStats Nothing -> throwChatError $ CEContactNotActive ct - APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withChatLock "syncGroupMemberRatchet" $ do + APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case memberConnId m of Just connId -> do @@ -1397,7 +1435,7 @@ 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 -> withChatLock "addContact" . procCmd $ do + APIAddContact userId incognito -> withUserId userId $ \user -> procCmd $ do -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode @@ -1424,9 +1462,8 @@ processChatCommand' vr = \case Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' Nothing -> throwChatError CEConnectionIncognitoChangeProhibited APIConnectPlan userId cReqUri -> withUserId userId $ \user -> - withChatLock "connectPlan" . procCmd $ - CRConnectionPlan user <$> connectPlan user cReqUri - APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + CRConnectionPlan user <$> connectPlan user cReqUri + APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing @@ -1471,7 +1508,7 @@ processChatCommand' vr = \case CRContactsList user <$> withStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId - APICreateMyAddress userId -> withUserId userId $ \user -> withChatLock "createMyAddress" . procCmd $ do + APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode -- TODO v5.7 pass IPPQOn (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOff subMode @@ -1636,7 +1673,7 @@ processChatCommand' vr = \case pure $ CRGroupCreated user groupInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand $ APINewGroup userId incognito gProfile - APIAddMember groupId contactId memRole -> withUser $ \user -> withChatLock "addMember" $ do + 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) <- withStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId assertDirectAllowed user MDSnd contact XGrpInv_ @@ -1666,7 +1703,7 @@ processChatCommand' vr = \case Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do - withChatLock "joinGroup" . procCmd $ do + withGroupLock "joinGroup" groupId . procCmd $ do (invitation, ct) <- withStore $ \db -> do inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId (inv,) <$> getContactViaMember db vr user fromMember @@ -1697,7 +1734,7 @@ processChatCommand' vr = \case changeMemberRole user gInfo members m gEvent = do let GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberContactId, localDisplayName = cName} = m assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] - withChatLock "memberRole" . procCmd $ do + withGroupLock "memberRole" groupId . procCmd $ do unless (mRole == memRole) $ do withStore' $ \db -> updateGroupMemberRole db user m memRole case mStatus of @@ -1719,7 +1756,7 @@ processChatCommand' vr = \case let GroupMember {memberId = bmMemberId, memberRole = bmRole, memberProfile = bmp} = bm assertUserGroupRole gInfo $ max GRAdmin bmRole when (blocked == blockedByAdmin bm) $ throwChatError $ CECommandError $ if blocked then "already blocked" else "already unblocked" - withChatLock "blockForAll" . procCmd $ do + withGroupLock "blockForAll" groupId . procCmd $ do let mrs = if blocked then MRSBlocked else MRSUnrestricted event = XGrpMemRestrict bmMemberId MemberRestrictions {restriction = mrs} (msg, _) <- sendGroupMessage' user gInfo remainingMembers event @@ -1741,7 +1778,7 @@ processChatCommand' vr = \case Nothing -> throwChatError CEGroupMemberNotFound Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do assertUserGroupRole gInfo $ max GRAdmin mRole - withChatLock "removeMember" . procCmd $ do + withGroupLock "removeMember" groupId . procCmd $ do case mStatus of GSMemInvited -> do deleteMemberConnection user m @@ -1757,7 +1794,7 @@ processChatCommand' vr = \case APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo - withChatLock "leaveGroup" . procCmd $ do + withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo (msg, _) <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) @@ -1807,7 +1844,7 @@ processChatCommand' vr = \case updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withStore (\db -> getGroupInfoByName db vr user gName) - APICreateGroupLink groupId mRole -> withUser $ \user -> withChatLock "createGroupLink" $ do + APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole @@ -1817,14 +1854,14 @@ processChatCommand' vr = \case (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) IKPQOff subMode withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo cReq mRole - APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do + APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId (groupLinkId, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo assertUserGroupRole gInfo GRAdmin when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' when (mRole' /= mRole) $ withStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' pure $ CRGroupLink user gInfo groupLink mRole' - APIDeleteGroupLink groupId -> withUser $ \user -> withChatLock "deleteGroupLink" $ do + APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do gInfo <- withStore $ \db -> getGroupInfo db vr user groupId deleteGroupLink' user gInfo pure $ CRGroupLinkDeleted user gInfo @@ -1932,19 +1969,19 @@ processChatCommand' vr = \case ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> - withChatLock "receiveFile" . procCmd $ do + withFileLock "receiveFile" fileId . procCmd $ do (user, ft) <- withStore (`getRcvFileTransferById` fileId) encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles ft' <- (if encrypt then setFileToEncrypt else pure) ft receiveFile' user ft' rcvInline_ filePath_ SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do - withChatLock "setFileToReceive" . procCmd $ do + withFileLock "setFileToReceive" fileId . procCmd $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> - withChatLock "cancelFile" . procCmd $ + withFileLock "cancelFile" fileId . procCmd $ withStore (\db -> getFileTransfer db user fileId) >>= \case FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" @@ -2074,8 +2111,18 @@ processChatCommand' vr = \case pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} DebugLocks -> lift $ do chatLockName <- atomically . tryReadTMVar =<< asks chatLock + chatEntityLocks <- getLocks =<< asks entityLocks agentLocks <- withAgent' debugAgentLocks - pure CRDebugLocks {chatLockName, agentLocks} + pure CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} + where + getLocks ls = atomically $ M.mapKeys enityLockString . M.mapMaybe id <$> (mapM tryReadTMVar =<< readTVar ls) + enityLockString cle = case cle of + CLInvitation bs -> "Invitation " <> B.unpack bs + CLConnection connId -> "Connection " <> show connId + CLContact ctId -> "Contact " <> show ctId + CLGroup gId -> "Group " <> show gId + CLUserContact ucId -> "UserContact " <> show ucId + CLFile fId -> "File " <> show fId GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails GetAgentStats -> lift $ CRAgentStats . map stat <$> withAgent' getAgentStats @@ -2101,7 +2148,6 @@ processChatCommand' vr = \case -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" where - withChatLock name action = asks chatLock >>= \l -> withLock l name action -- below code would make command responses asynchronous where they can be slow -- in View.hs `r'` should be defined as `id` in this case -- procCmd :: m ChatResponse -> m ChatResponse @@ -2167,7 +2213,7 @@ processChatCommand' vr = \case CTLocal -> withStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> CM ChatResponse - connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do + connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq case groupLinkId of @@ -2198,7 +2244,7 @@ processChatCommand' vr = \case pure $ CRSentInvitation user conn incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectContactViaAddress user incognito ct cReq = - withChatLock "connectViaContact" $ do + withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do newXContactId <- XContactId <$> drgRandomBytes 16 pqSup <- chatReadVar pqExperimentalEnabled (connId, incognitoProfile, subMode, chatV) <- requestContact user incognito cReq newXContactId False pqSup @@ -2265,8 +2311,9 @@ processChatCommand' vr = \case -- [incognito] filter out contacts with whom user has incognito connections addChangedProfileContact :: User -> Contact -> [ChangedProfileContact] -> [ChangedProfileContact] addChangedProfileContact user' ct changedCts = case contactSendConn_ ct' of - Right conn | not (connIncognito conn) && mergedProfile' /= mergedProfile -> - ChangedProfileContact ct ct' mergedProfile' conn : changedCts + Right conn + | not (connIncognito conn) && mergedProfile' /= mergedProfile -> + ChangedProfileContact ct ct' mergedProfile' conn : changedCts _ -> changedCts where mergedProfile = userProfileToSend user Nothing (Just ct) False @@ -2289,7 +2336,7 @@ processChatCommand' vr = \case let mergedProfile = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct) False mergedProfile' = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') False when (mergedProfile' /= mergedProfile) $ - withChatLock "updateProfile" $ do + withContactLock "updateProfile" (contactId' ct) $ do void (sendDirectContactMessage user ct' $ XInfo mergedProfile') `catchChatError` (toView . CRChatError (Just user)) lift . when (directOrUsed ct') $ createSndFeatureItems user ct ct' pure $ CRContactPrefsUpdated user ct ct' @@ -2334,7 +2381,7 @@ processChatCommand' vr = \case user <- getUserByContactId db ctId (user,) <$> getContact db vr user ctId calls <- asks currentCalls - withChatLock "currentCall" $ + withContactLock "currentCall" ctId $ atomically (TM.lookup ctId calls) >>= \case Nothing -> throwChatError CENoCurrentCall Just call@Call {contactId} @@ -2988,21 +3035,16 @@ deleteGroupLink_ user gInfo conn = do agentSubscriber :: CM' () agentSubscriber = do q <- asks $ subQ . smpAgent - l <- asks chatLock - forever $ atomically (readTBQueue q) >>= process l + forever $ atomically (readTBQueue q) >>= process where - process :: Lock -> (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () - process l (corrId, entId, APC e msg) = run $ case e of + process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () + process (corrId, entId, APC e msg) = run $ case e of SAENone -> processAgentMessageNoConn msg SAEConn -> processAgentMessage corrId entId msg SAERcvFile -> processAgentMsgRcvFile corrId entId msg SAESndFile -> processAgentMsgSndFile corrId entId msg where - run action = do - let name = "agentSubscriber entity=" <> show e <> " entId=" <> str entId <> " msg=" <> str (aCommandTag msg) - withLock' l name $ action `catchChatError'` (toView' . CRChatError Nothing) - str :: StrEncoding a => a -> String - str = B.unpack . strEncode + run action = action `catchChatError'` (toView' . CRChatError Nothing) type AgentBatchSubscribe = AgentClient -> [ConnId] -> ExceptT AgentErrorType IO (Map ConnId (Either AgentErrorType ())) @@ -3150,8 +3192,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do forM_ err_ $ toView . CRSndFileSubError user ft void . forkIO $ do threadDelay 1000000 - l <- asks chatLock - when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withLock l "subscribe sendFileChunk" $ + when (fileStatus == FSConnected) . unlessM (isFileActive fileId sndFiles) . withChatLock "subscribe sendFileChunk" $ sendFileChunk user ft rcvFileSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId RcvFileTransfer -> CM () rcvFileSubsToView rs = mapM_ (toView . uncurry (CRRcvFileSubError user)) . filterErrors . resultsFor rs @@ -3317,11 +3358,13 @@ processAgentMessage _ connId (DEL_RCVQ srv qId err_) = processAgentMessage _ connId DEL_CONN = toView $ CRAgentConnDeleted (AgentConnId connId) processAgentMessage corrId connId msg = do - vr <- chatVersionRange - -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here - critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) - _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) + lockEntity <- critical (withStore (`getChatLockEntity` AgentConnId connId)) + withEntityLock "processAgentMessage" lockEntity $ do + vr <- chatVersionRange + -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here + critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case + Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user)) + _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. -- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries, @@ -3358,18 +3401,18 @@ processAgentMessageNoConn = \case toView $ event srv cs processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () -processAgentMsgSndFile _corrId aFileId msg = - withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) - throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId +processAgentMsgSndFile _corrId aFileId msg = do + fileId <- withStore (`getXFTPSndFileDBId` AgentSndFileId aFileId) + withFileLock "processAgentMsgSndFile" fileId $ + withStore' (`getUserByASndFileId` AgentSndFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + _ -> do + lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) + throwChatError $ CENoSndFileUser $ AgentSndFileId aFileId where - process :: User -> CM () - process user = do - (ft@FileTransferMeta {fileId, xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> do - fileId <- getXFTPSndFileDBId db user $ AgentSndFileId aFileId - getSndFileTransfer db user fileId + process :: User -> FileTransferId -> CM () + process user fileId = do + (ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId vr <- chatVersionRange unless cancelled $ case msg of SFPROG sndProgress sndTotal -> do @@ -3386,11 +3429,11 @@ processAgentMsgSndFile _corrId aFileId msg = lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) case rfds of - [] -> sendFileError "no receiver descriptions" fileId vr ft + [] -> sendFileError "no receiver descriptions" vr ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft - Just _ -> sendFileError "Prohibit chaining redirects" fileId vr ft + Just _ -> sendFileError "Prohibit chaining redirects" vr ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor @@ -3439,7 +3482,7 @@ processAgentMsgSndFile _corrId aFileId msg = | temporaryAgentError e -> throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e | otherwise -> - sendFileError (tshow e) fileId vr ft + sendFileError (tshow e) vr ft where fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText = safeDecodeUtf8 . strEncode @@ -3457,8 +3500,8 @@ processAgentMsgSndFile _corrId aFileId msg = case L.nonEmpty fds of Just fds' -> loopSend fds' Nothing -> pure msgDeliveryId - sendFileError :: Text -> Int64 -> (PQSupport -> VersionRangeChat) -> FileTransferMeta -> CM () - sendFileError err fileId vr ft = do + sendFileError :: Text -> (PQSupport -> VersionRangeChat) -> FileTransferMeta -> CM () + sendFileError err vr ft = do logError $ "Sent file error: " <> err ci <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId CIFSSndError @@ -3480,18 +3523,18 @@ splitFileDescr rfdText = do else fileDescr <| splitParts (partNo + 1) partSize rest processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () -processAgentMsgRcvFile _corrId aFileId msg = - withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case - Just user -> process user `catchChatError` (toView . CRChatError (Just user)) - _ -> do - lift $ withAgent' (`xftpDeleteRcvFile` aFileId) - throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId +processAgentMsgRcvFile _corrId aFileId msg = do + fileId <- withStore (`getXFTPRcvFileDBId` AgentRcvFileId aFileId) + withFileLock "processAgentMsgRcvFile" fileId $ + withStore' (`getUserByARcvFileId` AgentRcvFileId aFileId) >>= \case + Just user -> process user fileId `catchChatError` (toView . CRChatError (Just user)) + _ -> do + lift $ withAgent' (`xftpDeleteRcvFile` aFileId) + throwChatError $ CENoRcvFileUser $ AgentRcvFileId aFileId where - process :: User -> CM () - process user = do - ft@RcvFileTransfer {fileId} <- withStore $ \db -> do - fileId <- getXFTPRcvFileDBId db $ AgentRcvFileId aFileId - getRcvFileTransfer db user fileId + process :: User -> FileTransferId -> CM () + process user fileId = do + ft <- withStore $ \db -> getRcvFileTransfer db user fileId vr <- chatVersionRange unless (rcvFileCompleteOrCancelled ft) $ case msg of RFPROG rcvProgress rcvTotal -> do @@ -3597,7 +3640,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- probably this branch is never executed, so there should be no reason -- to save message if contact hasn't been created yet - chat item isn't created anyway withAckMessage' agentConnId meta $ - void $ saveDirectRcvMSG conn meta msgBody + void $ + saveDirectRcvMSG conn meta msgBody SENT msgId -> sentMsgDeliveryEvent conn msgId OK -> @@ -3634,7 +3678,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody let ct'' = ct' {activeConn = Just conn''} :: Contact assertDirectAllowed user MDRcv ct'' $ toCMEventTag event - updateChatLock "direct message" event case event of XMsgNew mc -> newContentMessage ct'' mc msg msgMeta XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr @@ -4053,7 +4096,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processEvent :: MsgEncodingI e => ChatMessage e -> CM () processEvent chatMsg = do (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg - updateChatLock "groupMessage" event case event of XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False XMsgFileDescr sharedMsgId fileDescr -> memberCanSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr @@ -4389,13 +4431,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CRConnectionDisabled connEntity _ -> pure () - updateChatLock :: MsgEncodingI enc => String -> ChatMsgEvent enc -> CM () - updateChatLock name event = do - l <- asks chatLock - atomically $ tryReadTMVar l >>= mapM_ (swapTMVar l . (<> s)) - where - s = " " <> name <> "=" <> B.unpack (strEncode $ toCMEventTag event) - -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- we could save command records only for agent APIs we process continuations for (INV) withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () @@ -4433,9 +4468,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- This prevents losing the message that failed to be processed. Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing Left e -> ackMsg msgMeta Nothing >> throwError e - where - ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () - ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt + where + ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () + ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt sentMsgDeliveryEvent :: Connection -> AgentMsgId -> CM () sentMsgDeliveryEvent Connection {connId} msgId = diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 0291793843..024757e7bb 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -59,7 +59,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types -import Simplex.Chat.Store (AutoAccept, StoreError (..), UserContactLink, UserMsgReceiptSettings) +import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Util (liftIOEither) @@ -165,7 +165,7 @@ defaultChatHooks = ChatHooks { preCmdHook = \_ -> pure . Right, eventHook = \_ -> pure - } + } data DefaultAgentServers = DefaultAgentServers { smp :: NonEmpty SMPServerWithAuth, @@ -208,6 +208,7 @@ data ChatController = ChatController connNetworkStatuses :: TMap AgentConnId NetworkStatus, subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, + entityLocks :: TMap ChatLockEntity Lock, sndFiles :: TVar (Map Int64 Handle), rcvFiles :: TVar (Map Int64 Handle), currentCalls :: TMap ContactId Call, @@ -491,9 +492,9 @@ data ChatCommand | GetAgentSubsDetails | GetAgentWorkers | GetAgentWorkersDetails - -- The parser will return this command for strings that start from "//". - -- This command should be processed in preCmdHook - | CustomChatCommand ByteString + | -- The parser will return this command for strings that start from "//". + -- This command should be processed in preCmdHook + CustomChatCommand ByteString deriving (Show) allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal @@ -731,7 +732,7 @@ data ChatResponse | CRContactPQEnabled {user :: User, contact :: Contact, pqEnabled :: PQEncryption} | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} - | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} + | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} | CRAgentStats {agentStats :: [[String]]} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} @@ -1353,7 +1354,7 @@ handleDBErrors = [ E.Handler $ \(e :: SQLError) -> let se = SQL.sqlError e busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked - in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e, + in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e, E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e ] diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index e2810dafa9..8727a592a7 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -48,7 +48,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Compression (compress1, decompressBatch) -import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOn, pattern PQSupportOff) +import Simplex.Messaging.Crypto.Ratchet (PQSupport (..), pattern PQSupportOff, pattern PQSupportOn) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 91021713b1..4b0591fb3a 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -1,6 +1,7 @@ module Simplex.Chat.Store ( SQLiteStore, StoreError (..), + ChatLockEntity (..), UserMsgReceiptSettings (..), UserContactLink (..), AutoAccept (..), diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 6584aabb0a..0e543eacf2 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -3,11 +3,13 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeOperators #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} module Simplex.Chat.Store.Connections - ( getConnectionEntity, + ( getChatLockEntity, + getConnectionEntity, getConnectionEntityByConnReq, getContactConnEntityByConnReqHash, getConnectionsToSubscribe, @@ -37,6 +39,31 @@ import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Crypto.Ratchet (PQSupport) import Simplex.Messaging.Util (eitherToMaybe) +getChatLockEntity :: DB.Connection -> AgentConnId -> ExceptT StoreError IO ChatLockEntity +getChatLockEntity db agentConnId = do + ((connId, connType) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId)) <- + ExceptT . firstRow id (SEConnectionNotFound agentConnId) $ + DB.query + db + [sql| + SELECT connection_id, conn_type, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id + FROM connections + WHERE agent_conn_id = ? + |] + (Only agentConnId) + let err = throwError $ SEInternalError $ "connection " <> show connType <> " without entity" + case connType of + ConnMember -> maybe err (fmap CLGroup . getMemberGroupId) groupMemberId + ConnContact -> pure $ maybe (CLConnection connId) CLContact contactId + ConnSndFile -> maybe err (pure . CLFile) sndFileId + ConnRcvFile -> maybe err (pure . CLFile) rcvFileId + ConnUserContact -> maybe err (pure . CLUserContact) userContactLinkId + where + getMemberGroupId :: GroupMemberId -> ExceptT StoreError IO GroupId + getMemberGroupId groupMemberId = + ExceptT . firstRow fromOnly (SEInternalError "group member connection group_id not found") $ + DB.query db "SELECT group_id FROM group_members WHERE group_member_id = ?" (Only groupMemberId) + getConnectionEntity :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do c@Connection {connType, entityId} <- getConnection_ diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index e77681bb9b..8ac54c7e9b 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -336,10 +336,10 @@ setSndFTAgentDeleted db User {userId} fileId = do "UPDATE files SET agent_snd_file_deleted = 1, updated_at = ? WHERE user_id = ? AND file_id = ?" (currentTs, userId, fileId) -getXFTPSndFileDBId :: DB.Connection -> User -> AgentSndFileId -> ExceptT StoreError IO FileTransferId -getXFTPSndFileDBId db User {userId} aSndFileId = +getXFTPSndFileDBId :: DB.Connection -> AgentSndFileId -> ExceptT StoreError IO FileTransferId +getXFTPSndFileDBId db aSndFileId = ExceptT . firstRow fromOnly (SESndFileNotFoundXFTP aSndFileId) $ - DB.query db "SELECT file_id FROM files WHERE user_id = ? AND agent_snd_file_id = ?" (userId, aSndFileId) + DB.query db "SELECT file_id FROM files WHERE agent_snd_file_id = ?" (Only aSndFileId) getXFTPRcvFileDBId :: DB.Connection -> AgentRcvFileId -> ExceptT StoreError IO FileTransferId getXFTPRcvFileDBId db aRcvFileId = diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 88540134fe..ef7cda4802 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -45,6 +45,15 @@ import Simplex.Messaging.Util (allFinally) import Simplex.Messaging.Version import UnliftIO.STM +data ChatLockEntity + = CLInvitation ByteString + | CLConnection Int64 + | CLContact ContactId + | CLGroup GroupId + | CLUserContact Int64 + | CLFile Int64 + deriving (Eq, Ord) + -- These error type constructors must be added to mobile apps data StoreError = SEDuplicateName diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 65a6626308..550fe97b18 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -351,8 +351,9 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe <> (" :: avg: " <> sShow timeAvg <> " ms") <> (" :: " <> plain (T.unwords $ T.lines query)) in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) - CRDebugLocks {chatLockName, agentLocks} -> + CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, + plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) ] CRAgentStats stats -> map (plain . intercalate ",") stats @@ -1595,7 +1596,7 @@ standaloneUploadComplete FileTransferMeta {fileId, fileName} = \case [] -> [fileTransferStr fileId fileName <> " upload complete."] uris -> fileTransferStr fileId fileName <> " upload complete. download with:" - : map plain uris + : map plain uris sndFile :: SndFileTransfer -> StyledString sndFile SndFileTransfer {fileId, fileName} = fileTransferStr fileId fileName diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 1e72df9156..e50b20844a 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -787,7 +787,8 @@ testXFTPCancelRcvRepeat = bob ##> "/fr 1 ./tests/tmp" bob <### [ "saving file 1 from alice to ./tests/tmp/testfile_1", - "started receiving file 1 (testfile) from alice" + "started receiving file 1 (testfile) from alice", + StartsWith "chat db error: SERcvFileNotFoundXFTP" ] bob <## "completed receiving file 1 (testfile) from alice" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 7996fde3ad..8a9191c988 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -179,10 +179,12 @@ testMultiWordProfileNames = alice <# "#'Our Team' 'Bob James'> hi" cath <# "#'Our Team' 'Bob James'> hi" alice `send` "@'Cath Johnson' hello" - alice <## "member #'Our Team' 'Cath Johnson' does not have direct connection, creating" - alice <## "contact for member #'Our Team' 'Cath Johnson' is created" - alice <## "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'" - alice <# "@'Cath Johnson' hello" + alice + <### [ "member #'Our Team' 'Cath Johnson' does not have direct connection, creating", + "contact for member #'Our Team' 'Cath Johnson' is created", + "sent invitation to connect directly to member #'Our Team' 'Cath Johnson'", + WithTime "@'Cath Johnson' hello" + ] cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" cath <# "'Alice Jones'> hello" cath <## "'Alice Jones': contact is connected" From 18efc28d16eecb6188bf34df157b72a645eb304b Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Thu, 4 Apr 2024 20:41:56 +0100 Subject: [PATCH 14/14] core: additional group preferences: prohibit SimpleX links, restrict some features to specific roles (#3964) * core: additional group preferences: prohibit SimpleX links, restrict some features to specific roles * add role to group preference items, tests --- .../src/Directory/Events.hs | 1 + .../src/Directory/Service.hs | 1 + simplex-chat.cabal | 1 + src/Simplex/Chat.hs | 72 ++++++---- src/Simplex/Chat/Controller.hs | 4 +- src/Simplex/Chat/Markdown.hs | 9 ++ src/Simplex/Chat/Messages/CIContent.hs | 33 ++--- src/Simplex/Chat/Messages/CIContent/Events.hs | 1 + src/Simplex/Chat/Protocol.hs | 1 + src/Simplex/Chat/Store/Groups.hs | 8 +- src/Simplex/Chat/Store/Profiles.hs | 1 + src/Simplex/Chat/Types.hs | 43 +----- src/Simplex/Chat/Types/Preferences.hs | 131 +++++++++++++++--- src/Simplex/Chat/Types/Shared.hs | 48 +++++++ src/Simplex/Chat/View.hs | 1 + tests/Bots/DirectoryTests.hs | 3 +- tests/ChatTests/Groups.hs | 4 +- tests/ChatTests/Profiles.hs | 126 ++++++++++++++++- tests/ChatTests/Utils.hs | 2 + tests/ProtocolTests.hs | 3 +- 20 files changed, 384 insertions(+), 109 deletions(-) create mode 100644 src/Simplex/Chat/Types/Shared.hs diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 1d7a866051..76f57585a8 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -31,6 +31,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util ((<$?>)) import Data.Char (isSpace) diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index c1428881b9..d158b57e22 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -36,6 +36,7 @@ import Simplex.Chat.Messages import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Chat.View (serializeChatResponse, simplexChatContact) import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index fb0635abad..005fbe10a9 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -174,6 +174,7 @@ library Simplex.Chat.Terminal.Output Simplex.Chat.Types Simplex.Chat.Types.Preferences + Simplex.Chat.Types.Shared Simplex.Chat.Types.Util Simplex.Chat.Util Simplex.Chat.View diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index c62fdfb8bd..d075c6cf70 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -80,6 +80,7 @@ import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Chat.Util (encryptFile, shuffle) import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard) @@ -748,10 +749,10 @@ processChatCommand' vr = \case assertUserGroupRole gInfo GRAuthor send g where - send g@(Group gInfo@GroupInfo {groupId} ms) - | isVoice mc && not (groupFeatureAllowed SGFVoice gInfo) = notAllowedError GFVoice - | not (isVoice mc) && isJust file_ && not (groupFeatureAllowed SGFFiles gInfo) = notAllowedError GFFiles - | otherwise = do + send g@(Group gInfo@GroupInfo {groupId, membership} ms) = + case prohibitedGroupContent gInfo membership mc file_ of + Just f -> notAllowedError f + Nothing -> do (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) timed_ <- sndGroupCITimed live gInfo itemTTL (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ fInv_ timed_ live @@ -1587,8 +1588,9 @@ processChatCommand' vr = \case let mc = MCText msg case memberContactId m of Nothing -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user gId - toView $ CRNoMemberContactCreating user gInfo m + g <- withStore $ \db -> getGroupInfo db vr user gId + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" + toView $ CRNoMemberContactCreating user g m processChatCommand (APICreateMemberContact gId mId) >>= \case cr@(CRNewMemberContact _ Contact {contactId} _ _) -> do toView cr @@ -1872,7 +1874,7 @@ processChatCommand' vr = \case APICreateMemberContact gId gMemberId -> withUser $ \user -> do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId assertUserGroupRole g GRAuthor - unless (groupFeatureAllowed SGFDirectMessages g) $ throwChatError $ CECommandError "direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" case memberConn m of Just mConn@Connection {peerChatVRange} -> do unless (maxVersion peerChatVRange >= groupDirectInvVersion) $ throwChatError CEPeerChatVRangeIncompatible @@ -2053,9 +2055,12 @@ processChatCommand' vr = \case ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db vr user cName let prefs' = setPreference f allowed_ $ Just userPreferences updateContactPrefs user ct prefs' - SetGroupFeature (AGF f) gName enabled -> + SetGroupFeature (AGFNR f) gName enabled -> updateGroupProfileByName gName $ \p -> p {groupPreferences = Just . setGroupPreference f enabled $ groupPreferences p} + SetGroupFeatureRole (AGFR f) gName enabled role -> + updateGroupProfileByName gName $ \p -> + p {groupPreferences = Just . setGroupPreferenceRole f enabled role $ groupPreferences p} SetUserTimedMessages onOff -> withUser $ \user@User {profile} -> do let allowed = if onOff then FAYes else FANo pref = TimedMessagesPreference allowed Nothing @@ -2645,7 +2650,7 @@ assertDirectAllowed user dir ct event = unless (allowedChatEvent || anyDirectOrUsed ct) . unlessM directMessagesAllowed $ throwChatError (CEDirectMessagesProhibited dir ct) where - directMessagesAllowed = any (groupFeatureAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) + directMessagesAllowed = any (uncurry $ groupFeatureMemberAllowed' SGFDirectMessages) <$> withStore' (\db -> getContactGroupPreferences db user ct) allowedChatEvent = case event of XMsgNew_ -> False XMsgUpdate_ -> False @@ -2655,6 +2660,13 @@ assertDirectAllowed user dir ct event = XCallInv_ -> False _ -> True +prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> Maybe GroupFeature +prohibitedGroupContent gInfo m mc file_ + | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice + | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles + | not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks + | otherwise = Nothing + roundedFDCount :: Int -> Int roundedFDCount n | n <= 0 = 4 @@ -4739,14 +4751,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = newGroupContentMessage :: GroupInfo -> GroupMember -> MsgContainer -> RcvMessage -> UTCTime -> Bool -> CM () newGroupContentMessage gInfo m@GroupMember {memberId, memberRole} mc msg@RcvMessage {sharedMsgId_} brokerTs forwarded | blockedByAdmin m = createBlockedByAdmin - | isVoice content && not (groupFeatureAllowed SGFVoice gInfo) = rejected GFVoice - | not (isVoice content) && isJust fInv_ && not (groupFeatureAllowed SGFFiles gInfo) = rejected GFFiles - | otherwise = - withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case - Just ciModeration -> do - applyModeration ciModeration - withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ - Nothing -> createContentItem + | otherwise = case prohibitedGroupContent gInfo m content fInv_ of + Just f -> rejected f + Nothing -> + withStore' (\db -> getCIModeration db vr user gInfo memberId sharedMsgId_) >>= \case + Just ciModeration -> do + applyModeration ciModeration + withStore' $ \db -> deleteCIModeration db gInfo memberId sharedMsgId_ + Nothing -> createContentItem where rejected f = void $ newChatItem (CIRcvGroupFeatureRejected f) Nothing Nothing False timed' = if forwarded then rcvCITimed_ (Just Nothing) itemTTL else rcvGroupCITimed gInfo itemTTL @@ -5189,8 +5201,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createGroupFeatureItems g@GroupInfo {fullGroupPreferences} m = forM_ allGroupFeatures $ \(AGF f) -> do let p = getGroupPreference f fullGroupPreferences - (_, param) = groupFeatureState p - createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param) Nothing + (_, param, role) = groupFeatureState p + createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param role) Nothing xInfoProbe :: ContactOrMember -> Probe -> CM () xInfoProbe cgm2 probe = do @@ -5701,7 +5713,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> UTCTime -> CM () xGrpDirectInv g m mConn connReq mContent_ msg brokerTs = do - unless (groupFeatureAllowed SGFDirectMessages g) $ messageError "x.grp.direct.inv: direct messages not allowed" + unless (groupFeatureMemberAllowed SGFDirectMessages m g) $ messageError "x.grp.direct.inv: direct messages not allowed" let GroupMember {memberContactId} = m subMode <- chatReadVar subscriptionMode case memberContactId of @@ -6681,14 +6693,14 @@ createContactsFeatureItems user cts chatDir ciFeature ciOffer getPref = do cup = getContactUserPreference f cups cup' = getContactUserPreference f cups' -createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> CIContent d) -> GroupInfo -> GroupInfo -> CM () +createGroupFeatureChangedItems :: MsgDirectionI d => User -> ChatDirection 'CTGroup d -> (GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent d) -> GroupInfo -> GroupInfo -> CM () createGroupFeatureChangedItems user cd ciContent GroupInfo {fullGroupPreferences = gps} GroupInfo {fullGroupPreferences = gps'} = forM_ allGroupFeatures $ \(AGF f) -> do let state = groupFeatureState $ getGroupPreference f gps pref' = getGroupPreference f gps' - state'@(_, int') = groupFeatureState pref' + state'@(_, param', role') = groupFeatureState pref' when (state /= state') $ - createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') int') Nothing + createInternalChatItem user cd (ciContent (toGroupFeature f) (toGroupPreference pref') param' role') Nothing sameGroupProfileInfo :: GroupProfile -> GroupProfile -> Bool sameGroupProfileInfo p p' = p {groupPreferences = Nothing} == p' {groupPreferences = Nothing} @@ -7046,20 +7058,22 @@ chatCommandP = "/show profile image" $> ShowProfileImage, ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames), ("/profile" <|> "/p") $> ShowProfile, - "/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)), + "/set voice #" *> (SetGroupFeatureRole (AGFR SGFVoice) <$> displayName <*> _strP <*> optional memberRole), "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)), "/set voice " *> (SetUserFeature (ACF SCFVoice) <$> strP), - "/set files #" *> (SetGroupFeature (AGF SGFFiles) <$> displayName <*> (A.space *> strP)), - "/set history #" *> (SetGroupFeature (AGF SGFHistory) <$> displayName <*> (A.space *> strP)), + "/set files #" *> (SetGroupFeatureRole (AGFR SGFFiles) <$> displayName <*> _strP <*> optional memberRole), + "/set history #" *> (SetGroupFeature (AGFNR SGFHistory) <$> displayName <*> (A.space *> strP)), + "/set reactions #" *> (SetGroupFeature (AGFNR SGFReactions) <$> displayName <*> (A.space *> strP)), "/set calls @" *> (SetContactFeature (ACF SCFCalls) <$> displayName <*> optional (A.space *> strP)), "/set calls " *> (SetUserFeature (ACF SCFCalls) <$> strP), - "/set delete #" *> (SetGroupFeature (AGF SGFFullDelete) <$> displayName <*> (A.space *> strP)), + "/set delete #" *> (SetGroupFeature (AGFNR SGFFullDelete) <$> displayName <*> (A.space *> strP)), "/set delete @" *> (SetContactFeature (ACF SCFFullDelete) <$> displayName <*> optional (A.space *> strP)), "/set delete " *> (SetUserFeature (ACF SCFFullDelete) <$> strP), - "/set direct #" *> (SetGroupFeature (AGF SGFDirectMessages) <$> displayName <*> (A.space *> strP)), + "/set direct #" *> (SetGroupFeatureRole (AGFR SGFDirectMessages) <$> displayName <*> _strP <*> optional memberRole), "/set disappear #" *> (SetGroupTimedMessages <$> displayName <*> (A.space *> timedTTLOnOffP)), "/set disappear @" *> (SetContactTimedMessages <$> displayName <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), + "/set links #" *> (SetGroupFeatureRole (AGFR SGFSimplexLinks) <$> displayName <*> _strP <*> optional memberRole), ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, "/set device name " *> (SetLocalDeviceName <$> textP), "/list remote hosts" $> ListRemoteHosts, @@ -7147,7 +7161,7 @@ chatCommandP = let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) - { directMessages = Just DirectMessagesGroupPreference {enable = FEOn}, + { directMessages = Just DirectMessagesGroupPreference {enable = FEOn, role = Nothing}, history = Just HistoryGroupPreference {enable = FEOn} } pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 024757e7bb..85d93a7d88 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -62,6 +62,7 @@ import Simplex.Chat.Remote.Types import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) @@ -461,7 +462,8 @@ data ChatCommand | ShowProfileImage | SetUserFeature AChatFeature FeatureAllowed -- UserId (not used in UI) | SetContactFeature AChatFeature ContactName (Maybe FeatureAllowed) - | SetGroupFeature AGroupFeature GroupName GroupFeatureEnabled + | SetGroupFeature AGroupFeatureNoRole GroupName GroupFeatureEnabled + | SetGroupFeatureRole AGroupFeatureRole GroupName GroupFeatureEnabled (Maybe GroupMemberRole) | SetUserTimedMessages Bool -- UserId (not used in UI) | SetContactTimedMessages ContactName (Maybe TimedMessagesEnabled) | SetGroupTimedMessages GroupName (Maybe Int) diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 2eabb48166..d3b9ea52f1 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -144,6 +144,15 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2 parseMarkdown :: Text -> Markdown parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s +containsFormat :: (Format -> Bool) -> Markdown -> Bool +containsFormat p (Markdown f _) = maybe False p f +containsFormat p (m1 :|: m2) = containsFormat p m1 || containsFormat p m2 + +isSimplexLink :: Format -> Bool +isSimplexLink = \case + SimplexLink {} -> True; + _ -> False + markdownP :: Parser Markdown markdownP = mconcat <$> A.many' fragmentP where diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 0e95570b85..9266a0c1ca 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -28,6 +28,7 @@ import Simplex.Chat.Messages.CIContent.Events import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOn, pattern PQEncOff) @@ -134,8 +135,8 @@ data CIContent (d :: MsgDirection) where CISndChatFeature :: ChatFeature -> PrefEnabled -> Maybe Int -> CIContent 'MDSnd CIRcvChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDRcv CISndChatPreference :: ChatFeature -> FeatureAllowed -> Maybe Int -> CIContent 'MDSnd - CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDRcv - CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> CIContent 'MDSnd + CIRcvGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDRcv + CISndGroupFeature :: GroupFeature -> GroupPreference -> Maybe Int -> Maybe GroupMemberRole -> CIContent 'MDSnd CIRcvChatFeatureRejected :: ChatFeature -> CIContent 'MDRcv CIRcvGroupFeatureRejected :: GroupFeature -> CIContent 'MDRcv CISndModerated :: CIContent 'MDSnd @@ -255,8 +256,8 @@ ciContentToText = \case CISndChatFeature feature enabled param -> featureStateText feature enabled param CIRcvChatPreference feature allowed param -> prefStateText feature allowed param CISndChatPreference feature allowed param -> "you " <> prefStateText feature allowed param - CIRcvGroupFeature feature pref param -> groupPrefStateText feature pref param - CISndGroupFeature feature pref param -> groupPrefStateText feature pref param + CIRcvGroupFeature feature pref param role -> groupPrefStateText feature pref param role + CISndGroupFeature feature pref param role -> groupPrefStateText feature pref param role CIRcvChatFeatureRejected feature -> chatFeatureNameText feature <> ": received, prohibited" CIRcvGroupFeatureRejected feature -> groupFeatureNameText feature <> ": received, prohibited" CISndModerated -> ciModeratedText @@ -413,8 +414,8 @@ data JSONCIContent | JCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | JCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | JCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | JCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | JCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | JCIRcvChatFeatureRejected {feature :: ChatFeature} | JCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | JCISndModerated @@ -447,8 +448,8 @@ jsonCIContent = \case CISndChatFeature feature enabled param -> JCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> JCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> JCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> JCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> JCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> JCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> JCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> JCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> JCISndModerated @@ -481,8 +482,8 @@ aciContentJSON = \case JCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param JCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param JCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - JCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - JCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + JCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + JCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ JCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature JCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature JCISndModerated -> ACIContent SMDSnd CISndModerated @@ -516,8 +517,8 @@ data DBJSONCIContent | DBJCISndChatFeature {feature :: ChatFeature, enabled :: PrefEnabled, param :: Maybe Int} | DBJCIRcvChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} | DBJCISndChatPreference {feature :: ChatFeature, allowed :: FeatureAllowed, param :: Maybe Int} - | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} - | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int} + | DBJCIRcvGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} + | DBJCISndGroupFeature {groupFeature :: GroupFeature, preference :: GroupPreference, param :: Maybe Int, memberRole_ :: Maybe GroupMemberRole} | DBJCIRcvChatFeatureRejected {feature :: ChatFeature} | DBJCIRcvGroupFeatureRejected {groupFeature :: GroupFeature} | DBJCISndModerated @@ -550,8 +551,8 @@ dbJsonCIContent = \case CISndChatFeature feature enabled param -> DBJCISndChatFeature {feature, enabled, param} CIRcvChatPreference feature allowed param -> DBJCIRcvChatPreference {feature, allowed, param} CISndChatPreference feature allowed param -> DBJCISndChatPreference {feature, allowed, param} - CIRcvGroupFeature groupFeature preference param -> DBJCIRcvGroupFeature {groupFeature, preference, param} - CISndGroupFeature groupFeature preference param -> DBJCISndGroupFeature {groupFeature, preference, param} + CIRcvGroupFeature groupFeature preference param memberRole_ -> DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} + CISndGroupFeature groupFeature preference param memberRole_ -> DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} CIRcvChatFeatureRejected feature -> DBJCIRcvChatFeatureRejected {feature} CIRcvGroupFeatureRejected groupFeature -> DBJCIRcvGroupFeatureRejected {groupFeature} CISndModerated -> DBJCISndModerated @@ -584,8 +585,8 @@ aciContentDBJSON = \case DBJCISndChatFeature {feature, enabled, param} -> ACIContent SMDSnd $ CISndChatFeature feature enabled param DBJCIRcvChatPreference {feature, allowed, param} -> ACIContent SMDRcv $ CIRcvChatPreference feature allowed param DBJCISndChatPreference {feature, allowed, param} -> ACIContent SMDSnd $ CISndChatPreference feature allowed param - DBJCIRcvGroupFeature {groupFeature, preference, param} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param - DBJCISndGroupFeature {groupFeature, preference, param} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param + DBJCIRcvGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDRcv $ CIRcvGroupFeature groupFeature preference param memberRole_ + DBJCISndGroupFeature {groupFeature, preference, param, memberRole_} -> ACIContent SMDSnd $ CISndGroupFeature groupFeature preference param memberRole_ DBJCIRcvChatFeatureRejected {feature} -> ACIContent SMDRcv $ CIRcvChatFeatureRejected feature DBJCIRcvGroupFeatureRejected {groupFeature} -> ACIContent SMDRcv $ CIRcvGroupFeatureRejected groupFeature DBJCISndModerated -> ACIContent SMDSnd CISndModerated diff --git a/src/Simplex/Chat/Messages/CIContent/Events.hs b/src/Simplex/Chat/Messages/CIContent/Events.hs index 7ce5f73cde..74f7d94399 100644 --- a/src/Simplex/Chat/Messages/CIContent/Events.hs +++ b/src/Simplex/Chat/Messages/CIContent/Events.hs @@ -7,6 +7,7 @@ module Simplex.Chat.Messages.CIContent.Events where import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson.TH as J import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (RatchetSyncState (..), SwitchPhase (..)) import Simplex.Messaging.Parsers (dropPrefix, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Crypto.Ratchet (PQEncryption) diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 8727a592a7..e262de0e74 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -45,6 +45,7 @@ import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Call import Simplex.Chat.Types +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Compression (compress1, decompressBatch) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 832b928012..cd62f17f4c 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -124,6 +124,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Crypto.Random (ChaChaDRG) +import Data.Bifunctor (second) import Data.Either (rights) import Data.Int (Int64) import Data.List (partition, sortOn) @@ -139,6 +140,7 @@ import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -668,13 +670,13 @@ getGroupSummary db User {userId} groupId = do (userId, groupId, GSMemRemoved, GSMemLeft, GSMemUnknown, GSMemInvited) pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} -getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] +getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [(GroupMemberRole, FullGroupPreferences)] getContactGroupPreferences db User {userId} Contact {contactId} = do - map (mergeGroupPreferences . fromOnly) + map (second mergeGroupPreferences) <$> DB.query db [sql| - SELECT gp.preferences + SELECT m.member_role, gp.preferences FROM groups g JOIN group_profiles gp USING (group_profile_id) JOIN group_members m USING (group_id) diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 512c857b23..0e2445572c 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -81,6 +81,7 @@ import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e419f8c4cb..f7174a635b 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -30,7 +30,6 @@ import qualified Data.Aeson.TH as JQ import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString, pack, unpack) -import qualified Data.ByteString.Char8 as B import Data.Int (Int64) import Data.Maybe (isJust) import Data.Text (Text) @@ -45,6 +44,7 @@ import Database.SQLite.Simple.Internal (Field (..)) import Database.SQLite.Simple.Ok import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId) @@ -439,9 +439,13 @@ featureAllowed feature forWhom Contact {mergedPreferences} = let ContactUserPreference {enabled} = getContactUserPreference feature mergedPreferences in forWhom enabled -groupFeatureAllowed :: GroupFeatureI f => SGroupFeature f -> GroupInfo -> Bool +groupFeatureAllowed :: GroupFeatureNoRoleI f => SGroupFeature f -> GroupInfo -> Bool groupFeatureAllowed feature gInfo = groupFeatureAllowed' feature $ fullGroupPreferences gInfo +groupFeatureMemberAllowed :: GroupFeatureRoleI f => SGroupFeature f -> GroupMember -> GroupInfo -> Bool +groupFeatureMemberAllowed feature GroupMember {memberRole} = + groupFeatureMemberAllowed' feature memberRole . fullGroupPreferences + mergeUserChatPrefs :: User -> Contact -> FullPreferences mergeUserChatPrefs user ct = mergeUserChatPrefs' user (contactConnIncognito ct) (userPreferences ct) @@ -796,41 +800,6 @@ fromInvitedBy userCtId = \case IBContact ctId -> Just ctId IBUser -> Just userCtId -data GroupMemberRole - = GRObserver -- connects to all group members and receives all messages, can't send messages - | GRAuthor -- reserved, unused - | GRMember -- + can send messages to all group members - | GRAdmin -- + add/remove members, change member role (excl. Owners) - | GROwner -- + delete and change group information, add/remove/change roles for Owners - deriving (Eq, Show, Ord) - -instance FromField GroupMemberRole where fromField = fromBlobField_ strDecode - -instance ToField GroupMemberRole where toField = toField . strEncode - -instance StrEncoding GroupMemberRole where - strEncode = \case - GROwner -> "owner" - GRAdmin -> "admin" - GRMember -> "member" - GRAuthor -> "author" - GRObserver -> "observer" - strDecode = \case - "owner" -> Right GROwner - "admin" -> Right GRAdmin - "member" -> Right GRMember - "author" -> Right GRAuthor - "observer" -> Right GRObserver - r -> Left $ "bad GroupMemberRole " <> B.unpack r - strP = strDecode <$?> A.takeByteString - -instance FromJSON GroupMemberRole where - parseJSON = strParseJSON "GroupMemberRole" - -instance ToJSON GroupMemberRole where - toJSON = strToJSON - toEncoding = strToJEncoding - data GroupMemberSettings = GroupMemberSettings { showMessages :: Bool } diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 2286ae8f40..4cf9f862d2 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -10,6 +10,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilyDependencies #-} @@ -31,6 +32,7 @@ import qualified Data.Text as T import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import GHC.Records.Compat +import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) @@ -148,6 +150,7 @@ data GroupFeature | GFReactions | GFVoice | GFFiles + | GFSimplexLinks | GFHistory deriving (Show) @@ -158,14 +161,23 @@ data SGroupFeature (f :: GroupFeature) where SGFReactions :: SGroupFeature 'GFReactions SGFVoice :: SGroupFeature 'GFVoice SGFFiles :: SGroupFeature 'GFFiles + SGFSimplexLinks :: SGroupFeature 'GFSimplexLinks SGFHistory :: SGroupFeature 'GFHistory deriving instance Show (SGroupFeature f) data AGroupFeature = forall f. GroupFeatureI f => AGF (SGroupFeature f) +data AGroupFeatureNoRole = forall f. GroupFeatureNoRoleI f => AGFNR (SGroupFeature f) + +data AGroupFeatureRole = forall f. GroupFeatureRoleI f => AGFR (SGroupFeature f) + deriving instance Show AGroupFeature +deriving instance Show AGroupFeatureNoRole + +deriving instance Show AGroupFeatureRole + groupFeatureNameText :: GroupFeature -> Text groupFeatureNameText = \case GFTimedMessages -> "Disappearing messages" @@ -174,15 +186,21 @@ groupFeatureNameText = \case GFReactions -> "Message reactions" GFVoice -> "Voice messages" GFFiles -> "Files and media" + GFSimplexLinks -> "SimpleX links" GFHistory -> "Recent history" groupFeatureNameText' :: SGroupFeature f -> Text groupFeatureNameText' = groupFeatureNameText . toGroupFeature -groupFeatureAllowed' :: GroupFeatureI f => SGroupFeature f -> FullGroupPreferences -> Bool +groupFeatureAllowed' :: GroupFeatureNoRoleI f => SGroupFeature f -> FullGroupPreferences -> Bool groupFeatureAllowed' feature prefs = getField @"enable" (getGroupPreference feature prefs) == FEOn +groupFeatureMemberAllowed' :: GroupFeatureRoleI f => SGroupFeature f -> GroupMemberRole -> FullGroupPreferences -> Bool +groupFeatureMemberAllowed' feature role prefs = + let pref = getGroupPreference feature prefs + in getField @"enable" pref == FEOn && maybe True (role >=) (getField @"role" pref) + allGroupFeatures :: [AGroupFeature] allGroupFeatures = [ AGF SGFTimedMessages, @@ -191,17 +209,19 @@ allGroupFeatures = AGF SGFReactions, AGF SGFVoice, AGF SGFFiles, + AGF SGFSimplexLinks, AGF SGFHistory ] groupPrefSel :: SGroupFeature f -> GroupPreferences -> Maybe (GroupFeaturePreference f) -groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, history} = case f of +groupPrefSel f GroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, history} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete SGFReactions -> reactions SGFVoice -> voice SGFFiles -> files + SGFSimplexLinks -> simplexLinks SGFHistory -> history toGroupFeature :: SGroupFeature f -> GroupFeature @@ -212,6 +232,7 @@ toGroupFeature = \case SGFReactions -> GFReactions SGFVoice -> GFVoice SGFFiles -> GFFiles + SGFSimplexLinks -> GFSimplexLinks SGFHistory -> GFHistory class GroupPreferenceI p where @@ -224,13 +245,14 @@ instance GroupPreferenceI (Maybe GroupPreferences) where getGroupPreference pt prefs = fromMaybe (getGroupPreference pt defaultGroupPrefs) (groupPrefSel pt =<< prefs) instance GroupPreferenceI FullGroupPreferences where - getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, history} = case f of + getGroupPreference f FullGroupPreferences {timedMessages, directMessages, fullDelete, reactions, voice, files, simplexLinks, history} = case f of SGFTimedMessages -> timedMessages SGFDirectMessages -> directMessages SGFFullDelete -> fullDelete SGFReactions -> reactions SGFVoice -> voice SGFFiles -> files + SGFSimplexLinks -> simplexLinks SGFHistory -> history {-# INLINE getGroupPreference #-} @@ -242,17 +264,25 @@ data GroupPreferences = GroupPreferences reactions :: Maybe ReactionsGroupPreference, voice :: Maybe VoiceGroupPreference, files :: Maybe FilesGroupPreference, + simplexLinks :: Maybe SimplexLinksGroupPreference, history :: Maybe HistoryGroupPreference } deriving (Eq, Show) -setGroupPreference :: forall f. GroupFeatureI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences +setGroupPreference :: forall f. GroupFeatureNoRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupPreferences -> GroupPreferences setGroupPreference f enable prefs_ = setGroupPreference_ f pref prefs where prefs = mergeGroupPreferences prefs_ pref :: GroupFeaturePreference f pref = setField @"enable" (getGroupPreference f prefs) enable +setGroupPreferenceRole :: forall f. GroupFeatureRoleI f => SGroupFeature f -> GroupFeatureEnabled -> Maybe GroupMemberRole -> Maybe GroupPreferences -> GroupPreferences +setGroupPreferenceRole f enable role prefs_ = setGroupPreference_ f pref prefs + where + prefs = mergeGroupPreferences prefs_ + pref :: GroupFeaturePreference f + pref = setField @"role" (setField @"enable" (getGroupPreference f prefs) enable) role + setGroupPreference' :: SGroupFeature f -> GroupFeaturePreference f -> Maybe GroupPreferences -> GroupPreferences setGroupPreference' f pref prefs_ = setGroupPreference_ f pref prefs where @@ -267,6 +297,7 @@ setGroupPreference_ f pref prefs = SGFReactions -> prefs {reactions = pref} SGFVoice -> prefs {voice = pref} SGFFiles -> prefs {files = pref} + SGFSimplexLinks -> prefs {simplexLinks = pref} SGFHistory -> prefs {history = pref} setGroupTimedMessagesPreference :: TimedMessagesGroupPreference -> Maybe GroupPreferences -> GroupPreferences @@ -295,6 +326,7 @@ data FullGroupPreferences = FullGroupPreferences reactions :: ReactionsGroupPreference, voice :: VoiceGroupPreference, files :: FilesGroupPreference, + simplexLinks :: SimplexLinksGroupPreference, history :: HistoryGroupPreference } deriving (Eq, Show) @@ -346,16 +378,17 @@ defaultGroupPrefs :: FullGroupPreferences defaultGroupPrefs = FullGroupPreferences { timedMessages = TimedMessagesGroupPreference {enable = FEOff, ttl = Just 86400}, - directMessages = DirectMessagesGroupPreference {enable = FEOff}, + directMessages = DirectMessagesGroupPreference {enable = FEOff, role = Nothing}, fullDelete = FullDeleteGroupPreference {enable = FEOff}, reactions = ReactionsGroupPreference {enable = FEOn}, - voice = VoiceGroupPreference {enable = FEOn}, - files = FilesGroupPreference {enable = FEOn}, + voice = VoiceGroupPreference {enable = FEOn, role = Nothing}, + files = FilesGroupPreference {enable = FEOn, role = Nothing}, + simplexLinks = SimplexLinksGroupPreference {enable = FEOn, role = Nothing}, history = HistoryGroupPreference {enable = FEOff} } emptyGroupPrefs :: GroupPreferences -emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing +emptyGroupPrefs = GroupPreferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing data TimedMessagesPreference = TimedMessagesPreference { allow :: FeatureAllowed, @@ -431,7 +464,7 @@ data TimedMessagesGroupPreference = TimedMessagesGroupPreference deriving (Eq, Show) data DirectMessagesGroupPreference = DirectMessagesGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FullDeleteGroupPreference = FullDeleteGroupPreference @@ -443,11 +476,15 @@ data ReactionsGroupPreference = ReactionsGroupPreference deriving (Eq, Show) data VoiceGroupPreference = VoiceGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data FilesGroupPreference = FilesGroupPreference - {enable :: GroupFeatureEnabled} + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} + deriving (Eq, Show) + +data SimplexLinksGroupPreference = SimplexLinksGroupPreference + {enable :: GroupFeatureEnabled, role :: Maybe GroupMemberRole} deriving (Eq, Show) data HistoryGroupPreference = HistoryGroupPreference @@ -458,6 +495,11 @@ class (Eq (GroupFeaturePreference f), HasField "enable" (GroupFeaturePreference type GroupFeaturePreference (f :: GroupFeature) = p | p -> f sGroupFeature :: SGroupFeature f groupPrefParam :: GroupFeaturePreference f -> Maybe Int + groupPrefRole :: GroupFeaturePreference f -> Maybe GroupMemberRole + +class GroupFeatureI f => GroupFeatureNoRoleI f + +class (GroupFeatureI f, HasField "role" (GroupFeaturePreference f) (Maybe GroupMemberRole)) => GroupFeatureRoleI f instance HasField "enable" GroupPreference GroupFeatureEnabled where hasField p@GroupPreference {enable} = (\e -> p {enable = e}, enable) @@ -480,6 +522,9 @@ instance HasField "enable" VoiceGroupPreference GroupFeatureEnabled where instance HasField "enable" FilesGroupPreference GroupFeatureEnabled where hasField p@FilesGroupPreference {enable} = (\e -> p {enable = e}, enable) +instance HasField "enable" SimplexLinksGroupPreference GroupFeatureEnabled where + hasField p@SimplexLinksGroupPreference {enable} = (\e -> p {enable = e}, enable) + instance HasField "enable" HistoryGroupPreference GroupFeatureEnabled where hasField p@HistoryGroupPreference {enable} = (\e -> p {enable = e}, enable) @@ -487,42 +532,84 @@ instance GroupFeatureI 'GFTimedMessages where type GroupFeaturePreference 'GFTimedMessages = TimedMessagesGroupPreference sGroupFeature = SGFTimedMessages groupPrefParam TimedMessagesGroupPreference {ttl} = ttl + groupPrefRole _ = Nothing instance GroupFeatureI 'GFDirectMessages where type GroupFeaturePreference 'GFDirectMessages = DirectMessagesGroupPreference sGroupFeature = SGFDirectMessages groupPrefParam _ = Nothing + groupPrefRole DirectMessagesGroupPreference {role} = role instance GroupFeatureI 'GFFullDelete where type GroupFeaturePreference 'GFFullDelete = FullDeleteGroupPreference sGroupFeature = SGFFullDelete groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFReactions where type GroupFeaturePreference 'GFReactions = ReactionsGroupPreference sGroupFeature = SGFReactions groupPrefParam _ = Nothing + groupPrefRole _ = Nothing instance GroupFeatureI 'GFVoice where type GroupFeaturePreference 'GFVoice = VoiceGroupPreference sGroupFeature = SGFVoice groupPrefParam _ = Nothing + groupPrefRole VoiceGroupPreference {role} = role instance GroupFeatureI 'GFFiles where type GroupFeaturePreference 'GFFiles = FilesGroupPreference sGroupFeature = SGFFiles groupPrefParam _ = Nothing + groupPrefRole FilesGroupPreference {role} = role + +instance GroupFeatureI 'GFSimplexLinks where + type GroupFeaturePreference 'GFSimplexLinks = SimplexLinksGroupPreference + sGroupFeature = SGFSimplexLinks + groupPrefParam _ = Nothing + groupPrefRole SimplexLinksGroupPreference {role} = role instance GroupFeatureI 'GFHistory where type GroupFeaturePreference 'GFHistory = HistoryGroupPreference sGroupFeature = SGFHistory groupPrefParam _ = Nothing + groupPrefRole _ = Nothing -groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Text -groupPrefStateText feature pref param = +instance GroupFeatureNoRoleI 'GFTimedMessages + +instance GroupFeatureNoRoleI 'GFFullDelete + +instance GroupFeatureNoRoleI 'GFReactions + +instance GroupFeatureNoRoleI 'GFHistory + +instance HasField "role" DirectMessagesGroupPreference (Maybe GroupMemberRole) where + hasField p@DirectMessagesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" VoiceGroupPreference (Maybe GroupMemberRole) where + hasField p@VoiceGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" FilesGroupPreference (Maybe GroupMemberRole) where + hasField p@FilesGroupPreference {role} = (\r -> p {role = r}, role) + +instance HasField "role" SimplexLinksGroupPreference (Maybe GroupMemberRole) where + hasField p@SimplexLinksGroupPreference {role} = (\r -> p {role = r}, role) + +instance GroupFeatureRoleI 'GFDirectMessages + +instance GroupFeatureRoleI 'GFVoice + +instance GroupFeatureRoleI 'GFFiles + +instance GroupFeatureRoleI 'GFSimplexLinks + +groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature -> p -> Maybe Int -> Maybe GroupMemberRole -> Text +groupPrefStateText feature pref param role = let enabled = getField @"enable" pref paramText = if enabled == FEOn then groupParamText_ feature param else "" - in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText + roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role + in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText groupParamText_ :: GroupFeature -> Maybe Int -> Text groupParamText_ feature param = case feature of @@ -532,7 +619,7 @@ groupParamText_ feature param = case feature of groupPreferenceText :: forall f. GroupFeatureI f => GroupFeaturePreference f -> Text groupPreferenceText pref = let feature = toGroupFeature $ sGroupFeature @f - in groupPrefStateText feature pref $ groupPrefParam pref + in groupPrefStateText feature pref (groupPrefParam pref) (groupPrefRole pref) timedTTLText :: Int -> Text timedTTLText 0 = "0 sec" @@ -602,7 +689,7 @@ instance StrEncoding GroupFeatureEnabled where "on" -> Right FEOn "off" -> Right FEOff r -> Left $ "bad GroupFeatureEnabled " <> B.unpack r - strP = strDecode <$?> A.takeByteString + strP = strDecode <$?> A.takeTill (== ' ') instance FromJSON GroupFeatureEnabled where parseJSON = strParseJSON "GroupFeatureEnabled" @@ -611,11 +698,13 @@ instance ToJSON GroupFeatureEnabled where toJSON = strToJSON toEncoding = strToJEncoding -groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int) +groupFeatureState :: GroupFeatureI f => GroupFeaturePreference f -> (GroupFeatureEnabled, Maybe Int, Maybe GroupMemberRole) groupFeatureState p = let enable = getField @"enable" p - param = if enable == FEOn then groupPrefParam p else Nothing - in (enable, param) + (param, role) + | enable == FEOn = (groupPrefParam p, groupPrefRole p) + | otherwise = (Nothing, Nothing) + in (enable, param, role) mergePreferences :: Maybe Preferences -> Maybe Preferences -> FullPreferences mergePreferences contactPrefs userPreferences = @@ -641,6 +730,7 @@ mergeGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, history = pref SGFHistory } where @@ -656,6 +746,7 @@ toGroupPreferences groupPreferences = reactions = pref SGFReactions, voice = pref SGFVoice, files = pref SGFFiles, + simplexLinks = pref SGFSimplexLinks, history = pref SGFHistory } where @@ -762,6 +853,8 @@ $(J.deriveJSON defaultJSON ''VoiceGroupPreference) $(J.deriveJSON defaultJSON ''FilesGroupPreference) +$(J.deriveJSON defaultJSON ''SimplexLinksGroupPreference) + $(J.deriveJSON defaultJSON ''HistoryGroupPreference) $(J.deriveJSON defaultJSON ''GroupPreferences) diff --git a/src/Simplex/Chat/Types/Shared.hs b/src/Simplex/Chat/Types/Shared.hs new file mode 100644 index 0000000000..f44457160f --- /dev/null +++ b/src/Simplex/Chat/Types/Shared.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Types.Shared where + +import Data.Aeson (FromJSON (..), ToJSON (..)) +import qualified Data.Attoparsec.ByteString.Char8 as A +import qualified Data.ByteString.Char8 as B +import Database.SQLite.Simple.FromField (FromField (..)) +import Database.SQLite.Simple.ToField (ToField (..)) +import Simplex.Chat.Types.Util +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util ((<$?>)) + +data GroupMemberRole + = GRObserver -- connects to all group members and receives all messages, can't send messages + | GRAuthor -- reserved, unused + | GRMember -- + can send messages to all group members + | GRAdmin -- + add/remove members, change member role (excl. Owners) + | GROwner -- + delete and change group information, add/remove/change roles for Owners + deriving (Eq, Show, Ord) + +instance FromField GroupMemberRole where fromField = fromBlobField_ strDecode + +instance ToField GroupMemberRole where toField = toField . strEncode + +instance StrEncoding GroupMemberRole where + strEncode = \case + GROwner -> "owner" + GRAdmin -> "admin" + GRMember -> "member" + GRAuthor -> "author" + GRObserver -> "observer" + strDecode = \case + "owner" -> Right GROwner + "admin" -> Right GRAdmin + "member" -> Right GRMember + "author" -> Right GRAuthor + "observer" -> Right GRObserver + r -> Left $ "bad GroupMemberRole " <> B.unpack r + strP = strDecode <$?> A.takeByteString + +instance FromJSON GroupMemberRole where + parseJSON = strParseJSON "GroupMemberRole" + +instance ToJSON GroupMemberRole where + toJSON = strToJSON + toEncoding = strToJEncoding diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 550fe97b18..55e0078d0d 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -49,6 +49,7 @@ import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (.. import Simplex.Chat.Styled import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import qualified Simplex.FileTransfer.Transport as XFTPTransport import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs index b78d36f489..fbabccfb54 100644 --- a/tests/Bots/DirectoryTests.hs +++ b/tests/Bots/DirectoryTests.hs @@ -19,7 +19,8 @@ import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Core import Simplex.Chat.Options (CoreChatOpts (..)) -import Simplex.Chat.Types (GroupMemberRole (..), Profile (..)) +import Simplex.Chat.Types (Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import System.FilePath (()) import Test.Hspec hiding (it) diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 77bac11145..a7a646c17b 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -15,7 +15,8 @@ import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) -import Simplex.Chat.Types (GroupMemberRole (..), VersionRangeChat) +import Simplex.Chat.Types (VersionRangeChat) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Crypto.Ratchet (pattern PQSupportOff) import System.Directory (copyFile) @@ -1509,6 +1510,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile alice <## "Message reactions: on" alice <## "Voice messages: on" alice <## "Files and media: on" + alice <## "SimpleX links: on" alice <## "Recent history: on" bobAddedDan :: HasCallStack => TestCC -> IO () bobAddedDan cc = do diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 8a9191c988..a6cc491456 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -13,7 +13,8 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Char8 as B import qualified Data.Text as T import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), GroupMemberRole (..), Profile (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..)) +import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Messaging.Encoding.String (StrEncoding (..)) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -68,6 +69,10 @@ chatProfileTests = do it "enable timed messages in group" testEnableTimedMessagesGroup xit'' "timed messages enabled globally, contact turns on" testTimedMessagesEnabledGlobally it "update multiple user preferences for multiple contacts" testUpdateMultipleUserPrefs + describe "group preferences for specific member role" $ do + it "direct messages" testGroupPrefsDirectForRole + it "files & media" testGroupPrefsFilesForRole + it "SimpleX links" testGroupPrefsSimplexLinksForRole testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile = @@ -1903,3 +1908,122 @@ testUpdateMultipleUserPrefs = testChat3 aliceProfile bobProfile cathProfile $ alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi bob"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) alice #$> ("/_get chat @3 count=100", chat, chatFeatures <> [(1, "hi cath"), (1, "Full deletion: enabled for contact"), (1, "Message reactions: off")]) + +testGroupPrefsDirectForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set direct #team on owner" + alice <## "updated group preferences:" + alice <## "Direct messages: on for owners" + directForOwners bob + directForOwners cath + threadDelay 1000000 + bob ##> "@cath hello again" + bob <## "bad chat command: direct messages not allowed" + (cath "/j #team" + concurrentlyN_ + [ cath <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member alice (Alice) is connected", + "#team: member bob (Bob) is connected" + ], + do + alice <## "#team: cath added dan (Daniel) to the group (connecting...)" + alice <## "#team: new member dan is connected", + do + bob <## "#team: cath added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected" + ] + -- dan cannot send direct messages to alice (owner) + dan ##> "@alice hello alice" + dan <## "bad chat command: direct messages not allowed" + (alice hello dan" + dan <## "alice (Alice): contact is connected" + -- and now dan can too + dan #> "@alice hi alice" + alice <# "dan> hi alice" + where + directForOwners :: HasCallStack => TestCC -> IO () + directForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Direct messages: on for owners" + +testGroupPrefsFilesForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsFilesForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + alice #$> ("/_files_folder ./tests/tmp/alice", id, "ok") + bob #$> ("/_files_folder ./tests/tmp/bob", id, "ok") + createDirectoryIfMissing True "./tests/tmp/alice" + createDirectoryIfMissing True "./tests/tmp/bob" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/alice/test1.txt" + copyFile "./tests/fixtures/test.txt" "./tests/tmp/bob/test2.txt" + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set files #team on owner" + alice <## "updated group preferences:" + alice <## "Files and media: on for owners" + filesForOwners bob + filesForOwners cath + threadDelay 1000000 + bob ##> "/f #team test2.txt" + bob <## "bad chat command: feature not allowed Files and media" + (alice "/f #team test1.txt" + alice <## "use /fc 1 to cancel sending" + alice <## "completed uploading file 1 (test1.txt) for #team" + bob <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + cath <# "#team alice> sends file test1.txt (11 bytes / 11 bytes)" + cath <## "use /fr 1 [/ | ] to receive it" + where + filesForOwners :: HasCallStack => TestCC -> IO () + filesForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "Files and media: on for owners" + +testGroupPrefsSimplexLinksForRole :: HasCallStack => FilePath -> IO () +testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> withXFTPServer $ do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set links #team on owner" + alice <## "updated group preferences:" + alice <## "SimpleX links: on for owners" + linksForOwners bob + linksForOwners cath + threadDelay 1000000 + bob ##> "/c" + inv <- getInvitation bob + bob ##> ("#team " <> inv) + bob <## "bad chat command: feature not allowed SimpleX links" + (alice ("#team " <> inv) + bob <# ("#team alice> " <> inv) + cath <# ("#team alice> " <> inv) + where + linksForOwners :: HasCallStack => TestCC -> IO () + linksForOwners cc = do + cc <## "alice updated group #team:" + cc <## "updated group preferences:" + cc <## "SimpleX links: on for owners" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 3b0748e7d0..98227fcd0c 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -30,6 +30,7 @@ import Simplex.Chat.Store.NoteFolders (createNoteFolder) import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.FileTransfer.Client.Main (xftpClientCLI) import Simplex.Messaging.Agent.Store.SQLite (maybeFirstRow, withTransaction) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -315,6 +316,7 @@ groupFeatures'' = ((0, "Message reactions: on"), Nothing, Nothing), ((0, "Voice messages: on"), Nothing, Nothing), ((0, "Files and media: on"), Nothing, Nothing), + ((0, "SimpleX links: on"), Nothing, Nothing), ((0, "Recent history: on"), Nothing, Nothing) ] diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 082af825e5..18fb677be2 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -12,6 +12,7 @@ import Data.Time.Clock.System (SystemTime (..), systemToUTCTime) import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Shared import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.Ratchet @@ -99,7 +100,7 @@ testChatPreferences :: Maybe Preferences testChatPreferences = Just Preferences {voice = Just VoicePreference {allow = FAYes}, fullDelete = Nothing, timedMessages = Nothing, calls = Nothing, reactions = Just ReactionsPreference {allow = FAYes}} testGroupPreferences :: Maybe GroupPreferences -testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn}, files = Nothing, fullDelete = Nothing, history = Nothing} +testGroupPreferences = Just GroupPreferences {timedMessages = Nothing, directMessages = Nothing, reactions = Just ReactionsGroupPreference {enable = FEOn}, voice = Just VoiceGroupPreference {enable = FEOn, role = Nothing}, files = Nothing, fullDelete = Nothing, simplexLinks = Nothing, history = Nothing} testProfile :: Profile testProfile = Profile {displayName = "alice", fullName = "Alice", image = Just (ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII="), contactLink = Nothing, preferences = testChatPreferences}