diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a86d4790a8..f73bfa7927 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -147,6 +147,12 @@ jobs: with: swap-size-gb: 30 + - name: Get UID and GID + id: ids + run: | + echo "uid=$(id -u)" >> $GITHUB_OUTPUT + echo "gid=$(id -g)" >> $GITHUB_OUTPUT + # Otherwise we run out of disk space with Docker build - name: Free disk space if: matrix.should_run == true @@ -177,6 +183,8 @@ jobs: build-args: | TAG=${{ matrix.os }} GHC=${{ matrix.ghc }} + USER_UID=${{ steps.ids.outputs.uid }} + USER_GID=${{ steps.ids.outputs.gid }} # Docker needs these flags for AppImage build: # --device /dev/fuse @@ -209,7 +217,6 @@ jobs: if: matrix.should_run == true shell: docker exec -t builder sh -eu {0} run: | - chmod -R 777 dist-newstyle ~/.cabal && git config --global --add safe.directory '*' cabal clean cabal update cabal build -j --enable-tests diff --git a/Dockerfile.build b/Dockerfile.build index fddc96b6c2..3ddff59d12 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -13,6 +13,10 @@ ARG JAVA_HASH_ARM64=2b460859b681757b33a7591b6238ecaf51569d05d2684984e5f0a89c6514 ENV TZ=Etc/UTC \ DEBIAN_FRONTEND=noninteractive +ARG USER_UID=1000 +ARG USER_GID=1000 +ARG USER_NAME=builder + # Install curl, git and and simplex-chat dependencies RUN apt-get update && \ apt-get install -y curl \ @@ -38,6 +42,11 @@ RUN apt-get update && \ file \ appstream \ gpg \ + zipalign \ + apksigner \ + python3 \ + python3-venv \ + xz-utils \ unzip &&\ ln -s /bin/fusermount /bin/fusermount3 || : @@ -67,6 +76,12 @@ RUN export JAVA_FILENAME='java-corretto.deb' \ echo "Checksum mismatch" && exit 1; \ fi +RUN userdel -r ubuntu || : +RUN groupadd -g ${USER_GID} ${USER_NAME} || :; useradd -u ${USER_UID} -g ${USER_GID} --create-home --shell /bin/bash ${USER_NAME} || : +RUN mkdir /nix /out && chown ${USER_NAME}:${USER_NAME} /nix /out +USER ${USER_NAME} +WORKDIR /home/${USER_NAME} + # Specify bootstrap Haskell versions ENV BOOTSTRAP_HASKELL_GHC_VERSION=${GHC} ENV BOOTSTRAP_HASKELL_CABAL_VERSION=${CABAL} @@ -78,8 +93,10 @@ ENV BOOTSTRAP_HASKELL_INSTALL_NO_STACK_HOOK=true # Install ghcup RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh +# Setup basic env variables (required) +ENV HOME="/home/${USER_NAME}" USER="${USER_NAME}" # Adjust PATH -ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH" +ENV PATH="$HOME/.cabal/bin:$HOME/.ghcup/bin:$PATH" # Set both as default RUN ghcup set ghc "${GHC}" && \ @@ -90,8 +107,8 @@ RUN ghcup set ghc "${GHC}" && \ #===================== ARG SDK_VERSION=13114758 -ENV SDK_VERSION=$SDK_VERSION \ - ANDROID_HOME=/root +ENV SDK_VERSION="$SDK_VERSION" \ + ANDROID_HOME="$HOME" RUN curl -L -o tools.zip "https://dl.google.com/android/repository/commandlinetools-linux-${SDK_VERSION}_latest.zip" && \ unzip tools.zip && rm tools.zip && \ @@ -101,11 +118,17 @@ RUN curl -L -o tools.zip "https://dl.google.com/android/repository/commandlineto ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin" # https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded -RUN mkdir -p ~/.android ~/.gradle && \ - touch ~/.android/repositories.cfg && \ - echo 'org.gradle.console=plain' > ~/.gradle/gradle.properties &&\ +RUN mkdir -p "$HOME/.android" "$HOME/.gradle" && \ + touch "$HOME/.android/repositories.cfg" && \ + echo 'org.gradle.console=plain' > "$HOME/.gradle/gradle.properties" &&\ yes | sdkmanager --licenses >/dev/null -ENV PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools +ENV PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/build-tools" + +# Android reproducibility scripts +RUN python3 -m venv "$HOME/.venv" +RUN "$HOME/.venv/bin/pip" install apksigcopier repro-apk + +ENV PATH="$HOME/.venv/bin:$PATH" WORKDIR /project diff --git a/README.md b/README.md index b1d2556072..3364c28284 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) - 🖲 Protects your messages and metadata - who you talk to and when. - 🔐 Double ratchet end-to-end encryption, with additional encryption layer. -- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 📱 Mobile apps for Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) with the new features 1-2 weeks earlier - **limited to 10,000 users**! - 🖥 Available as a terminal (console) [app / CLI](#zap-quick-installation-of-a-terminal-app) on Linux, MacOS, Windows. @@ -145,9 +145,9 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission). - BTC: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u -- XMR: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- XMR: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c - BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg -- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a +- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) - ZEC: t1fwjQW5gpFhDqXNhxqDWyF9j9WeKvVS5Jg - ZEC shielded: u16rnvkflumf5uw9frngc2lymvmzgdr2mmc9unyu0l44unwfmdcpfm0axujd2w34ct3ye709azxsqge45705lpvvqu264ltzvfay55ygyq - DOGE: D99pV4n9TrPxBPCkQGx4w4SMSa6QjRBxPf @@ -435,4 +435,4 @@ Graphic designs, artworks and layouts are not licensed for re-use. If you want t   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 7abab33177..3047572236 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -10,6 +10,7 @@ import SwiftUI struct ChatHelp: View { @EnvironmentObject var chatModel: ChatModel + @State private var showNewChatSheet = false let dismissSettingsSheet: DismissAction var body: some View { @@ -38,7 +39,7 @@ struct ChatHelp: View { HStack(spacing: 8) { Text("Tap button ") - NewChatMenuButton() + NewChatMenuButton(showNewChatSheet: $showNewChatSheet) Text("above, then choose:") } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 0450bd439c..efaba518a9 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -140,6 +140,7 @@ struct ChatListView: View { @StateObject private var connectProgressManager = ConnectProgressManager.shared @EnvironmentObject var theme: AppTheme @Binding var activeUserPickerSheet: UserPickerSheet? + @State private var showNewChatSheet = false @State private var searchMode = false @FocusState private var searchFocussed @State private var searchText = "" @@ -189,6 +190,10 @@ struct ChatListView: View { onDismiss: { chatModel.laRequest = nil }, content: { UserPickerSheetView(sheet: $0) } ) + .appSheet(isPresented: $showNewChatSheet) { + NewChatSheet() + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } .onChange(of: activeUserPickerSheet) { if $0 != nil { DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { @@ -331,7 +336,7 @@ struct ChatListView: View { @ViewBuilder var trailingToolbarItem: some View { switch chatModel.chatRunning { - case .some(true): NewChatMenuButton() + case .some(true): NewChatMenuButton(showNewChatSheet: $showNewChatSheet) case .some(false): chatStoppedIcon() case .none: EmptyView() } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 2e3119a8b8..7adb04cb7e 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -12,7 +12,7 @@ import SimpleXChat struct NewChatMenuButton: View { // do not use chatModel here because it prevents showing AddGroupMembersView after group creation and QR code after link creation on iOS 16 // @EnvironmentObject var chatModel: ChatModel - @State private var showNewChatSheet = false + @Binding var showNewChatSheet: Bool @State private var alert: SomeAlert? = nil var body: some View { @@ -25,10 +25,6 @@ struct NewChatMenuButton: View { .scaledToFit() .frame(width: 24, height: 24) } - .appSheet(isPresented: $showNewChatSheet) { - NewChatSheet() - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) - } .alert(item: $alert) { a in return a.alert } @@ -471,5 +467,5 @@ struct DeletedChats: View { } #Preview { - NewChatMenuButton() + NewChatMenuButton(showNewChatSheet: Binding.constant(false)) } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 2d08d1159b..fa9a4efdf7 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -178,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -545,8 +545,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -708,8 +708,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -795,8 +795,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */, ); path = Libraries; sourceTree = ""; @@ -2003,7 +2003,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2053,7 +2053,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -2095,7 +2095,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2115,7 +2115,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; @@ -2140,7 +2140,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -2177,7 +2177,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -2214,7 +2214,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2265,7 +2265,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -2316,7 +2316,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; @@ -2350,7 +2350,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 315; + CURRENT_PROJECT_VERSION = 318; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 60ff535e88..1e7bda37c4 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -40,6 +40,7 @@ compose { } mainClass = "chat.simplex.desktop.MainKt" nativeDistributions { + copyright = "(c) 2020-2026 SimpleX Chat" // For debugging via VisualVM if (debugJava) { modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent") diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 1501cb43dc..38ef04daaa 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -24,13 +24,13 @@ android.nonTransitiveRClass=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=6.5-beta.2 -android.version_code=330 +android.version_name=6.5-beta.3 +android.version_code=331 android.bundle=false -desktop.version_name=6.5-beta.2 -desktop.version_code=127 +desktop.version_name=6.5-beta.3 +desktop.version_code=128 kotlin.version=2.1.20 gradle.plugin.version=8.7.0 diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index b78b446821..b5f7220724 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -351,11 +351,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " STRecent -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, pageSize) @@ -363,11 +363,11 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> orderBy) (GRSActive, gId) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ?") (GRSActive, gId) pure (gs, n) where countQuery' = countQuery <> " WHERE r.group_reg_status = ? " - orderBy = " ORDER BY r.created_at DESC " + orderBy = " ORDER BY r.created_at DESC, r.group_reg_id ASC " STSearch search -> case lastGroup_ of Nothing -> do gs <- groups $ DB.query db (listedGroupQuery <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, s, s, s, s, pageSize) @@ -375,12 +375,12 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa pure (gs, n) Just gId -> do gs <- groups $ DB.query db (listedGroupQuery <> " AND r.group_id > ? " <> searchCond <> orderBy <> " LIMIT ?") (userId, userContactId, GRSActive, gId, s, s, s, s, pageSize) - n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond <> orderBy) (GRSActive, gId, s, s, s, s) + n <- count $ DB.query db (countQuery' <> " AND r.group_id > ? " <> searchCond) (GRSActive, gId, s, s, s, s) pure (gs, n) where s = T.toLower search countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " - orderBy = " ORDER BY g.summary_current_members_count DESC " + orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " where groups = (map (toGroupInfoReg (vr cc) user) <$>) count = maybeFirstRow' 0 fromOnly diff --git a/blog/20220808-simplex-chat-v3.1-chat-groups.md b/blog/20220808-simplex-chat-v3.1-chat-groups.md index cce22393fb..7a93cecbc8 100644 --- a/blog/20220808-simplex-chat-v3.1-chat-groups.md +++ b/blog/20220808-simplex-chat-v3.1-chat-groups.md @@ -113,7 +113,7 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission. -- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c Thank you, diff --git a/blog/20220901-simplex-chat-v3.2-incognito-mode.md b/blog/20220901-simplex-chat-v3.2-incognito-mode.md index 73df29c8d7..ff6ef5bc30 100644 --- a/blog/20220901-simplex-chat-v3.2-incognito-mode.md +++ b/blog/20220901-simplex-chat-v3.2-incognito-mode.md @@ -99,7 +99,7 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission. -- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt +- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c Thank you, diff --git a/blog/20220928-simplex-chat-v4-encrypted-database.md b/blog/20220928-simplex-chat-v4-encrypted-database.md index 6f8064454f..b1ae048a51 100644 --- a/blog/20220928-simplex-chat-v4-encrypted-database.md +++ b/blog/20220928-simplex-chat-v4-encrypted-database.md @@ -132,8 +132,8 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat): it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat): it also accepts donations in crypto-currencies, but charges a commission. -- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Bitcoin wallet: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u Thank you, diff --git a/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md b/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md index 51dba8818c..e77a625b8f 100644 --- a/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md +++ b/blog/20221108-simplex-chat-v4.2-security-audit-new-website.md @@ -177,8 +177,8 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies. -- Monero wallet: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin wallet: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- Monero wallet: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Bitcoin wallet: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u - please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list. Thank you, diff --git a/blog/20221206-simplex-chat-v4.3-voice-messages.md b/blog/20221206-simplex-chat-v4.3-voice-messages.md index 07a6e227f0..a5af09d374 100644 --- a/blog/20221206-simplex-chat-v4.3-voice-messages.md +++ b/blog/20221206-simplex-chat-v4.3-voice-messages.md @@ -127,9 +127,9 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in many crypto-currencies. -- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 +- Monero address: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Bitcoin address: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- Ethereum address: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) - please let us know, via GitHub issue or chat, if you want to make a donation in some other cryptocurrency - we will add the address to the list. Thank you, diff --git a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md index ab9010535f..b9b84c7ed9 100644 --- a/blog/20230103-simplex-chat-v4.4-disappearing-messages.md +++ b/blog/20230103-simplex-chat-v4.4-disappearing-messages.md @@ -91,9 +91,10 @@ It is possible to donate via: - [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us. - [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies. -- Monero address: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt - Bitcoin address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- BCH address: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- Ethereum address: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 +- Monero address: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Bitcoin address: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- BCH address: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- Ethereum address: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) - please let us know, via GitHub issue or chat, if you want to create a donation in some other cryptocurrency - we will add the address to the list. Thank you, diff --git a/bots/README.md b/bots/README.md index 9449e9d847..80c6689dce 100644 --- a/bots/README.md +++ b/bots/README.md @@ -192,8 +192,16 @@ It is usually simpler to run your bot process on the same machine where you run If you have to run your bot on another machine, you need to secure access to bot CLI via any web proxy that supports WebSockets, e.g. Caddy or Nginx. You must configure TLS termination in the proxy and connect CLI process from bot via a secure TLS connection. If you connect to bot via a public network, you also must configure HTTP basic auth to prevent unauthorized access. You can validate TLS security of your proxy via a free test at [SSLLabs.com](https://www.ssllabs.com/ssltest/). You can also configure firewall on the machine where you run SimpleX CLI to only allow connections from the IP address of your bot. +## Available libraries + +#### Libraries with full bot API support + +- [The official TypeScript SDK](https://www.npmjs.com/package/simplex-chat) +- [Unofficial Rust SDK](https://crates.io/crates/simploxide-client) + ## Useful bots - [Broadcast bot](../apps/simplex-broadcast-bot/) (Haskell) - we use it to send [status and release updates](https://status.simplex.chat/status/public). - [Moderation bot](https://github.com/NCalex42/simplex-bot) (Java) - [Matterbridge bot](https://github.com/UnkwUsr/matterbridge-simplex) (JavaScript) +- [Nodify](https://nodify.ie) (Low-Code) \ No newline at end of file diff --git a/cabal.project b/cabal.project index 1f331bdf74..e5d7464ece 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: 2ca440dd2dfd494ff2bb40cc0409d08069d02e04 + tag: a7b43b1a3e204759d4b7ad60928fa897b1600654 source-repository-package type: git diff --git a/docs/DONATIONS.md b/docs/DONATIONS.md index 276a57ed8a..50eb62a401 100644 --- a/docs/DONATIONS.md +++ b/docs/DONATIONS.md @@ -17,7 +17,7 @@ Please donate via: - [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission) - BTC: [bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u](bitcoin:bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u) -- XMR: [8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt](monero:8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt) +- XMR: [8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c](monero:8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c) - ETH/USDT (Ethereum, Arbitrum One): [0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a](ethereum:0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a) ([donate.simplexchat.eth](ethereum:0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a)) - [Other cryptocurrencies](https://github.com/simplex-chat/simplex-chat#please-support-us-with-your-donations) diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index a23a920bbb..b9889fe7a7 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -24,6 +24,8 @@ You can link your mobile device with desktop to use the same profile remotely, b - Ubuntu 22.04 and Debian-based distros ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-aarch64.deb)). - Ubuntu 24.04 ([x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-24_04-x86_64.deb), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-24_04-aarch64.deb)). +You can [verify and reproduce](./REPRODUCE.md) Linux builds. + **Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). **Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-windows-x86_64.msi). @@ -32,7 +34,9 @@ You can link your mobile device with desktop to use the same profile remotely, b **iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084), [TestFlight](https://testflight.apple.com/join/DWuT2LQu). -**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk). +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-armv7a.apk). + +You can [verify and reproduce](./REPRODUCE.md) Android APKs. ## Terminal (console) app diff --git a/docs/REPRODUCE.md b/docs/REPRODUCE.md new file mode 100644 index 0000000000..03fb6b4336 --- /dev/null +++ b/docs/REPRODUCE.md @@ -0,0 +1,203 @@ +--- +title: Verify and reproduce builds +permalink: /reproduce/index.html +revision: 19.12.2025 +--- + +# Verifying and reproducing release builds + +- [Obtain release signing key](#obtain-release-signing-key) +- [Verify release signature](#verify-release-signature) +- [How to reproduce builds](#how-to-reproduce-builds) + - [Server binaries](#server-binaries) + - [Linux desktop apps and CLI](#linux-desktop-apps-and-cli) + - [Android apps](#android-apps) + +## Obtain release signing key + +To verify the signature of `_sha256sums` or apks you need to obtain the signing key. You can do it from keyservers: + +```sh +gpg --keyserver hkps://keys.openpgp.org --search build@simplex.chat +gpg --keyserver hkps://keyserver.ubuntu.com --search build@simplex.chat +``` + +```sh +gpg --list-keys build@simplex.chat +``` + +Once you obtain the signing key, verify that its fingerprint is: + +``` +BBDF 7BDA D154 8B16 836A F5B9 D53B DFD1 53C3 66BA +``` + +Additionally, compare the key fingerprint with: + +- [simplexchat.eth](https://app.ens.domains/simplexchat.eth) (release key record) +- [Mastodon](https://mastodon.social/@simplex) (profile) +- [Reddit](https://www.reddit.com/r/SimpleXChat/) (side panel) + +You can set the imported key as "ultimately trusted": + +```sh +echo -e "trust\n5\ny\nquit" | gpg --command-fd 0 --edit-key build@simplex.chat +``` + +## Verify release signature + +**Linux dekstop apps and CLI**: + +Download the file with executable hashes and the signature. For example, to verify the `v6.5.0-beta.3` release: + +```sh +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_sha256sums.asc' +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_sha256sums' +``` + +Verify the signature: + +```sh +gpg --verify _sha256sums.asc _sha256sums +``` + +**Android APKs**: + +Download the APK files and signatures. For example, to verify the `v6.5.0-beta.3` release: + +```sh +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/simplex-aarch64.apk' +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_simplex-aarch64.apk.asc' +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/simplex-armv7a.apk' +curl -LO 'https://github.com/simplex-chat/simplex-chat/releases/download/v6.5.0-beta.3/_simplex-armv7a.apk.asc' +``` + +Verify the signatures: + +```sh +gpg --verify _simplex-armv7a.apk.asc simplex-armv7a.apk +gpg --verify _simplex-aarch64.apk.asc simplex-aarch64.apk +``` + +## How to reproduce builds + +To reproduce the build you must have: + +- Linux machine +- `x86-64` architecture +- Installed `docker`, `curl` and `git` + +### Server binaries + +1. Download script: + + ```sh + curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplexmq/refs/heads/master/scripts/simplexmq-reproduce-builds.sh' + ``` + +2. Make it executable: + + ```sh + chmod +x simplexmq-reproduce-builds.sh + ``` + +3. Execute the script with the required tag: + + ```sh + ./simplexmq-reproduce-builds.sh 'v6.3.1' + ``` + + The script executes these steps (please review the script to confirm): + + 1) builds all server binaries for the release in docker container. + 2) downloads binaries from the same GitHub release and compares them with the built binaries. + 3) if they all match, generates _sha256sums file with their checksums. + + This will take a while. + +4. After compilation, you should see the folder named as the tag and repository name (e.g., `v6.3.1-simplexmq`) with two subfolders: + + ```sh + ls v6.3.1-simplexmq + ``` + + ```sh + from-source prebuilt _sha256sums + ``` + + The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release. + +### Linux desktop apps and CLI + +1. Download script: + + ```sh + curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplex-chat/refs/heads/master/scripts/simplex-chat-reproduce-builds.sh' + ``` + +2. Make it executable: + + ```sh + chmod +x simplex-chat-reproduce-builds.sh + ``` + +3. Execute the script with the required tag: + + ```sh + ./simplex-chat-reproduce-builds.sh 'v6.4.8' + ``` + + The script executes these steps (please review the script to confirm): + + 1) builds all Linux CLI and Dekstop binaries for the release in docker container. + 2) downloads binaries from the same GitHub release and compares them with the built binaries. + 3) if they all match, generates _sha256sums file with their checksums. + + This will take a while. + +4. After compilation, you should see the folder named as the tag and reprository name (e.g., `v6.4.8-simplex-chat`) with two subfolders: + + ```sh + ls v6.4.8-simplex-chat + ``` + + ```sh + from-source prebuilt _sha256sums + ``` + + The file _sha256sums contains the hashes of all builds - you can compare it with the same file in GitHub release. + +### Android apps + +In addition to basic requirments, Android build will: + +- Take ~150gb of disc space +- Take ~20h to build all the architectures (depends on core count) +- Require at least 16gb of RAM + +1. Download script: + + ```sh + curl -LO 'https://raw.githubusercontent.com/simplex-chat/simplex-chat/refs/heads/master/scripts/simplex-chat-reproduce-builds-android.sh' + ``` + +2. Make it executable: + + ```sh + chmod +x simplex-chat-reproduce-builds-android.sh + ``` + +3. Execute the script with the required tag: + + ```sh + ./simplex-chat-reproduce-builds-android.sh 'v6.5.0-beta.3' + ``` + + The script executes these steps (please review the script to confirm): + + 1) Downloads and checks that APKs from GitHub are signed with valid key. + 2) Builds Android APKs in a docker container. + 3) Compares the releases by copying the signature from downloaded APKs to locally built APKs. + 4) If the resulting build is bit-by-bit identical, prints the message that this tag was reproduced. + + This will take a while. diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md index d5c1cdef0b..2b1febb6f2 100644 --- a/docs/TRANSLATIONS.md +++ b/docs/TRANSLATIONS.md @@ -1,5 +1,5 @@ --- -title: Contributing translations to SimpleX Chat +title: Contributing SimpleX app translations revision: 19.03.2023 --- diff --git a/docs/WEBRTC.md b/docs/WEBRTC.md index 8ce31bf959..a48cd12b00 100644 --- a/docs/WEBRTC.md +++ b/docs/WEBRTC.md @@ -1,5 +1,5 @@ --- -title: Using custom WebRTC ICE servers in SimpleX Chat +title: Using custom WebRTC ICE servers revision: 31.01.2023 --- @@ -155,4 +155,3 @@ This is it - you now can make audio and video calls via your own server, without If results show `srflx` and `relay` candidates, everything is set up correctly! - diff --git a/docs/lang/cs/README.md b/docs/lang/cs/README.md index 7eab61395e..5ba6a87803 100644 --- a/docs/lang/cs/README.md +++ b/docs/lang/cs/README.md @@ -18,11 +18,11 @@   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) - 🖲 Chrání vaše zprávy a metadata - s kým a kdy mluvíte. - 🔐 Koncové šifrování s další vrstvou šifrování. -- 📱 Mobilní aplikace pro Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) a [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 📱 Mobilní aplikace pro Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) a [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**! - 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows. @@ -277,11 +277,11 @@ Přispět je možné prostřednictvím: - [GitHub](https://github.com/sponsors/simplex-chat) - je to pro nás bez provize. - OpenCollective](https://opencollective.com/simplex-chat) - účtuje si provizi a přijímá také dary v kryptoměnách. -- Adresa Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt. -- Bitcoinová adresa: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- BCH adresa: BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- Ethereum adresa: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 -- Adresa Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L +- Adresa Monero: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c. +- Bitcoinová adresa: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- BCH adresa: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- ETH/USDT (Ethereum, Arbitrum One) adresa: 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) +- Adresa Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu - dejte nám prosím vědět prostřednictvím GitHub issue nebo chatu, pokud chcete vytvořit příspěvek v nějaké jiné kryptoměně - přidáme adresu do seznamu. Děkujeme, @@ -324,4 +324,4 @@ Jakákoli zjištění možných útoků korelace provozu umožňujících korelo   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) diff --git a/docs/lang/fr/README.md b/docs/lang/fr/README.md index 69401da5a2..bc06e9e228 100644 --- a/docs/lang/fr/README.md +++ b/docs/lang/fr/README.md @@ -32,11 +32,11 @@   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) - 🖲 Protégez vos messages et vos métadonnées - avec qui vous parlez et quand. - 🔐 Chiffrement de bout en bout à double ratchet, avec couche de chiffrement supplémentaire. -- 📱 Apps mobiles pour Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) et [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 📱 Apps mobiles pour Android ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) et [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 🚀 [Bêta TestFlight pour iOS](https://testflight.apple.com/join/DWuT2LQu) avec les nouvelles fonctionnalités 1 à 2 semaines plus tôt - **limitée à 10 000 utilisateurs** ! - 🖥 Disponible en tant que [terminal (console) / CLI](#⚡-installation-rapide-dune-application-pour-terminal) sur Linux, MacOS, Windows. @@ -112,11 +112,11 @@ Il est possible de faire un don via : - [GitHub](https://github.com/sponsors/simplex-chat) - il n'y a pas de commission à payer. - [OpenCollective](https://opencollective.com/simplex-chat) - ils prélèvent une commission et acceptent également les dons en crypto-monnaies. -- Adresse Monero : 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Adresse Bitcoin : 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- Adresse BCH : 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- Adresse Ethereum : 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 -- Adresse Solana : 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L +- Adresse Monero : 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Adresse Bitcoin : bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- Adresse BCH : bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- Adresse ETH/USDT (Ethereum, Arbitrum One) : 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) +- Adresse Solana : 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu Nous vous remercions, @@ -351,4 +351,4 @@ Veuillez traiter toute découverte d'une éventuelle attaque par corrélation de   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) diff --git a/docs/lang/pl/README.md b/docs/lang/pl/README.md index 23ca00c3e6..fc94863658 100644 --- a/docs/lang/pl/README.md +++ b/docs/lang/pl/README.md @@ -32,11 +32,11 @@   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) - 🖲 Chroni Twoje wiadomości i metadane - z kim rozmawiasz i kiedy. - 🔐 Szyfrowanie end-to-end double ratchet, z dodatkową warstwą szyfrowania. -- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). +- 📱 Aplikacje mobilne dla Androida ([Google Play](https://play.google.com/store/apps/details?id=chat.simplex.app), [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)) oraz [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084). - 🚀 [TestFlight dla iOS](https://testflight.apple.com/join/DWuT2LQu) z nowymi funkcjami na tydzień-dwa wcześniej - **limitowane do 10,000 użytkowników**! - 🖥 Dostępny jako terminalowa (konsolowa) [aplikacja / CLI](#zap-quick-installation-of-a-terminal-app) na Linuxa, MacOSa, Windowsa. @@ -164,14 +164,11 @@ Możesz nas wesprzeć za pomocą: - [GitHuba](https://github.com/sponsors/simplex-chat) - jest to dla nas wolne od prowizji. - [OpenCollective](https://opencollective.com/simplex-chat) - pobiera prowizję, a także przyjmuje darowizny w kryptowalutach. -- Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- USDT: - - BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 - - Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW -- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 -- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L +- Monero: 8A3ZWAXrrQddvnT1fPrtbK86ZAoM4nai3Gjg1LEow3JWcryJtovMnHYZnxTJpCLmAbfWbnPMeTzPmMBjAhyd4xoM89hYq1c +- Bitcoin: bc1q2gy6f02nn6vvcxs0pnu29tpnpyz0qf66505d4u +- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- ETH/USDT (Ethereum, Arbitrum One): 0xD7047Fe3Eecb2f2FF78d839dD927Be27Bc12c86a (donate.simplexchat.eth) +- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu Dziękuję, @@ -198,9 +195,9 @@ Twórca SimpleX Chat. ## Dlaczego prywatność ma znaczenie -Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia. +Każdy powinien dbać o prywatność i bezpieczeństwo swojej komunikacji - nieszkodliwe rozmowy mogą narazić Cię na niebezpieczeństwo, nawet jeśli nie masz nic do ukrycia. -Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech. +Jedną z najbardziej wstrząsających historii jest doświadczenie [Mohamedou Ould Salahi](https://en.wikipedia.org/wiki/Mohamedou_Ould_Slahi). opisane w jego pamiętniku i pokazane w filmie Mauretańczyk (2021). Został on umieszczony w obozie Guantanamo, bez procesu, i był tam torturowany przez 15 lat po telefonie do swojego krewnego w Afganistanie, pod zarzutem udziału w atakach 9/11, mimo że przez poprzednie 10 lat mieszkał w Niemczech. Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinniśmy używać komunikatorów, które zapewniają prywatność naszym powiązaniom, czyli tym z kim jesteśmy jakkolwiek połączeni. @@ -432,4 +429,4 @@ Prosimy o traktowanie wszelkich ustaleń dotyczących możliwych ataków korelac   [iOS TestFlight](https://testflight.apple.com/join/DWuT2LQu)   -[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) +[APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk) diff --git a/docs/rfcs/2025-12-10-vouchers-2.md b/docs/rfcs/2025-12-10-vouchers-2.md new file mode 100644 index 0000000000..60e80215dd --- /dev/null +++ b/docs/rfcs/2025-12-10-vouchers-2.md @@ -0,0 +1,98 @@ +# Community Vouchers contract + +This document simplifies [the previous RFC](./2025-10-23-vouchers.md) in two ways: +- only one type of credits (previous design had conversion from community to operator credits). +- vouchers can support any amount and change. +- using a single fixed-size Merkle tree for vouchers. +- proposal for balancing privacy and the size of the tree that the client has to load. + +## Objectives + +- voucher expiration - fixed period per contract. +- make assignment of voucher to redeemer (a community) private, not allowing further transactions/re-assignments. +- vouchers support any amount. +- allow assigning and redeeming part of the voucher, with change. + +## Design proposal + +### General ideas + +- Voucher is issued by the smart contract in exchange for stable-coin deposit, and recorded as blinded commitment added to Merkle tree. +- Voucher is nominated in infrastructure credits with 18 decimals, as all ETH tokens. +- Voucher assignments and redemptions (partial or full) are done via zero-knowledge proof and recorded as nullifier to prevent double spends, with the new commitment for the remaining amount and transaction count (can be 0). +- Commitment includes: + - the amount. + - the whether the voucher is assigned. + - the expiration time. +- Merkle tree of commitments has a fixed depth of say 20-40 levels (for approx 10^6-10^12 voucher capacity). +- Assignments and redemptions should prove consistency between the old and new commitments, without revealing them, and include the nullifier to prevent "double spends". +- Redemptions also include the blockchain address and ID of the operator. +- Release of funds with revenue sharing can be done via call to another contract. + +### Data storage + +Smart contract: +- total deposit amounts bucketed by time ranges (to allow the release of funds to network after expiration). +- redemption amounts bucketed by time ranges with homomorphic encryption (to prevent reduction of anonymity set). +- nullifiers bucketed by creation time (to allow removing old nullifiers). +- the path in the Merkle tree on "the front" of the filled part of the tree. +- multiple last Merkle tree roots: + - to accept the proof after the root changes. + - to frustrate timing correlation by computing proofs in advance (for better usability). + +Transaction event log: +- Merkle tree of commitments are recorded in event log + +### Voucher lifecycle + +1. Issuance: blinded commitment recorded as described above in exchange for stablecoin deposit. + +The contract should be able to verify that: +- commitment amount is the same as deposit amount. +- expiration time is in 12 months from now (possibly rounded up to 1-3 months). +- not assigned to redeemer. + +Initially, the deposit UX could be dApp generating some token to be used in the app to assign voucher to the community via app (to preserve privacy). + +2. Computing zero-knowledge proof for assignment or redemption. + +To compute the proof the client needs to have a path from commitment to tree root. + +As the path to commitment changes when the tree gets filled, we need to find a balance between privacy and usability - to request as little as possible and have as large anonymity set as possible. + + +3. Assignment. + +Includes 2 new commitments (to destination and a "change") and nullifier are submitted. + +ZK proof should prove that: +- the old commitment expiration time is greater than transaction submission time (possibly has to be included in parameters to be part of the proof). +- the total amount of 2 new commitments is the same as the amount of nullified old commitment. +- the new commitments expiration time is the same as the old. +- the old commitment is not assigned to redeemer. +- the new commitments is assigned to redeemer. + +Questions: +1) how would the redeemer know that it received a voucher? Probably, community owners would be notified by chat relays. +2) do we want to conceal from the relays when and to which group donation was made? If yes, community owners can monitor blockchain themselves. +3) can we have view keys to delegate the detection of incoming transactions to relays? +4) should we round expiration time to a month or even 3 months? (to avoid exposing expiration time to the assignment recipient, as it also leaks purchase time) If so, we should also have overlapping rounding ranges to avoid leaking purchase time in case it is assigned straight after purchase, and straight after range change. + + +4. Redemption. + +Includes one new commitment (with the change) and public record of funds released to operator and network. + +The revenues received by the operators are publicly visible. + +ZK proof should prove that: +- the old commitment expiration time is greater than transaction submission time. +- the total amount of the new commitment and of released funds is the same as the amount of nullified commitment. +- the new commitment expiration time is the same as the old. +- the old commitment is assigned to the redeemer that redeems the voucher. +- the new commitment has the same assignment. + + +5. Releasing deposits for expired vouchers to the network. + +That would require tracking purchases in buckets (in the clear) and redemptions in homomorphicly encrypted structure, so an observer cannot correlate redemptions to purchases (as the same structure will change, and it won't be possible to establish which bucket). diff --git a/scripts/android/build-android.sh b/scripts/android/build-android.sh index 90d6092385..afd13011c9 100755 --- a/scripts/android/build-android.sh +++ b/scripts/android/build-android.sh @@ -102,6 +102,7 @@ build() { sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts" sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts" + sed -i.bak "s/android.version_code=.*/android.version_code=${vercode}/" "$folder/apps/multiplatform/gradle.properties" for arch in $arches; do if [ "$arch" = "armv7a" ]; then @@ -169,8 +170,10 @@ pre() { done shift $(( $OPTIND - 1 )) - - commit="${1:-HEAD}" + + vercode="${1}" + + commit="${2:-HEAD}" } main() { diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh index d9e46a015f..b1c9b6b5e1 100755 --- a/scripts/android/compress-and-sign-apk.sh +++ b/scripts/android/compress-and-sign-apk.sh @@ -41,7 +41,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do if [ $case_insensitive -eq 1 ]; then # For case-insensitive file systems - list_of_files=$(unzip -l "$ORIG_NAME_COPY" | grep res/ | sed -e "s|.*res/|res/|" | sort -z) + list_of_files=$(unzip -l "$ORIG_NAME_COPY" | grep res/ | sed -e "s|.*res/|res/|" | sort) for file in $list_of_files; do unzip -o -q -d apk "$ORIG_NAME_COPY" "$file" ( diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 61ede61996..16454f63d1 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."2ca440dd2dfd494ff2bb40cc0409d08069d02e04" = "1jc1a9vh59l0l5hxlin1spv03afrgmmiml5xnakhbi4rk67n0wwr"; + "https://github.com/simplex-chat/simplexmq.git"."a7b43b1a3e204759d4b7ad60928fa897b1600654" = "169vjn5gyw42cmak6kwyl27zm57il43khnlj40zjwjw7cldkzdzi"; "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/scripts/simplex-chat-reproduce-builds-android.sh b/scripts/simplex-chat-reproduce-builds-android.sh new file mode 100755 index 0000000000..0cef17c091 --- /dev/null +++ b/scripts/simplex-chat-reproduce-builds-android.sh @@ -0,0 +1,251 @@ +#!/usr/bin/env sh +set -eu + +SIMPLEX_KEY='3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85' + +REPO_NAME="simplex-chat" +REPO="https://github.com/simplex-chat/${REPO_NAME}" + +IMAGE_NAME='sx-local-android' +CONTAINER_NAME='sx-builder-android' +DOCKER_PATH_PROJECT='/project' +DOCKER_PATH_VERIFY='/verify' + +export DOCKER_BUILDKIT=1 + +SIMPLEX_REPO='simplex-chat/simplex-chat' +CMDS="curl git docker" + +INIT_DIR="$PWD" +TEMPDIR="$(mktemp -d)" + +ARCHES="${ARCHES:-aarch64 armv7a}" + +COLOR_CYAN="\033[36m" +COLOR_RESET="\033[0m" + +SUFFIX_BUILT='built' +SUFFIX_DOWNLOADED='downloaded' +SUFFIX_BUILT_WITH_SIGNATURE='built-with-downloaded-signature' + +cleanup() { + rm -rf -- "${TEMPDIR}" + docker rm --force "${CONTAINER_NAME}" 2>/dev/null || : + docker image rm "${IMAGE_NAME}" 2>/dev/null || : +} +trap 'cleanup' EXIT INT + +check() { + commands="$1" + + set +u + + for i in $commands; do + if ! command -v "$i" > /dev/null 2>&1; then + commands_failed="$i $commands_failed" + fi + done + + if [ -n "$commands_failed" ]; then + commands_failed=${commands_failed% *} + printf "%s is not found in your \$PATH. Please install them and re-run the script.\n" "$commands_failed" + exit 1 + fi + + set -u +} + +download_apk() { + tag="$1" + filename="$2" + file_out="$3" + + curl -L "${REPO}/releases/download/${tag}/${filename}" -o "$file_out" +} + +setup_git() { + workdir="$1" + name="$2" + + git -C "$workdir" clone "${REPO}.git" "$name" +} + +checkout_git() { + git_dir="$1" + tag="$2" + + git -C "$git_dir" reset --hard + git -C "$git_dir" clean -dfx + git -C "$git_dir" checkout "$tag" +} + +check_apk() { + apk_name="$1" + expected="$2" + + actual=$(docker exec "${CONTAINER_NAME}" apksigner verify --print-certs "${DOCKER_PATH_VERIFY}/${apk_name}" | grep 'SHA-256' | awk '{print $NF}' | fold -w2 | paste -sd: | tr '[:lower:]' '[:upper:]') + + if [ "$expected" = "$actual" ]; then + return 0 + else + return 1 + fi +} + +verify_apk() { + apk_name="$1" + + # https://github.com/obfusk/apksigcopier?tab=readme-ov-file#what-about-signatures-made-by-apksigner-from-build-tools--3500-rc1 + docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --pad-like-apksigner --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" + docker exec "${CONTAINER_NAME}" mv "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" + + docker exec "${CONTAINER_NAME}" apksigcopier copy "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_DOWNLOADED}" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \ + "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT_WITH_SIGNATURE}" + + downloaded_apk_hash=$(docker exec "${CONTAINER_NAME}" sha256sum "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_DOWNLOADED}" | awk '{print $1}') + built_apk_hash=$(docker exec "${CONTAINER_NAME}" sha256sum "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT_WITH_SIGNATURE}" | awk '{print $1}') + + if [ "$downloaded_apk_hash" = "$built_apk_hash" ]; then + return 0 + else + return 1 + fi +} + +print_vercode() { + build_dir="$1" + awk -F'=' '/android.version_code=/ {print $2}' "${build_dir}/apps/multiplatform/gradle.properties" +} + +setup_container() { + dir_git="$1" + dir_apk="$2" + + docker build \ + --no-cache \ + -f "${dir_git}/Dockerfile.build" \ + -t "${IMAGE_NAME}" \ + --build-arg=USER_UID="$(id -u)" \ + --build-arg=USER_GID="$(id -g)" \ + . + + # Run container in background + docker run -t -d \ + --name "${CONTAINER_NAME}" \ + --device /dev/fuse \ + --cap-add SYS_ADMIN \ + --security-opt apparmor:unconfined \ + --security-opt seccomp:unconfined \ + -v "${dir_git}:${DOCKER_PATH_PROJECT}" \ + -v "${dir_apk}:${DOCKER_PATH_VERIFY}" \ + "${IMAGE_NAME}" +} + +build_apk() { + arch="$1" + vercode="$2" + + apk_out="simplex-${arch}.apk.${SUFFIX_BUILT}" + + # Gradle setup + docker exec -i "${CONTAINER_NAME}" sh << EOF +cd $DOCKER_PATH_PROJECT/apps/multiplatform +./gradlew +EOF + + docker exec -i "${CONTAINER_NAME}" sh << EOF +GRADLE_BIN=\$(find \$HOME/.gradle/wrapper/dists -name "gradle" -type f -executable 2>/dev/null | head -1) +GRADLE_DIR=\$(dirname "\$GRADLE_BIN") +export PATH="\$GRADLE_DIR:\$PATH" + +ARCHES="$arch" ./scripts/android/build-android.sh -gs "$vercode" || ARCHES="$arch" ./scripts/android/build-android.sh -gs "$vercode" + +APK_FILE=\$(find . -maxdepth 1 -type f -name '*.apk') + +mv "\$APK_FILE" $DOCKER_PATH_VERIFY/$apk_out +EOF +} + +main() { + tag="$1" + + build_directory="${TEMPDIR}/${REPO_NAME}" + final_directory="$INIT_DIR/${tag}-${REPO_NAME}" + apk_directory="${final_directory}/android" + + printf 'This script will: +1) build docker container. +2) download APK from GitHub and validate signatures. +3) build core library with nix (12-24 hours). +4) build APK and compare with downloaded one + +Continue?' + + read _ + + check "$CMDS" + + mkdir -p "${apk_directory}" + + # Setup initial git for Dockerfile.build + setup_git "$TEMPDIR" "$REPO_NAME" + checkout_git "$build_directory" "$tag" + + printf "${COLOR_CYAN}Building Docker container...${COLOR_RESET}\n" + setup_container "$build_directory" "$apk_directory" + + # Check phase + for arch in $ARCHES; do + filename="simplex-${arch}.apk" + + download_apk "$tag" "$filename" "${apk_directory}/${filename}.${SUFFIX_DOWNLOADED}" + + if check_apk "${filename}.${SUFFIX_DOWNLOADED}" "$SIMPLEX_KEY"; then + printf "${COLOR_CYAN}APK for %s is signed by valid key.${COLOR_RESET}\n" "$arch" + else + printf "${COLOR_CYAN}Signature of APK for %s is invalid., aborting the script.${COLOR_RESET}\n" "$arch" + exit 1 + fi + done + + # Build phase + for arch in $ARCHES; do + case "$arch" in + armv7a) + build_tag="${tag}-armv7a" + ;; + aarch64) + build_tag="${tag}" + ;; + *) + printf "${COLOR_CYAN}Unknown architecture: %s! Skipping the build...${COLOR_RESET}\n" "$arch" + continue + esac + + # Setup the code + checkout_git "$build_directory" "$build_tag" + vercode=$(print_vercode "$build_directory") + + printf "${COLOR_CYAN}Building APK for for %s...${COLOR_RESET}\n" "$arch" + build_apk "$arch" "$vercode" + done + + # Verification phase + for arch in $ARCHES; do + filename="simplex-${arch}.apk" + + if ! verify_apk "$filename"; then + printf "${COLOR_CYAN}Failed to verify %s! Aborting.\n${COLOR_RESET}" "$filename" + exit 1 + fi + done + + printf "${COLOR_CYAN}%s is reproducible.${COLOR_RESET}\n" "$tag" + + cleanup +} + +main "$@" diff --git a/scripts/simplex-chat-reproduce-builds.sh b/scripts/simplex-chat-reproduce-builds.sh index e1a62dc73a..0ca2522fa0 100755 --- a/scripts/simplex-chat-reproduce-builds.sh +++ b/scripts/simplex-chat-reproduce-builds.sh @@ -50,6 +50,8 @@ for os in '22.04' '24.04'; do --no-cache \ --build-arg TAG="${os}" \ --build-arg GHC="${ghc}" \ + --build-arg=USER_UID="$(id -u)" \ + --build-arg=USER_GID="$(id -g)" \ -f "${tempdir}/${repo_name}/Dockerfile.build" \ -t "${image_name}" \ . diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 0b815c9fe5..e782d3c90f 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.5.0.5 +version: 6.5.0.7 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -123,7 +123,7 @@ library Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector - Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 + Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.Postgres.Migrations.M20251212_chat_relays else exposed-modules: @@ -271,7 +271,7 @@ library Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector - Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 + Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations Simplex.Chat.Store.SQLite.Migrations.M20251212_chat_relays other-modules: Paths_simplex_chat diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 94b6c90495..7074364a61 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -114,6 +114,7 @@ defaultChatConfig = deliveryWorkerDelay = 0, deliveryBucketSize = 10000, deviceNameForRemote = "", + remoteCompression = True, chatHooks = defaultChatHooks } diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index bca2131d86..8832f4e1ea 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -96,7 +96,12 @@ import Simplex.RemoteControl.Types import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM -#if !defined(dbPostgres) + +#if defined(dbPostgres) +import qualified Database.PostgreSQL.Simple as PSQL + +type SQLError = PSQL.SqlError +#else import Database.SQLite.Simple (SQLError) import qualified Database.SQLite.Simple as SQL import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) @@ -159,6 +164,7 @@ data ChatConfig = ChatConfig deliveryBucketSize :: Int, highlyAvailable :: Bool, deviceNameForRemote :: Text, + remoteCompression :: Bool, chatHooks :: ChatHooks } @@ -759,7 +765,7 @@ data ChatResponse | CRRemoteFileStored {remoteHostId :: RemoteHostId, remoteFileSource :: CryptoFile} | CRRemoteCtrlList {remoteCtrls :: [RemoteCtrlInfo]} | CRRemoteCtrlConnecting {remoteCtrl_ :: Maybe RemoteCtrlInfo, ctrlAppInfo :: CtrlAppInfo, appVersion :: AppVersion} - | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo} + | CRRemoteCtrlConnected {remoteCtrl :: RemoteCtrlInfo, compression :: Bool} | CRSQLResult {rows :: [Text]} #if !defined(dbPostgres) | CRArchiveExported {archiveErrors :: [ArchiveError]} @@ -862,7 +868,7 @@ data ChatEvent | CEvtNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgAckInfo} | CEvtRemoteHostSessionCode {remoteHost_ :: Maybe RemoteHostInfo, sessionCode :: Text} | CEvtNewRemoteHost {remoteHost :: RemoteHostInfo} - | CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo} + | CEvtRemoteHostConnected {remoteHost :: RemoteHostInfo, compression :: Bool} | CEvtRemoteHostStopped {remoteHostId_ :: Maybe RemoteHostId, rhsState :: RemoteHostSessionState, rhStopReason :: RemoteHostStopReason} | CEvtRemoteCtrlFound {remoteCtrl :: RemoteCtrlInfo, ctrlAppInfo_ :: Maybe CtrlAppInfo, appVersion :: AppVersion, compatible :: Bool} | CEvtRemoteCtrlSessionCode {remoteCtrl_ :: Maybe RemoteCtrlInfo, sessionCode :: Text} @@ -902,7 +908,7 @@ allowRemoteEvent = \case CEvtChatSuspended -> False CEvtRemoteHostSessionCode {} -> False CEvtNewRemoteHost _ -> False - CEvtRemoteHostConnected _ -> False + CEvtRemoteHostConnected {} -> False CEvtRemoteHostStopped {} -> False CEvtRemoteCtrlFound {} -> False CEvtRemoteCtrlSessionCode {} -> False @@ -1403,7 +1409,8 @@ data RemoteCtrlSession | RCSessionConnecting { remoteCtrlId_ :: Maybe RemoteCtrlId, rcsClient :: RCCtrlClient, - rcsWaitSession :: Async () + rcsWaitSession :: Async (), + ctrlAppInfo :: CtrlAppInfo } | RCSessionPendingConfirmation { remoteCtrlId_ :: Maybe RemoteCtrlId, @@ -1412,7 +1419,8 @@ data RemoteCtrlSession tls :: TLS 'TClient, sessionCode :: Text, rcsWaitSession :: Async (), - rcsWaitConfirmation :: TMVar (Either RCErrorType (RCCtrlSession, RCCtrlPairing)) + rcsWaitConfirmation :: TMVar (Either RCErrorType (RCCtrlSession, RCCtrlPairing)), + ctrlAppInfo :: CtrlAppInfo } | RCSessionConnected { remoteCtrlId :: RemoteCtrlId, @@ -1420,7 +1428,8 @@ data RemoteCtrlSession tls :: TLS 'TClient, rcsSession :: RCCtrlSession, http2Server :: Async (), - remoteOutputQ :: TBQueue (Either ChatError ChatEvent) + remoteOutputQ :: TBQueue (Either ChatError ChatEvent), + ctrlAppInfo :: CtrlAppInfo } data RemoteCtrlSessionState @@ -1544,25 +1553,24 @@ withFastStore = withStorePriority True withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a withStorePriority priority action = do ChatController {chatStore} <- ask - liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catch` handleDBErrors withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a)) withStoreBatch actions = do ChatController {chatStore} <- ask - liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions + liftIO $ withTransaction chatStore $ mapM (`E.catch` handleDBErrors) . actions --- TODO [postgres] postgres specific error handling -handleDBErrors :: [E.Handler (Either ChatError a)] -handleDBErrors = -#if !defined(dbPostgres) - ( 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 - ) : +handleDBErrors :: E.SomeException -> IO (Either ChatError a) +handleDBErrors e = pure $ Left $ ChatErrorStore $ case E.fromException e of + Just (e' :: SQLError) -> +#if defined(dbPostgres) + SEDBException $ show e' +#else + let se = SQL.sqlError e' + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in (if busy then SEDBBusyError else SEDBException) $ show e' #endif - [ E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e - ] + Nothing -> SEDBException $ show e withStoreBatch' :: Traversable t => (DB.Connection -> t (IO a)) -> CM' (t (Either ChatError a)) withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 721e4db837..6bdfdd12b6 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -167,9 +167,6 @@ startChatController mainApp enableSndFiles = do runExceptT (syncConnections' users) >>= \case Left e -> liftIO $ putStrLn $ "Error synchronizing connections: " <> show e Right _ -> pure () - runExceptT migrateMemberRelations >>= \case - Left e -> liftIO $ putStrLn $ "Error migrating member relations: " <> show e - Right _ -> pure () restoreCalls s <- asks agentAsync readTVarIO s >>= maybe (start s users) (pure . fst) @@ -181,10 +178,6 @@ startChatController mainApp enableSndFiles = do (userDiff, connDiff) <- withAgent (\a -> syncConnections a aUserIds connIds) withFastStore' setConnectionsSyncTs toView $ CEvtConnectionsDiff (AgentUserId <$> userDiff) (AgentConnId <$> connDiff) - migrateMemberRelations = - when mainApp $ - whenM (withStore' hasMembersWithoutVector) $ - void $ forkIO runRelationsVectorMigration start s users = do a1 <- async agentSubscriber a2 <- @@ -269,7 +262,7 @@ stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd) atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd) disconnectAgentClient smpAgent - readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) + readTVarIO s >>= mapM_ (\(a1, a2) -> forkIO $ uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2) closeFiles sndFiles closeFiles rcvFiles atomically $ do @@ -1837,7 +1830,7 @@ processChatCommand vr nm = \case conn <- withFastStore $ \db -> getPendingContactConnection db userId connId let PendingContactConnection {pccConnStatus, connLinkInv} = conn case (pccConnStatus, connLinkInv) of - (ConnNew, Just _ссLink) -> do + (ConnNew, Just _ccLink) -> do newUser <- privateGetUser newUserId conn' <- recreateConn user conn newUser pure $ CRConnectionUserChanged user conn conn' newUser @@ -2995,7 +2988,7 @@ processChatCommand vr nm = \case ConfirmRemoteCtrl rcId -> withUser_ $ do (rc, ctrlAppInfo) <- confirmRemoteCtrl rcId pure CRRemoteCtrlConnecting {remoteCtrl_ = Just rc, ctrlAppInfo, appVersion = currentAppVersion} - VerifyRemoteCtrlSession sessId -> withUser_ $ CRRemoteCtrlConnected <$> verifyRemoteCtrlSession (execChatCommand Nothing) sessId + VerifyRemoteCtrlSession sessId -> withUser_ $ verifyRemoteCtrlSession (execChatCommand Nothing) sessId StopRemoteCtrl -> withUser_ $ stopRemoteCtrl >> ok_ ListRemoteCtrls -> withUser_ $ CRRemoteCtrlList <$> listRemoteCtrls DeleteRemoteCtrl rc -> withUser_ $ deleteRemoteCtrl rc >> ok_ @@ -4220,21 +4213,6 @@ agentSubscriber = do type AgentSubResult = Map ConnId (Either AgentErrorType (Maybe ClientServiceId)) -runRelationsVectorMigration :: CM () -runRelationsVectorMigration = do - liftIO $ threadDelay' 5000000 -- 5 seconds (initial delay) - migrateMembers - where - stepDelay = 1000000 -- 1 second - migrateMembers = flip catchAllErrors eToView $ do - lift waitChatStartedAndActivated - gmIds <- withStore' getGMsWithoutVectorIds - forM_ gmIds $ \gmId -> do - lift waitChatStartedAndActivated - withStore' (`migrateMemberRelationsVector'` gmId) `catchAllErrors` eToView - liftIO $ threadDelay' stepDelay - unless (null gmIds) migrateMembers - cleanupManager :: CM () cleanupManager = do interval <- asks (cleanupManagerInterval . config) diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 190021b121..1a70492afb 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1030,11 +1030,11 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol else XMsgNew $ MCSimple $ extMsgContent (MCText pendingReviewMessage) Nothing void $ sendDirectMemberMessage mConn msg groupId modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo - let rcpModMs = filter shouldIntroduce modMs - introduceMember vr user gInfo m rcpModMs (Just $ MSMember $ memberId' m) + let rcpModMs = filter shouldIntroduceToMod modMs + introduceMember user gInfo m rcpModMs (Just $ MSMember $ memberId' m) where - shouldIntroduce :: GroupMember -> Bool - shouldIntroduce mem = + shouldIntroduceToMod :: GroupMember -> Bool + shouldIntroduceToMod mem = memberCurrent mem && groupMemberId' mem /= groupMemberId' m && maxVersion (memberChatVRange mem) >= groupKnockingVersion @@ -1042,42 +1042,33 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToAll vr user gInfo m = do members <- withStore' $ \db -> getGroupMembers db vr user gInfo - vector_ <- withStore' (`getMemberRelationsVector_` m) - let recipients = filter (shouldIntroduce vector_) members - introduceMember vr user gInfo m recipients Nothing - where - shouldIntroduce :: Maybe ByteString -> GroupMember -> Bool - shouldIntroduce vector_ m' = - memberCurrent m' - && groupMemberId' m' /= groupMemberId' m - && maybe True (\v -> getRelation (indexInGroup m') v == MRNew) vector_ + vector <- withStore (`getMemberRelationsVector` m) + let recipients = filter (shouldIntroduce m vector) members + introduceMember user gInfo m recipients Nothing introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () introduceToRemaining vr user gInfo m = do members <- withStore' $ \db -> getGroupMembers db vr user gInfo - vector_ <- withStore' (`getMemberRelationsVector_` m) - recipients <- filterRecipients vector_ members - introduceMember vr user gInfo m recipients Nothing - where - filterRecipients :: Maybe ByteString -> [GroupMember] -> CM [GroupMember] - filterRecipients vector_ members = do - newRelation <- case vector_ of - Nothing -> do - introducedGMIds <- S.fromList <$> withStore' (`getIntroducedGroupMemberIds` m) - pure $ \m' -> groupMemberId' m' `S.notMember` introducedGMIds - Just vec -> pure $ \m' -> getRelation (indexInGroup m') vec == MRNew - pure $ filter (\m' -> groupMemberId' m' /= groupMemberId' m && memberCurrent m' && newRelation m') members + vector <- withStore (`getMemberRelationsVector` m) + let recipients = filter (shouldIntroduce m vector) members + introduceMember user gInfo m recipients Nothing -introduceMember :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM () -introduceMember _ _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active" -introduceMember vr user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do +shouldIntroduce :: GroupMember -> ByteString -> GroupMember -> Bool +shouldIntroduce m vec mem = + memberCurrent mem + && groupMemberId' mem /= groupMemberId' m + && getRelation (indexInGroup mem) vec == MRNew + +introduceMember :: User -> GroupInfo -> GroupMember -> [GroupMember] -> Maybe MsgScope -> CM () +introduceMember _ _ GroupMember {activeConn = Nothing} _ _ = throwChatError $ CEInternalError "member connection not active" +introduceMember user gInfo@GroupInfo {groupId} toMember@GroupMember {activeConn = Just conn} introduceToMembers msgScope = do void . sendGroupMessage' user gInfo introduceToMembers $ XGrpMemNew (memberInfo gInfo toMember) msgScope sendIntroductions introduceToMembers where sendIntroductions reMembers = do updateToMemberVector reMembers - reMembers' <- withStore' $ \db -> createIntrosOrUpdateVectors db vr reMembers toMember - shuffledReMembers <- liftIO $ shuffleMembers reMembers' + updateReMembersVectors reMembers + shuffledReMembers <- liftIO $ shuffleMembers reMembers if toMember `supportsVersion` batchSendVersion then do let events = map memberIntro shuffledReMembers @@ -1089,6 +1080,10 @@ introduceMember vr user gInfo@GroupInfo {groupId} toMember@GroupMember {activeCo updateToMemberVector reMembers = do let relations = map (\GroupMember {indexInGroup} -> (indexInGroup, (IDReferencedIntroduced, MRIntroduced))) reMembers withStore' $ \db -> setMemberVectorNewRelations db toMember relations + updateReMembersVectors :: [GroupMember] -> CM () + updateReMembersVectors reMembers = do + let GroupMember {indexInGroup} = toMember + withStore' $ \db -> setMembersVectorsNewRelation db reMembers indexInGroup IDSubjectIntroduced MRIntroduced memberIntro :: GroupMember -> ChatMsgEvent 'Json memberIntro reMember = let mInfo = memberInfo gInfo reMember @@ -2026,7 +2021,7 @@ sendGroupMessages_ _user gInfo@GroupInfo {groupId} recipientMembers events = do pendingReq SndMessage {msgId} = (groupMemberId, msgId) createPendingMsg :: DB.Connection -> (GroupMemberId, MessageId) -> IO (Either ChatError ()) createPendingMsg db (groupMemberId, msgId) = - createPendingGroupMessage db groupMemberId msgId Nothing $> Right () + createPendingGroupMessage db groupMemberId msgId $> Right () data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded @@ -2089,32 +2084,25 @@ readyMemberConn GroupMember {groupMemberId, activeConn = Just conn@Connection {c | otherwise = Nothing readyMemberConn GroupMember {activeConn = Nothing} = Nothing -sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe GroupMemberIntro -> CM () -> CM () -sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent intro_ postDeliver = do +sendGroupMemberMessage :: MsgEncodingI e => GroupInfo -> GroupMember -> ChatMsgEvent e -> CM () +sendGroupMemberMessage gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent = do msg <- createSndMessage chatMsgEvent (GroupId groupId) messageMember msg `catchAllErrors` eToView where messageMember :: SndMessage -> CM () messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case - MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver - MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver - MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId (introId <$> intro_) + MSASend conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId + MSASendBatched conn -> void $ deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId + MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId MSAForwarded -> pure () -- TODO ensure order - pending messages interleave with user input messages sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM () sendPendingGroupMessages user GroupMember {groupMemberId} conn = do - pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId - forM_ (L.nonEmpty pgms) $ \pgms' -> do - let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms' - void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs - lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs - lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms' - where - updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO () - updateIntro_ db tag introId_ = case (tag, introId_) of - (ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded - _ -> pure () + msgs <- withStore' $ \db -> getPendingGroupMessages db groupMemberId + forM_ (L.nonEmpty msgs) $ \msgs' -> do + void $ batchSendConnMessages user conn MsgFlags {notification = True} msgs' + lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs' saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage) saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index c1fc1e917f..625e879607 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2615,14 +2615,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = GCInviteeMember -> withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" - Right reMember -> do - intro_ <- withStore' $ \db -> getIntroduction db reMember m - update intro_ GMIntroInvReceived - sendGroupMemberMessage gInfo reMember (XGrpMemFwd (memberInfo gInfo m) introInv) intro_ $ - update intro_ GMIntroInvForwarded - where - update (Just GroupMemberIntro {introId}) status = withStore' $ \db -> updateIntroStatus db introId status - update Nothing _ = pure () + Right reMember -> sendGroupMemberMessage gInfo reMember $ XGrpMemFwd (memberInfo gInfo m) introInv _ -> messageError "x.grp.mem.inv can be only sent by invitee member" xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> CM () @@ -2718,8 +2711,6 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM () xGrpMemCon gInfo sendingMem memId = do refMem <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId - withStore' (`migrateMemberRelationsVector` sendingMem) - withStore' (`migrateMemberRelationsVector` refMem) -- Updating vectors in separate transactions to avoid deadlocks. withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected @@ -2783,7 +2774,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let GroupMember {memberId} = m memberName = Just $ memberShortenedName m event = XGrpMsgForward memberId memberName chatMsg brokerTs - sendGroupMemberMessage gInfo member event Nothing (pure ()) + sendGroupMemberMessage gInfo member event -- TODO [channels fwd] base on differentiation between groups and channels isUserGrpFwdRelay :: GroupInfo -> Bool @@ -3228,7 +3219,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do unless (null ms) $ deliver body ms where buildMemberList sender = do - vec <- withStore $ \db -> migrateGetMemberRelationsVector db sender + vec <- withStore (`getMemberRelationsVector` sender) -- this excludes the sender let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec case jobScope of diff --git a/src/Simplex/Chat/Options/Postgres.hs b/src/Simplex/Chat/Options/Postgres.hs index c74ae37750..ab7414566c 100644 --- a/src/Simplex/Chat/Options/Postgres.hs +++ b/src/Simplex/Chat/Options/Postgres.hs @@ -42,7 +42,7 @@ chatDbOptsP _appDir defaultDbName = do ( long "pool-size" <> metavar "DB_POOL_SIZE" <> help "Database connection pool size" - <> value 10 + <> value 1 <> showDefault ) dbCreateSchema <- @@ -84,7 +84,7 @@ mobileDbOpts schemaPrefix connstr = do ChatDbOpts { dbConnstr, dbSchemaPrefix, - dbPoolSize = 10, + dbPoolSize = 1, dbCreateSchema = True } diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 255af5f318..b89d8a11f0 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -165,7 +165,8 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do where mkCtrlAppInfo = do deviceName <- chatReadVar localDeviceName - pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName} + useCompression <- asks $ remoteCompression . config + pure CtrlAppInfo {appVersionRange = ctrlAppVersionRange, deviceName, compression = BoolDef useCompression} parseHostAppInfo :: RCHostHello -> ExceptT RemoteHostError IO HostAppInfo parseHostAppInfo RCHostHello {app = hostAppInfo} = do hostInfo@HostAppInfo {appVersion, encoding} <- @@ -213,7 +214,9 @@ startRemoteHost rh_ rcAddrPrefs_ port_ = do RHSessionConfirmed _ RHPendingSession {rchClient} -> Right ((), RHSessionConnected {rchClient, tls, rhClient, pollAction, storePath}) _ -> Left $ ChatErrorRemoteHost rhKey RHEBadState chatWriteVar currentRemoteHost $ Just remoteHostId -- this is required for commands to be passed to remote host - toView $ CEvtRemoteHostConnected rhi {sessionState = Just RHSConnected {sessionCode}} + let RemoteHostClient {encryption = RemoteCrypto {compression}} = rhClient + remoteHost = rhi {sessionState = Just RHSConnected {sessionCode}} :: RemoteHostInfo + toView $ CEvtRemoteHostConnected {remoteHost, compression} upsertRemoteHost :: RCHostPairing -> Maybe RemoteHostInfo -> Maybe RCCtrlAddress -> Text -> SessionSeq -> RemoteHostSessionState -> CM RemoteHostInfo upsertRemoteHost pairing'@RCHostPairing {knownHost = kh_} rhi_ rcAddr_ hostDeviceName sseq state = do KnownHostPairing {hostDhPubKey = hostDhPubKey'} <- maybe (throwError . ChatError $ CEInternalError "KnownHost is known after verification") pure kh_ @@ -459,7 +462,7 @@ startRemoteCtrlSession = do connectRemoteCtrl :: RCVerifiedInvitation -> SessionSeq -> CM (Maybe RemoteCtrlInfo, CtrlAppInfo) connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) sseq = handleCtrlError sseq RCSRConnectionFailed "connectRemoteCtrl" $ do - ctrlInfo@CtrlAppInfo {deviceName = ctrlDeviceName} <- parseCtrlAppInfo app + ctrlInfo <- parseCtrlAppInfo app v <- checkAppVersion ctrlInfo rc_ <- withStore' $ \db -> getRemoteCtrlByFingerprint db ca mapM_ (validateRemoteCtrl inv) rc_ @@ -469,23 +472,23 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) cmdOk <- newEmptyTMVarIO rcsWaitSession <- async $ do atomically $ takeTMVar cmdOk - handleCtrlError sseq RCSRConnectionFailed "waitForCtrlSession" $ waitForCtrlSession rc_ ctrlDeviceName rcsClient vars + handleCtrlError sseq RCSRConnectionFailed "waitForCtrlSession" $ waitForCtrlSession rc_ ctrlInfo rcsClient vars updateRemoteCtrlSession sseq $ \case - RCSessionStarting -> Right RCSessionConnecting {remoteCtrlId_ = remoteCtrlId' <$> rc_, rcsClient, rcsWaitSession} + RCSessionStarting -> Right RCSessionConnecting {remoteCtrlId_ = remoteCtrlId' <$> rc_, rcsClient, rcsWaitSession, ctrlAppInfo = ctrlInfo} _ -> Left $ ChatErrorRemoteCtrl RCEBadState atomically $ putTMVar cmdOk () pure ((`remoteCtrlInfo` Just RCSConnecting) <$> rc_, ctrlInfo) where validateRemoteCtrl RCInvitation {idkey} RemoteCtrl {ctrlPairing = RCCtrlPairing {idPubKey}} = unless (idkey == idPubKey) $ throwError $ ChatErrorRemoteCtrl $ RCEProtocolError $ PRERemoteControl RCEIdentity - waitForCtrlSession :: Maybe RemoteCtrl -> Text -> RCCtrlClient -> RCStepTMVar (ByteString, TLS 'TClient, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM () - waitForCtrlSession rc_ ctrlName rcsClient vars = do + waitForCtrlSession :: Maybe RemoteCtrl -> CtrlAppInfo -> RCCtrlClient -> RCStepTMVar (ByteString, TLS 'TClient, RCStepTMVar (RCCtrlSession, RCCtrlPairing)) -> CM () + waitForCtrlSession rc_ ctrlAppInfo@CtrlAppInfo {deviceName = ctrlName} rcsClient vars = do (uniq, tls, rcsWaitConfirmation) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars let sessionCode = verificationCode uniq updateRemoteCtrlSession sseq $ \case RCSessionConnecting {rcsWaitSession} -> let remoteCtrlId_ = remoteCtrlId' <$> rc_ - in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation} + in Right RCSessionPendingConfirmation {remoteCtrlId_, ctrlDeviceName = ctrlName, rcsClient, tls, sessionCode, rcsWaitSession, rcsWaitConfirmation, ctrlAppInfo} _ -> Left $ ChatErrorRemoteCtrl RCEBadState toView CEvtRemoteCtrlSessionCode {remoteCtrl_ = (`remoteCtrlInfo` Just RCSPendingConfirmation {sessionCode}) <$> rc_, sessionCode} checkAppVersion CtrlAppInfo {appVersionRange} = @@ -495,7 +498,8 @@ connectRemoteCtrl verifiedInv@(RCVerifiedInvitation inv@RCInvitation {ca, app}) getHostAppInfo appVersion = do hostDeviceName <- chatReadVar localDeviceName encryptFiles <- chatReadVar encryptLocalFiles - pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles} + useCompression <- asks $ remoteCompression . config + pure HostAppInfo {appVersion, deviceName = hostDeviceName, encoding = localEncoding, encryptFiles, compression = BoolDef useCompression} parseCtrlAppInfo :: JT.Value -> CM CtrlAppInfo parseCtrlAppInfo ctrlAppInfo = do @@ -514,7 +518,7 @@ handleRemoteCommand execCC encryption remoteOutputQ HTTP2Request {request, reqBo parseRequest :: ExceptT RemoteProtocolError IO (C.SbKeyNonce, GetChunk, RemoteCommand) parseRequest = do (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption request reqBody - (rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecode header) + (rfKN,getNext,) <$> liftEitherWith RPEInvalidJSON (J.eitherDecodeStrict header) replyError = reply . RRChatResponse . RRError processCommand :: User -> C.SbKeyNonce -> GetChunk -> RemoteCommand -> CM () processCommand user rfKN getNext = \case @@ -611,7 +615,7 @@ remoteCtrlInfo RemoteCtrl {remoteCtrlId, ctrlDeviceName} sessionState = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName, sessionState} -- | Take a look at emoji of tlsunique, commit pairing, and start session server -verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM RemoteCtrlInfo +verifyRemoteCtrlSession :: (ByteString -> Int -> CM' (Either ChatError ChatResponse)) -> Text -> CM ChatResponse verifyRemoteCtrlSession execCC sessCode' = do (sseq, client, ctrlName, sessionCode, vars) <- chatReadVar remoteCtrlSession >>= \case @@ -625,14 +629,15 @@ verifyRemoteCtrlSession execCC sessCode' = do (rcsSession@RCCtrlSession {tls, sessionKeys}, rcCtrlPairing) <- timeoutThrow (ChatErrorRemoteCtrl RCETimeout) networkIOTimeout $ takeRCStep vars rc@RemoteCtrl {remoteCtrlId} <- upsertRemoteCtrl ctrlName rcCtrlPairing remoteOutputQ <- asks (tbqSize . config) >>= newTBQueueIO - encryption <- mkCtrlRemoteCrypto sessionKeys $ tlsUniq tls + encryption@RemoteCrypto {compression} <- mkCtrlRemoteCrypto sessionKeys (tlsUniq tls) =<< getRemoteCtrlAppInfo sseq cc <- ask http2Server <- liftIO . async $ attachHTTP2Server tls $ \req -> handleRemoteCommand execCC encryption remoteOutputQ req `runReaderT` cc void . forkIO $ monitor sseq http2Server updateRemoteCtrlSession sseq $ \case - RCSessionPendingConfirmation {} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ} + RCSessionPendingConfirmation {ctrlAppInfo} -> Right RCSessionConnected {remoteCtrlId, rcsClient = client, rcsSession, tls, http2Server, remoteOutputQ, ctrlAppInfo} _ -> Left $ ChatErrorRemoteCtrl RCEBadState - pure $ remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls} + let remoteCtrl = remoteCtrlInfo rc $ Just RCSConnected {sessionCode = tlsSessionCode tls} + pure CRRemoteCtrlConnected {remoteCtrl, compression} where upsertRemoteCtrl :: Text -> RCCtrlPairing -> CM RemoteCtrl upsertRemoteCtrl ctrlName rcCtrlPairing = withStore $ \db -> do @@ -717,6 +722,18 @@ updateRemoteCtrlSession sseq state = do Right st' -> Right () <$ writeTVar session (Just (sseq, st')) liftEither r +getRemoteCtrlAppInfo :: SessionSeq -> CM (Maybe CtrlAppInfo) +getRemoteCtrlAppInfo sseq = chatReadVar remoteCtrlSession $>>= pure . appInfo + where + appInfo (currSseq, sess) + | sseq == currSseq = case sess of + RCSessionStarting -> Nothing + RCSessionSearching {} -> Nothing + RCSessionConnecting {ctrlAppInfo} -> Just ctrlAppInfo + RCSessionPendingConfirmation {ctrlAppInfo} -> Just ctrlAppInfo + RCSessionConnected {ctrlAppInfo} -> Just ctrlAppInfo + | otherwise = Nothing + utf8String :: [Char] -> ByteString utf8String = encodeUtf8 . T.pack {-# INLINE utf8String #-} diff --git a/src/Simplex/Chat/Remote/Protocol.hs b/src/Simplex/Chat/Remote/Protocol.hs index d8155db7d6..b71c6baed8 100644 --- a/src/Simplex/Chat/Remote/Protocol.hs +++ b/src/Simplex/Chat/Remote/Protocol.hs @@ -11,6 +11,7 @@ module Simplex.Chat.Remote.Protocol where +import qualified Codec.Compression.Zstd as Z1 import Control.Monad import Control.Monad.Except import Control.Monad.Reader @@ -27,6 +28,7 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.ByteString.Builder (Builder, byteString, lazyByteString) import qualified Data.ByteString.Lazy as LB +import qualified Data.ByteString.Lazy.Internal as LB import Data.String (fromString) import Data.Text (Text) import Data.Text.Encoding (decodeUtf8) @@ -37,6 +39,7 @@ import Network.Transport.Internal (decodeWord32, encodeWord32) import Simplex.Chat.Controller import Simplex.Chat.Remote.Transport import Simplex.Chat.Remote.Types +import Simplex.Chat.Types (BoolDef (..)) import Simplex.FileTransfer.Description (FileDigest (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) @@ -102,10 +105,10 @@ $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "RR") ''RemoteResponse) -- * Client side / desktop mkRemoteHostClient :: HTTP2Client -> HostSessKeys -> SessionCode -> FilePath -> HostAppInfo -> CM RemoteHostClient -mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles} = do +mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {encoding, deviceName, encryptFiles, compression} = do let HostSessKeys {chainKeys, idPrivKey, sessPrivKey} = sessionKeys signatures = RSSign {idPrivKey, sessPrivKey} - encryption <- liftIO $ mkRemoteCrypto sessionCode chainKeys signatures + encryption <- mkRemoteCrypto sessionCode chainKeys signatures $ isTrue compression pure RemoteHostClient { hostEncoding = encoding, @@ -116,17 +119,19 @@ mkRemoteHostClient httpClient sessionKeys sessionCode storePath HostAppInfo {enc storePath } -mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> CM RemoteCrypto -mkCtrlRemoteCrypto CtrlSessKeys {chainKeys, idPubKey, sessPubKey} sessionCode = +mkCtrlRemoteCrypto :: CtrlSessKeys -> SessionCode -> Maybe CtrlAppInfo -> CM RemoteCrypto +mkCtrlRemoteCrypto CtrlSessKeys {chainKeys, idPubKey, sessPubKey} sessionCode ctrlAppInfo_ = do let signatures = RSVerify {idPubKey, sessPubKey} - in liftIO $ mkRemoteCrypto sessionCode chainKeys signatures + peerCompression = maybe False (\CtrlAppInfo {compression} -> isTrue compression) ctrlAppInfo_ + mkRemoteCrypto sessionCode chainKeys signatures peerCompression -mkRemoteCrypto :: SessionCode -> TSbChainKeys -> RemoteSignatures -> IO RemoteCrypto -mkRemoteCrypto sessionCode chainKeys signatures = do +mkRemoteCrypto :: SessionCode -> TSbChainKeys -> RemoteSignatures -> Bool -> CM RemoteCrypto +mkRemoteCrypto sessionCode chainKeys signatures peerCompression = do sndCounter <- newTVarIO 0 rcvCounter <- newTVarIO 0 skippedKeys <- liftIO TM.emptyIO - pure RemoteCrypto {sessionCode, sndCounter, rcvCounter, chainKeys, skippedKeys, signatures} + useCompression <- asks $ remoteCompression . config + pure RemoteCrypto {sessionCode, sndCounter, rcvCounter, chainKeys, skippedKeys, signatures, compression = peerCompression && useCompression} closeRemoteHostClient :: RemoteHostClient -> IO () closeRemoteHostClient RemoteHostClient {httpClient} = closeHTTP2Client httpClient @@ -176,7 +181,7 @@ sendRemoteCommand RemoteHostClient {httpClient, hostEncoding, encryption} file_ let req = httpRequest encFile_ encCmd HTTP2Response {response, respBody} <- liftError' (RPEHTTP2 . tshow) $ sendRequestDirect httpClient req Nothing (rfKN, header, getNext) <- parseDecryptHTTP2Body encryption response respBody - rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecode header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding + rr <- liftEitherWith (RPEInvalidJSON . fromString) $ J.eitherDecodeStrict header >>= JT.parseEither J.parseJSON . convertJSON hostEncoding localEncoding pure (rfKN, getNext, rr) where httpRequest encFile_ cmdBld = H.requestStreaming N.methodPost "/" mempty $ \send flush -> do @@ -247,8 +252,11 @@ pattern OwsfTag = (SingleFieldJSONTag, J.Bool True) -- See https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2023-10-25-remote-control.md for encoding encryptEncodeHTTP2Body :: Word32 -> C.SbKeyNonce -> RemoteCrypto -> LazyByteString -> ExceptT RemoteProtocolError IO Builder -encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures} s = do - ct <- liftError PRERemoteControl $ RC.rcEncryptBody cmdKN $ LB.fromStrict (smpEncode sessionCode) <> s +encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures, compression} s = do + let s' + | compression = LB.fromStrict $ Z1.compress 3 $ LB.toStrict s + | otherwise = s + ct <- liftError PRERemoteControl $ RC.rcEncryptBody cmdKN $ LB.Chunk (smpEncode sessionCode) s' let ctLen = encodeWord32 (fromIntegral $ LB.length ct) signed = LB.fromStrict (encodeWord32 corrId <> ctLen) <> ct sigs <- bodySignatures signed @@ -266,12 +274,12 @@ encryptEncodeHTTP2Body corrId cmdKN RemoteCrypto {sessionCode, signatures} s = d sign k = C.signatureBytes . C.sign' k . BA.convert . CH.hashFinalize -- | Parse and decrypt HTTP2 request/response -parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, LazyByteString, Int -> IO ByteString) -parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures} hr HTTP2Body {bodyBuffer} = do +parseDecryptHTTP2Body :: HTTP2BodyChunk a => RemoteCrypto -> a -> HTTP2Body -> ExceptT RemoteProtocolError IO (C.SbKeyNonce, ByteString, Int -> IO ByteString) +parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures, compression} hr HTTP2Body {bodyBuffer} = do (corrId, ct) <- getBody (cmdKN, rfKN) <- ExceptT $ atomically $ getRemoteRcvKeys rc corrId s <- liftError PRERemoteControl $ RC.rcDecryptBody cmdKN ct - s' <- parseBody s + s' <- decompress =<< parseBody s pure (rfKN, s', getNext) where getBody :: ExceptT RemoteProtocolError IO (Word32, LazyByteString) @@ -320,3 +328,10 @@ parseDecryptHTTP2Body rc@RemoteCrypto {sessionCode, signatures} hr HTTP2Body {bo unless (LB.length bs == n) $ throwError PRESessionCode pure (LB.toStrict bs, rest) getNext sz = getBuffered bodyBuffer sz Nothing $ getBodyChunk hr + decompress :: LazyByteString -> ExceptT RemoteProtocolError IO ByteString + decompress s + | compression = case Z1.decompress $ LB.toStrict s of + Z1.Error e -> throwError $ RPEInvalidBody e + Z1.Skip -> pure B.empty + Z1.Decompress s' -> pure s' + | otherwise = pure $ LB.toStrict s diff --git a/src/Simplex/Chat/Remote/Types.hs b/src/Simplex/Chat/Remote/Types.hs index b7af624e9e..746aa7f566 100644 --- a/src/Simplex/Chat/Remote/Types.hs +++ b/src/Simplex/Chat/Remote/Types.hs @@ -21,7 +21,7 @@ import Data.Int (Int64) import Data.Text (Text) import Data.Word (Word16, Word32) import Simplex.Chat.Remote.AppVersion -import Simplex.Chat.Types (verificationCode) +import Simplex.Chat.Types (BoolDef, verificationCode) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON) @@ -47,7 +47,8 @@ data RemoteCrypto = RemoteCrypto rcvCounter :: TVar Word32, chainKeys :: TSbChainKeys, skippedKeys :: TM.TMap Word32 (C.SbKeyNonce, C.SbKeyNonce), - signatures :: RemoteSignatures + signatures :: RemoteSignatures, + compression :: Bool } getRemoteSndKeys :: RemoteCrypto -> STM (Word32, C.SbKeyNonce, C.SbKeyNonce) @@ -220,7 +221,8 @@ data RemoteFile = RemoteFile data CtrlAppInfo = CtrlAppInfo { appVersionRange :: AppVersionRange, - deviceName :: Text + deviceName :: Text, + compression :: BoolDef } deriving (Show) @@ -228,7 +230,8 @@ data HostAppInfo = HostAppInfo { appVersion :: AppVersion, deviceName :: Text, encoding :: PlatformEncoding, - encryptFiles :: Bool -- if the host encrypts files in app storage + encryptFiles :: Bool, -- if the host encrypts files in app storage + compression :: BoolDef } $(J.deriveJSON defaultJSON ''RemoteFile) diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index b1f9dc633f..ad241e9ea0 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -99,18 +99,10 @@ module Simplex.Chat.Store.Groups deleteGroupMember, deleteGroupMemberConnection, updateGroupMemberRole, - createIntroductions, - createIntrosOrUpdateVectors, setMemberVectorNewRelations, setMembersVectorsNewRelation, setMemberVectorRelationConnected, - migrateGetMemberRelationsVector, - migrateMemberRelationsVector, - migrateMemberRelationsVector', - getMemberRelationsVector_, - updateIntroStatus, - getIntroduction, - getIntroducedGroupMemberIds, + getMemberRelationsVector, createIntroReMember, createIntroToMemberContact, getMatchingContacts, @@ -151,8 +143,6 @@ module Simplex.Chat.Store.Groups setGroupChatTTL, getGroupChatTTL, getUserGroupsToExpire, - hasMembersWithoutVector, - getGMsWithoutVectorIds, updateGroupAlias, ) where @@ -166,7 +156,6 @@ import Data.ByteString (ByteString) import qualified Data.ByteString as B import Data.Char (toLower) import Data.Either (rights) -import Data.Foldable (foldrM) import Data.Int (Int64) import Data.List (partition, sortOn) import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing) @@ -1609,75 +1598,6 @@ updateGroupMemberRole :: DB.Connection -> User -> GroupMember -> GroupMemberRole updateGroupMemberRole db User {userId} GroupMember {groupMemberId} memRole = DB.execute db "UPDATE group_members SET member_role = ? WHERE user_id = ? AND group_member_id = ?" (memRole, userId, groupMemberId) -createIntroductions :: DB.Connection -> VersionChat -> [GroupMember] -> GroupMember -> IO [GroupMember] -createIntroductions db chatV reMembers toMember - | null reMembers = pure [] - | otherwise = do - currentTs <- getCurrentTime - catMaybes <$> mapM (createIntro_ currentTs) reMembers - where - createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMember) - createIntro_ ts reMember = - -- when members connect concurrently, host would try to create introductions between them in both directions; - -- this check avoids creating second (redundant) introduction - checkInverseIntro >>= \case - Just _ -> pure Nothing - Nothing -> do - DB.execute - db - [sql| - INSERT INTO group_member_intros - (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) - VALUES (?,?,?,?,?,?) - |] - (groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts) - pure $ Just reMember - where - checkInverseIntro :: IO (Maybe Int64) - checkInverseIntro = - maybeFirstRow fromOnly $ - DB.query - db - "SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1" - (groupMemberId' toMember, groupMemberId' reMember) - --- Create introductions for members without vectors and update vectors for members with vectors. --- Partitioning and updates happen in same transaction to avoid race conditions. -createIntrosOrUpdateVectors :: DB.Connection -> VersionRangeChat -> [GroupMember] -> GroupMember -> IO [GroupMember] -createIntrosOrUpdateVectors db vr reMembers toMember - | null reMembers = pure [] - | otherwise = do - (memsWithVec, memsWithoutVec) <- partitionByVector reMembers - let GroupMember {indexInGroup} = toMember - setMembersVectorsNewRelation db memsWithVec indexInGroup IDSubjectIntroduced MRIntroduced - memsWithoutVec' <- createIntroductions db (maxVersion vr) memsWithoutVec toMember - pure $ memsWithoutVec' <> memsWithVec - where - partitionByVector :: [GroupMember] -> IO ([GroupMember], [GroupMember]) -#if defined(dbPostgres) - partitionByVector members = do - let memberIds = map groupMemberId' members - -- Lock rows first to ensure partitioning doesn't change in case of concurrent updates - _ :: [Only Int] <- - DB.query - db - "SELECT 1 FROM group_members WHERE group_member_id IN ? FOR UPDATE" - (Only $ In memberIds) - memberIdsWithVec <- S.fromList . map fromOnly <$> - DB.query - db - "SELECT group_member_id FROM group_members WHERE group_member_id IN ? AND member_relations_vector IS NOT NULL" - (Only $ In memberIds) - pure $ partition (\m -> groupMemberId' m `S.member` memberIdsWithVec) members -#else - partitionByVector = foldrM checkMember ([], []) - where - checkMember m (withVec, withoutVec) = do - hasVec <- isJust <$> maybeFirstRow fromOnly - (DB.query db "SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL" (Only $ groupMemberId' m) :: IO [Only Int64]) - pure $ if hasVec then (m : withVec, withoutVec) else (withVec, m : withoutVec) -#endif - setMemberVectorNewRelations :: DB.Connection -> GroupMember -> [(Int64, (IntroductionDirection, MemberRelation))] -> IO () setMemberVectorNewRelations db GroupMember {groupMemberId} relations = do v_ <- maybeFirstRow fromOnly $ @@ -1742,100 +1662,14 @@ setMemberVectorRelationConnected db GroupMember {groupMemberId} GroupMember {ind |] (Binary v', currentTs, groupMemberId) -migrateGetMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString -migrateGetMemberRelationsVector db m@GroupMember {groupMemberId} = do - liftIO $ migrateMemberRelationsVector db m +getMemberRelationsVector :: DB.Connection -> GroupMember -> ExceptT StoreError IO ByteString +getMemberRelationsVector db GroupMember {groupMemberId} = ExceptT . firstRow fromOnly (SEGroupMemberNotFound groupMemberId) $ DB.query db "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" (Only groupMemberId) -migrateMemberRelationsVector :: DB.Connection -> GroupMember -> IO () -migrateMemberRelationsVector db GroupMember {groupMemberId} = - migrateMemberRelationsVector' db groupMemberId - -migrateMemberRelationsVector' :: DB.Connection -> GroupMemberId -> IO () -migrateMemberRelationsVector' db groupMemberId = do - currentTs <- liftIO getCurrentTime - liftIO $ do -#if defined(dbPostgres) - -- Lock the row first to ensure computation runs only after lock is acquired - _ :: [Only Int] <- - DB.query - db - "SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NULL FOR UPDATE" - (Only groupMemberId) -#endif - DB.execute - db - [sql| - UPDATE group_members - SET - member_relations_vector = ( - SELECT migrate_relations_vector(idx, direction, intro_status) - FROM ( - SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status - FROM group_member_intros i - JOIN group_members m ON m.group_member_id = i.to_group_member_id - WHERE i.re_group_member_id = group_members.group_member_id - UNION ALL - SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status - FROM group_member_intros i - JOIN group_members m ON m.group_member_id = i.re_group_member_id - WHERE i.to_group_member_id = group_members.group_member_id - ) AS relations - ), - updated_at = ? - WHERE group_member_id = ? - AND member_relations_vector IS NULL - |] - (currentTs, groupMemberId) - -getMemberRelationsVector_ :: DB.Connection -> GroupMember -> IO (Maybe ByteString) -getMemberRelationsVector_ db GroupMember {groupMemberId} = - maybeFirstRow fromOnly $ - DB.query - db - "SELECT member_relations_vector FROM group_members WHERE group_member_id = ?" - (Only groupMemberId) - -updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO () -updateIntroStatus db introId introStatus = do - currentTs <- getCurrentTime - DB.execute - db - [sql| - UPDATE group_member_intros - SET intro_status = ?, updated_at = ? - WHERE group_member_intro_id = ? - |] - (introStatus, currentTs, introId) - -getIntroduction :: DB.Connection -> GroupMember -> GroupMember -> IO (Maybe GroupMemberIntro) -getIntroduction db reMember toMember = - maybeFirstRow toIntro $ - DB.query - db - [sql| - SELECT group_member_intro_id, intro_status - FROM group_member_intros - WHERE re_group_member_id = ? AND to_group_member_id = ? - |] - (groupMemberId' reMember, groupMemberId' toMember) - where - toIntro :: (Int64, GroupMemberIntroStatus) -> GroupMemberIntro - toIntro (introId, introStatus) = - GroupMemberIntro {introId, reMember, toMember, introStatus} - -getIntroducedGroupMemberIds :: DB.Connection -> GroupMember -> IO [GroupMemberId] -getIntroducedGroupMemberIds db invitee = - map fromOnly <$> - DB.query - db - "SELECT re_group_member_id FROM group_member_intros WHERE to_group_member_id = ?" - (Only $ groupMemberId' invitee) - createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> VersionChat -> MemberInfo -> Maybe MemberRestrictions -> (CommandId, ConnId) -> SubscriptionMode -> ExceptT StoreError IO GroupMember createIntroReMember db @@ -2725,25 +2559,6 @@ getUserGroupsToExpire db User {userId} globalTTL = where cond = if globalTTL == 0 then "" else " OR chat_item_ttl IS NULL" -hasMembersWithoutVector :: DB.Connection -> IO Bool -hasMembersWithoutVector db = - fromOnly . head - <$> DB.query_ - db - "SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1)" - -getGMsWithoutVectorIds :: DB.Connection -> IO [GroupMemberId] -getGMsWithoutVectorIds db = - map fromOnly <$> - DB.query_ - db - [sql| - SELECT group_member_id - FROM group_members - WHERE member_relations_vector IS NULL - LIMIT 1000 - |] - updateGroupAlias :: DB.Connection -> UserId -> GroupInfo -> LocalAlias -> IO GroupInfo updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do updatedAt <- getCurrentTime diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 0e2db9421b..de7632150b 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -335,24 +335,24 @@ updateSndMsgDeliveryStatus db connId agentMsgId sndMsgDeliveryStatus = do |] (sndMsgDeliveryStatus, currentTs, connId, agentMsgId) -createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> Maybe Int64 -> IO () -createPendingGroupMessage db groupMemberId messageId introId_ = do +createPendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO () +createPendingGroupMessage db groupMemberId messageId = do currentTs <- getCurrentTime DB.execute db [sql| INSERT INTO pending_group_messages - (group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?) + (group_member_id, message_id, created_at, updated_at) VALUES (?,?,?,?) |] - (groupMemberId, messageId, introId_, currentTs, currentTs) + (groupMemberId, messageId, currentTs, currentTs) -getPendingGroupMessages :: DB.Connection -> Int64 -> IO [(SndMessage, ACMEventTag, Maybe Int64)] +getPendingGroupMessages :: DB.Connection -> Int64 -> IO [SndMessage] getPendingGroupMessages db groupMemberId = map pendingGroupMessage <$> DB.query db [sql| - SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id + SELECT pgm.message_id, m.shared_msg_id, m.msg_body FROM pending_group_messages pgm JOIN messages m USING (message_id) WHERE pgm.group_member_id = ? @@ -360,8 +360,8 @@ getPendingGroupMessages db groupMemberId = |] (Only groupMemberId) where - pendingGroupMessage (msgId, sharedMsgId, msgBody, cmEventTag, introId_) = - (SndMessage {msgId, sharedMsgId, msgBody}, cmEventTag, introId_) + pendingGroupMessage (msgId, sharedMsgId, msgBody) = + SndMessage {msgId, sharedMsgId, msgBody} deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO () deletePendingGroupMessage db groupMemberId messageId = diff --git a/src/Simplex/Chat/Store/Postgres/Migrations.hs b/src/Simplex/Chat/Store/Postgres/Migrations.hs index 8c59511575..977baaad41 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations.hs @@ -22,7 +22,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20250922_remove_unused_connection import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector --- import Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 +import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.Postgres.Migrations.M20251212_chat_relays import Simplex.Messaging.Agent.Store.Shared (Migration (..)) @@ -46,7 +46,7 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - -- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), ("20251212_chat_relays", m20251212_chat_relays, Just down_m20251212_chat_relays) ] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20251117_member_relations_vector.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20251117_member_relations_vector.hs index 33583cc076..d442d93ffa 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20251117_member_relations_vector.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20251117_member_relations_vector.hs @@ -7,7 +7,7 @@ import qualified Data.Text as T import Text.RawString.QQ (r) -- This migration creates custom aggregate function migrate_relations_vector(idx, direction, intro_status). --- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2). +-- Used in live migration and stage 2 migration (M20251128_migrate_member_relations). -- -- Vector byte encoding: 4 reserved | 1 direction | 3 status -- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/M20251128_member_relations_vector_stage_2.hs b/src/Simplex/Chat/Store/Postgres/Migrations/M20251128_migrate_member_relations.hs similarity index 65% rename from src/Simplex/Chat/Store/Postgres/Migrations/M20251128_member_relations_vector_stage_2.hs rename to src/Simplex/Chat/Store/Postgres/Migrations/M20251128_migrate_member_relations.hs index 7cfc273d62..5345ca7932 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/M20251128_member_relations_vector_stage_2.hs +++ b/src/Simplex/Chat/Store/Postgres/Migrations/M20251128_migrate_member_relations.hs @@ -1,9 +1,9 @@ +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Store.Postgres.Migrations.M20251128_member_relations_vector_stage_2 where +module Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations where import Data.Text (Text) -import qualified Data.Text as T import Text.RawString.QQ (r) -- Build member_relations_vector for all members that don't have it yet. @@ -13,11 +13,9 @@ import Text.RawString.QQ (r) -- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member -- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it --- TODO [relations vector] drop group_member_intros in the end of migration -m20251128_member_relations_vector_stage_2 :: Text -m20251128_member_relations_vector_stage_2 = - T.pack - [r| +m20251128_migrate_member_relations :: Text +m20251128_migrate_member_relations = + [r| UPDATE group_members SET member_relations_vector = ( SELECT migrate_relations_vector(idx, direction, intro_status) @@ -34,12 +32,14 @@ SET member_relations_vector = ( ) AS relations ) WHERE member_relations_vector IS NULL; + +DROP INDEX idx_pending_group_messages_group_member_intro_id; +ALTER TABLE pending_group_messages DROP COLUMN group_member_intro_id; |] --- TODO [relations vector] re-create group_member_intros -down_m20251128_member_relations_vector_stage_2 :: Text -down_m20251128_member_relations_vector_stage_2 = - T.pack - [r| - +down_m20251128_migrate_member_relations :: Text +down_m20251128_migrate_member_relations = + [r| +ALTER TABLE pending_group_messages ADD COLUMN group_member_intro_id BIGINT REFERENCES group_member_intros ON DELETE CASCADE; +CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(group_member_intro_id); |] diff --git a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql index 832b282e70..d2f9b7967d 100644 --- a/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql @@ -1060,7 +1060,6 @@ CREATE TABLE test_chat_schema.pending_group_messages ( pending_group_message_id bigint NOT NULL, group_member_id bigint NOT NULL, message_id bigint NOT NULL, - group_member_intro_id bigint, created_at timestamp with time zone DEFAULT now() NOT NULL, updated_at timestamp with time zone DEFAULT now() NOT NULL ); @@ -2321,10 +2320,6 @@ CREATE INDEX idx_pending_group_messages_group_member_id ON test_chat_schema.pend -CREATE INDEX idx_pending_group_messages_group_member_intro_id ON test_chat_schema.pending_group_messages USING btree (group_member_intro_id); - - - CREATE INDEX idx_pending_group_messages_message_id ON test_chat_schema.pending_group_messages USING btree (message_id); @@ -2991,11 +2986,6 @@ ALTER TABLE ONLY test_chat_schema.pending_group_messages -ALTER TABLE ONLY test_chat_schema.pending_group_messages - ADD CONSTRAINT pending_group_messages_group_member_intro_id_fkey FOREIGN KEY (group_member_intro_id) REFERENCES test_chat_schema.group_member_intros(group_member_intro_id) ON DELETE CASCADE; - - - ALTER TABLE ONLY test_chat_schema.pending_group_messages ADD CONSTRAINT pending_group_messages_message_id_fkey FOREIGN KEY (message_id) REFERENCES test_chat_schema.messages(message_id) ON DELETE CASCADE; diff --git a/src/Simplex/Chat/Store/SQLite/Migrations.hs b/src/Simplex/Chat/Store/SQLite/Migrations.hs index 5eadd086c5..f67db4c4d1 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations.hs @@ -145,7 +145,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250922_remove_unused_connections import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector --- import Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 +import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations import Simplex.Chat.Store.SQLite.Migrations.M20251212_chat_relays import Simplex.Messaging.Agent.Store.Shared (Migration (..)) @@ -292,7 +292,7 @@ schemaMigrations = ("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync), ("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade), ("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector), - -- ("20251128_member_relations_vector_stage_2", m20251128_member_relations_vector_stage_2, Just down_m20251128_member_relations_vector_stage_2) + ("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations), ("20251212_chat_relays", m20251212_chat_relays, Just down_m20251212_chat_relays) ] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251117_member_relations_vector.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251117_member_relations_vector.hs index 3e4b4157f0..c4056245e8 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251117_member_relations_vector.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251117_member_relations_vector.hs @@ -15,7 +15,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Util (SQLiteFunc, SQLiteFuncFinal, m -- This module defines custom aggregate function migrate_relations_vector(idx, direction, intro_status). -- It is passed via DBOpts and registered on DB open. --- Used in live migration and stage 2 migration (M20251128_member_relations_vector_stage_2). +-- Used in live migration and stage 2 migration (M20251128_migrate_member_relations). -- -- Vector byte encoding: 4 reserved | 1 direction | 3 status -- Direction: 0 = IDSubjectIntroduced, 1 = IDReferencedIntroduced diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20251128_member_relations_vector_stage_2.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20251128_migrate_member_relations.hs similarity index 67% rename from src/Simplex/Chat/Store/SQLite/Migrations/M20251128_member_relations_vector_stage_2.hs rename to src/Simplex/Chat/Store/SQLite/Migrations/M20251128_migrate_member_relations.hs index f510a50410..7bb7643514 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20251128_member_relations_vector_stage_2.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20251128_migrate_member_relations.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module Simplex.Chat.Store.SQLite.Migrations.M20251128_member_relations_vector_stage_2 where +module Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations where import Database.SQLite.Simple (Query) import Database.SQLite.Simple.QQ (sql) @@ -12,9 +12,8 @@ import Database.SQLite.Simple.QQ (sql) -- - direction 0 (IDSubjectIntroduced): current member (subject) is re_group_member_id, was introduced to referenced member -- - direction 1 (IDReferencedIntroduced): current member (subject) is to_group_member_id, referenced member was introduced to it --- TODO [relations vector] drop group_member_intros in the end of migration -m20251128_member_relations_vector_stage_2 :: Query -m20251128_member_relations_vector_stage_2 = +m20251128_migrate_member_relations :: Query +m20251128_migrate_member_relations = [sql| UPDATE group_members SET member_relations_vector = ( @@ -32,11 +31,14 @@ SET member_relations_vector = ( ) ) WHERE member_relations_vector IS NULL; + +DROP INDEX idx_pending_group_messages_group_member_intro_id; +ALTER TABLE pending_group_messages DROP COLUMN group_member_intro_id; |] --- TODO [relations vector] re-create group_member_intros -down_m20251128_member_relations_vector_stage_2 :: Query -down_m20251128_member_relations_vector_stage_2 = +down_m20251128_migrate_member_relations :: Query +down_m20251128_migrate_member_relations = [sql| - +ALTER TABLE pending_group_messages ADD COLUMN group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE; +CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages(group_member_intro_id); |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt index 3c15a4303c..efbadf7c53 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/agent_query_plans.txt @@ -882,7 +882,7 @@ Query: FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id - WHERE c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? + WHERE c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ? Plan: SEARCH s USING PRIMARY KEY (host=? AND port=?) SEARCH q USING PRIMARY KEY (host=? AND port=?) @@ -894,7 +894,7 @@ Query: FROM rcv_queues q JOIN servers s ON q.host = s.host AND q.port = s.port JOIN connections c ON q.conn_id = c.conn_id - WHERE q.to_subscribe = 1 AND c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? + WHERE q.to_subscribe = 1 AND c.deleted = 0 AND q.deleted = 0 AND c.user_id = ? AND q.host = ? AND q.port = ? AND COALESCE(q.server_key_hash, s.key_hash) = ? Plan: SEARCH q USING INDEX idx_rcv_queues_to_subscribe (to_subscribe=? AND host=? AND port=?) SEARCH c USING PRIMARY KEY (conn_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index c3a1954220..e85dd19b95 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -3444,14 +3444,6 @@ Query: Plan: SEARCH chat_item_reactions USING INDEX idx_chat_item_reactions_group (group_id=? AND shared_msg_id=?) -Query: - SELECT group_member_intro_id, intro_status - FROM group_member_intros - WHERE re_group_member_id = ? AND to_group_member_id = ? - -Plan: -SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?) - Query: SELECT group_scope_tag, group_scope_group_member_id FROM chat_items @@ -3501,7 +3493,7 @@ SEARCH m USING INDEX idx_group_members_user_id (user_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) Query: - SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id + SELECT pgm.message_id, m.shared_msg_id, m.msg_body FROM pending_group_messages pgm JOIN messages m USING (message_id) WHERE pgm.group_member_id = ? @@ -3732,52 +3724,6 @@ SEARCH connections USING INDEX idx_connections_group_member_id (group_member_id= LIST SUBQUERY 1 SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name -Query: - UPDATE group_member_intros SET intro_status='fwd' - WHERE re_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?) - AND to_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?) - -Plan: -SEARCH group_member_intros USING INDEX sqlite_autoindex_group_member_intros_1 (re_group_member_id=? AND to_group_member_id=?) -LIST SUBQUERY 1 -SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name -LIST SUBQUERY 2 -SCAN group_members USING COVERING INDEX idx_group_members_user_id_local_display_name - -Query: - UPDATE group_members - SET - member_relations_vector = ( - SELECT migrate_relations_vector(idx, direction, intro_status) - FROM ( - SELECT m.index_in_group AS idx, 0 AS direction, i.intro_status - FROM group_member_intros i - JOIN group_members m ON m.group_member_id = i.to_group_member_id - WHERE i.re_group_member_id = group_members.group_member_id - UNION ALL - SELECT m.index_in_group AS idx, 1 AS direction, i.intro_status - FROM group_member_intros i - JOIN group_members m ON m.group_member_id = i.re_group_member_id - WHERE i.to_group_member_id = group_members.group_member_id - ) AS relations - ), - updated_at = ? - WHERE group_member_id = ? - AND member_relations_vector IS NULL - -Plan: -SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) -CORRELATED SCALAR SUBQUERY 3 -CO-ROUTINE relations -COMPOUND QUERY -LEFT-MOST SUBQUERY -SEARCH i USING INDEX idx_group_member_intros_re_group_member_id (re_group_member_id=?) -SEARCH m USING INTEGER PRIMARY KEY (rowid=?) -UNION ALL -SEARCH i USING INDEX idx_group_member_intros_to_group_member_id (to_group_member_id=?) -SEARCH m USING INTEGER PRIMARY KEY (rowid=?) -SCAN relations - Query: UPDATE group_members SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ? @@ -4449,7 +4395,7 @@ Plan: Query: INSERT INTO pending_group_messages - (group_member_id, message_id, group_member_intro_id, created_at, updated_at) VALUES (?,?,?,?,?) + (group_member_id, message_id, created_at, updated_at) VALUES (?,?,?,?) Plan: @@ -6111,10 +6057,6 @@ Plan: Query: INSERT INTO xftp_file_descriptions (user_id, file_descr_text, file_descr_part_no, file_descr_complete, created_at, updated_at) VALUES (?,?,?,?,?,?) Plan: -Query: SELECT 1 FROM group_members WHERE group_member_id = ? AND member_relations_vector IS NOT NULL -Plan: -SEARCH group_members USING INTEGER PRIMARY KEY (rowid=?) - Query: SELECT 1 FROM settings WHERE user_id = ? LIMIT 1 Plan: SEARCH settings USING COVERING INDEX idx_settings_user_id (user_id=?) @@ -6145,12 +6087,6 @@ SCAN CONSTANT ROW SCALAR SUBQUERY 1 SEARCH chat_items USING COVERING INDEX idx_chat_items_contacts_created_at (user_id=? AND contact_id=?) -Query: SELECT EXISTS (SELECT 1 FROM group_members WHERE member_relations_vector IS NULL LIMIT 1) -Plan: -SCAN CONSTANT ROW -SCALAR SUBQUERY 1 -SCAN group_members - Query: SELECT accepted_at FROM operator_usage_conditions WHERE server_operator_id = ? AND conditions_commit = ? Plan: SEARCH operator_usage_conditions USING INDEX idx_operator_usage_conditions_conditions_commit (conditions_commit=? AND server_operator_id=?) @@ -6531,14 +6467,6 @@ Query: UPDATE files SET private_snd_file_descr = ?, updated_at = ? WHERE user_id Plan: SEARCH files USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE group_member_intros SET intro_status='con' -Plan: -SCAN group_member_intros - -Query: UPDATE group_member_intros SET intro_status='fwd' -Plan: -SCAN group_member_intros - Query: UPDATE group_members SET contact_id = ?, updated_at = ? WHERE contact_profile_id = ? Plan: SEARCH group_members USING COVERING INDEX idx_group_members_contact_profile_id (contact_profile_id=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index d9586f1a6c..4d2e8d7832 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -395,7 +395,6 @@ CREATE TABLE pending_group_messages( pending_group_message_id INTEGER PRIMARY KEY, group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, - group_member_intro_id INTEGER REFERENCES group_member_intros ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); @@ -820,9 +819,6 @@ CREATE INDEX idx_group_profiles_user_id ON group_profiles(user_id); CREATE INDEX idx_groups_chat_item_id ON groups(chat_item_id); CREATE INDEX idx_groups_group_profile_id ON groups(group_profile_id); CREATE INDEX idx_messages_group_id ON messages(group_id); -CREATE INDEX idx_pending_group_messages_group_member_intro_id ON pending_group_messages( - group_member_intro_id -); CREATE INDEX idx_pending_group_messages_message_id ON pending_group_messages( message_id ); diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index cf5eb5f4e6..d03b75972f 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1752,49 +1752,6 @@ instance TextEncoding ConnType where ConnMember -> "member" ConnUserContact -> "user_contact" -data GroupMemberIntro = GroupMemberIntro - { introId :: Int64, - reMember :: GroupMember, - toMember :: GroupMember, - introStatus :: GroupMemberIntroStatus - } - deriving (Show) - -data GroupMemberIntroStatus - = GMIntroPending - | GMIntroSent - | GMIntroInvReceived - | GMIntroInvForwarded - | GMIntroReConnected - | GMIntroToConnected - | GMIntroConnected - deriving (Eq, Show) - -instance FromField GroupMemberIntroStatus where fromField = fromTextField_ introStatusT - -instance ToField GroupMemberIntroStatus where toField = toField . serializeIntroStatus - -introStatusT :: Text -> Maybe GroupMemberIntroStatus -introStatusT = \case - "new" -> Just GMIntroPending - "sent" -> Just GMIntroSent - "rcv" -> Just GMIntroInvReceived - "fwd" -> Just GMIntroInvForwarded - "re-con" -> Just GMIntroReConnected - "to-con" -> Just GMIntroToConnected - "con" -> Just GMIntroConnected - _ -> Nothing - -serializeIntroStatus :: GroupMemberIntroStatus -> Text -serializeIntroStatus = \case - GMIntroPending -> "new" - GMIntroSent -> "sent" - GMIntroInvReceived -> "rcv" - GMIntroInvForwarded -> "fwd" - GMIntroReConnected -> "re-con" - GMIntroToConnected -> "to-con" - GMIntroConnected -> "con" - type CommandId = Int64 aCorrId :: CommandId -> ACorrId diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 37cd308d4b..ee9a6bdd75 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -259,11 +259,10 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte rhi_ ] CRRemoteHostList hs -> viewRemoteHosts hs - CRRemoteHostStarted {remoteHost_, invitation, localAddrs = RCCtrlAddress {address} :| _, ctrlPort} -> - [ plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_, - "Remote session invitation:", - plain invitation - ] + CRRemoteHostStarted {remoteHost_, invitation, localAddrs = RCCtrlAddress {address} :| addrs, ctrlPort} -> + [plain $ maybe ("new remote host" <> started) (\RemoteHostInfo {remoteHostId = rhId} -> "remote host " <> show rhId <> started) remoteHost_] + <> [plain $ "other addresses: " <> intercalate " " (map (\RCCtrlAddress {address = a} -> B.unpack (strEncode a)) addrs) | not (null addrs)] + <> ["Remote session invitation:", plain invitation] where started = " started on " <> B.unpack (strEncode address) <> ":" <> ctrlPort CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> @@ -274,8 +273,10 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte [ (maybe "connecting new remote controller" (\RemoteCtrlInfo {remoteCtrlId} -> "connecting remote controller " <> sShow remoteCtrlId) remoteCtrl_ <> ": ") <> viewRemoteCtrl ctrlAppInfo appVersion True ] - CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> - ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] + CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} compression -> + ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName <> " (" <> compressStr <> " compression)"] + where + compressStr = if compression then "with" else "no" CRSQLResult rows -> map plain rows #if !defined(dbPostgres) CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)] @@ -487,7 +488,9 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView} plain sessionCode ] CEvtNewRemoteHost RemoteHostInfo {remoteHostId = rhId, hostDeviceName} -> ["new remote host " <> sShow rhId <> " added: " <> plain hostDeviceName] - CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} -> ["remote host " <> sShow rhId <> " connected"] + CEvtRemoteHostConnected RemoteHostInfo {remoteHostId = rhId} compression -> ["remote host " <> sShow rhId <> " connected (" <> compressStr <> " compression)"] + where + compressStr = if compression then "with" else "no" CEvtRemoteHostStopped {remoteHostId_} -> [ maybe "new remote host" (mappend "remote host " . sShow) remoteHostId_ <> " stopped" ] diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 395a6de634..77fb81076e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -131,7 +131,7 @@ testCoreOpts = -- dbSchemaPrefix is not used in tests (except bot tests where it's redefined), -- instead different schema prefix is passed per client so that single test database is used dbSchemaPrefix = "", - dbPoolSize = 3, + dbPoolSize = 1, dbCreateSchema = True #else { dbFilePrefix = "./simplex_v1", -- dbFilePrefix is not used in tests (except bot tests where it's redefined) @@ -188,16 +188,11 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16} testAgentCfg :: AgentConfig testAgentCfg = aCfg - { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000} - } - -testAgentCfgSlow :: AgentConfig -testAgentCfgSlow = - testAgentCfg - { smpClientVRange = mkVersionRange (Version 1) srvHostnamesSMPClientVersion, -- v2 - smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, -- v5 - smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange minClientSMPRelayVersion sendingProxySMPVersion} -- v8 + { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}, + messageRetryInterval = RetryInterval2 {riFast = riFast {initialInterval = 50000}, riSlow = riSlow {initialInterval = 50000}} } + where + RetryInterval2 {riFast, riSlow} = messageRetryInterval aCfg testAgentCfgNoShortLinks :: AgentConfig testAgentCfgNoShortLinks = @@ -217,9 +212,6 @@ testCfg = confirmMigrations = MCYesUp } -testCfgSlow :: ChatConfig -testCfgSlow = testCfg {agentConfig = testAgentCfgSlow} - testCfgNoShortLinks :: ChatConfig testCfgNoShortLinks = testCfg {agentConfig = testAgentCfgNoShortLinks} @@ -526,7 +518,7 @@ smpServerCfg :: ServerConfig STMMsgStore smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS, False)], - tbqSize = 1, + tbqSize = 4, msgQueueQuota = 16, maxJournalMsgCount = 24, maxJournalStateLines = 4, diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 1b93013258..a56ad6d4e3 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -94,22 +94,9 @@ chatDirectTests = do describe "operators and usage conditions" $ do it "get and enable operators, accept conditions" testOperators describe "async connection handshake" $ do - describe "connect when initiating client goes offline" $ do - it "curr" $ testAsyncInitiatingOffline True testCfg testCfg - it "v5" $ testAsyncInitiatingOffline False testCfgSlow testCfgSlow - it "v5/curr" $ testAsyncInitiatingOffline False testCfgSlow testCfg - it "curr/v5" $ testAsyncInitiatingOffline True testCfg testCfgSlow - describe "connect when accepting client goes offline" $ do - it "curr" $ testAsyncAcceptingOffline True testCfg testCfg - it "v5" $ testAsyncAcceptingOffline False testCfgSlow testCfgSlow - it "v5/curr" $ testAsyncAcceptingOffline False testCfgSlow testCfg - it "curr/v5" $ testAsyncAcceptingOffline True testCfg testCfgSlow - describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do - it "curr" testFullAsyncFast - -- fails in CI - xit'' "v5" $ testFullAsyncSlow False testCfgSlow testCfgSlow - xit'' "v5/curr" $ testFullAsyncSlow False testCfgSlow testCfg - xit'' "curr/v5" $ testFullAsyncSlow True testCfg testCfgSlow + it "connect when initiating client goes offline" $ testAsyncInitiatingOffline True + it "connect when accepting client goes offline" $ testAsyncAcceptingOffline True + it "connect, fully asynchronous (when clients are never simultaneously online)" $ testFullAsyncFast describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall #if !defined(dbPostgres) @@ -1241,33 +1228,33 @@ testOperators = where opts' = testOpts {coreOptions = testCoreOpts {smpServers = [], xftpServers = []}} -testAsyncInitiatingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testAsyncInitiatingOffline withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do +testAsyncInitiatingOffline :: HasCallStack => Bool -> TestParams -> IO () +testAsyncInitiatingOffline withShortLink ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChatCfg ps aliceCfg "alice" $ \alice -> do + withTestChat ps "alice" $ \alice -> do alice <## "subscribed 1 connections on server localhost" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testAsyncAcceptingOffline :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testAsyncAcceptingOffline withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do +testAsyncAcceptingOffline :: HasCallStack => Bool -> TestParams -> IO () +testAsyncAcceptingOffline withShortLink ps = do + inv <- withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/c" (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do + withNewTestChat ps "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChatCfg ps aliceCfg "alice" $ \alice -> do - withTestChatCfg ps bobCfg "bob" $ \bob -> do + withTestChat ps "alice" $ \alice -> do + withTestChat ps "bob" $ \bob -> do alice <## "subscribed 1 connections on server localhost" bob <## "subscribed 1 connections on server localhost" concurrently_ @@ -1292,30 +1279,6 @@ testFullAsyncFast ps = do bob <## "subscribed 1 connections on server localhost" bob <## "alice (Alice): contact is connected" -testFullAsyncSlow :: HasCallStack => Bool -> ChatConfig -> ChatConfig -> TestParams -> IO () -testFullAsyncSlow withShortLink aliceCfg bobCfg ps = do - inv <- withNewTestChatCfg ps aliceCfg "alice" aliceProfile $ \alice -> do - threadDelay 250000 - alice ##> "/c" - (if withShortLink then getInvitation else getInvitationNoShortLink) alice - withNewTestChatCfg ps bobCfg "bob" bobProfile $ \bob -> do - threadDelay 250000 - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - withAlice $ \alice -> - alice <## "subscribed 1 connections on server localhost" - withBob $ \bob -> - bob <## "subscribed 1 connections on server localhost" - withAlice $ \alice -> do - alice <## "subscribed 1 connections on server localhost" - alice <## "bob (Bob): contact is connected" - withBob $ \bob -> do - bob <## "subscribed 1 connections on server localhost" - bob <## "alice (Alice): contact is connected" - where - withAlice = withTestChatCfg ps aliceCfg "alice" - withBob = withTestChatCfg ps aliceCfg "bob" - testCallType :: CallType testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}} @@ -1341,7 +1304,7 @@ repeatM_ n a = forM_ [1 .. n] $ const a testNegotiateCall :: HasCallStack => TestParams -> IO () testNegotiateCall = - testChat2 aliceProfile bobProfile $ \alice bob -> do + withTestOutput $ testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob -- just for testing db query alice ##> "/_call get" @@ -2200,7 +2163,7 @@ testUsersDifferentCIExpirationTTL ps = do showActiveUser alice "alisa" alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) - threadDelay 2000000 + threadDelay 2100000 alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) where @@ -2419,7 +2382,7 @@ testDisableCIExpirationOnlyForOneUser ps = do cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} testUsersTimedMessages :: HasCallStack => TestParams -> IO () -testUsersTimedMessages ps = do +testUsersTimedMessages ps' = do withNewTestChat ps "bob" bobProfile $ \bob -> do withNewTestChat ps "alice" aliceProfile $ \alice -> do connectUsers alice bob @@ -2462,10 +2425,8 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "[user: alice] timed message deleted: alice 1" - alice <## "[user: alice] timed message deleted: alice 2" - bob <## "timed message deleted: alice 1" - bob <## "timed message deleted: alice 2" + alice <### ["[user: alice] timed message deleted: alice 1", "[user: alice] timed message deleted: alice 2"] + bob <### ["timed message deleted: alice 1", "timed message deleted: alice 2"] alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -2477,10 +2438,8 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "timed message deleted: alisa 1" - alice <## "timed message deleted: alisa 2" - bob <## "timed message deleted: alisa 1" - bob <## "timed message deleted: alisa 2" + alice <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"] + bob <### ["timed message deleted: alisa 1", "timed message deleted: alisa 2"] alice ##> "/user" showActiveUser alice "alisa" @@ -2519,10 +2478,8 @@ testUsersTimedMessages ps = do -- messages are deleted after restart threadDelay 1000000 - alice <## "[user: alice] timed message deleted: alice 3" - alice <## "[user: alice] timed message deleted: alice 4" - bob <## "timed message deleted: alice 3" - bob <## "timed message deleted: alice 4" + alice <### ["[user: alice] timed message deleted: alice 3", "[user: alice] timed message deleted: alice 4"] + bob <### ["timed message deleted: alice 3", "timed message deleted: alice 4"] alice ##> "/user alice" showActiveUser alice "alice (Alice)" @@ -2534,15 +2491,14 @@ testUsersTimedMessages ps = do threadDelay 1000000 - alice <## "timed message deleted: alisa 3" - alice <## "timed message deleted: alisa 4" - bob <## "timed message deleted: alisa 3" - bob <## "timed message deleted: alisa 4" + alice <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"] + bob <### ["timed message deleted: alisa 3", "timed message deleted: alisa 4"] alice ##> "/user" showActiveUser alice "alisa" alice #$> ("/_get chat @6 count=100", chat, [(1,"chat banner")]) where + ps = ps' {printOutput = True} :: TestParams configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () configureTimedMessages alice bob bobId ttl = do aliceName <- userName alice @@ -2699,7 +2655,7 @@ testUserPrivacy = testSetChatItemTTL :: HasCallStack => TestParams -> IO () testSetChatItemTTL = testChat2 aliceProfile bobProfile $ - \alice bob -> do + \alice bob -> withXFTPServer $ do connectUsers alice bob alice #> "@bob 1" bob <# "alice> 1" @@ -2713,6 +2669,7 @@ testSetChatItemTTL = alice <## "use /fc 1 to cancel sending" bob <# "alice> sends file test.jpg (136.5 KiB / 139737 bytes)" bob <## "use /fr 1 [/ | ] to receive it" + alice <## "completed uploading file 1 (test.jpg) for bob" -- above items should be deleted after we set ttl threadDelay 3000000 alice #> "@bob 3" diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 2de9d7ffe5..a71e7ae173 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -761,7 +761,9 @@ testXFTPDeleteUploadedFileGroup = alice ##> "/fc 1" concurrentlyN_ - [ alice <## "cancelled sending file 1 (test.pdf) to bob, cath", + [ do + recipients <- dropStrPrefix "cancelled sending file 1 (test.pdf) to " <$> getTermLine alice + recipients == "bob, cath" || recipients == "cath, bob" `shouldBe` True, cath <## "alice cancelled sending file 1 (test.pdf)" ] diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index b0f4fc99dc..166528e69c 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -94,10 +94,7 @@ chatGroupTests = do describe "batch send messages" $ do it "send multiple messages api" testSendMulti it "send multiple timed messages" testSendMultiTimed -#if !defined(dbPostgres) - -- TODO [postgres] this test hangs with PostgreSQL it "send multiple messages (many chat batches)" testSendMultiManyBatches -#endif it "shared message body is reused" testSharedMessageBody it "shared batch body is reused" testSharedBatchBody describe "async group connections" $ do @@ -124,7 +121,6 @@ chatGroupTests = do it "ok to connect; known group" testPlanGroupLinkKnown it "own group link" testPlanGroupLinkOwn it "group link without contact - connecting" testPlanGroupLinkConnecting - it "group link without contact - connecting (slow handshake)" testPlanGroupLinkConnectingSlow it "re-join existing group after leaving" testPlanGroupLinkLeaveRejoin #if !defined(dbPostgres) -- TODO [postgres] restore from outdated db backup (same as in agent) @@ -1774,8 +1770,6 @@ testGroupDelayedModeration ps = do -- imitate not implemented group forwarding -- (real client wouldn't have forwarding code, but tests use "current code" with configured version, -- and forwarding client doesn't check compatibility) - void $ withCCTransaction alice $ \db -> - DB.execute_ db "UPDATE group_member_intros SET intro_status='con'" updateGroupForwardingVectors alice "bob" "cath" MRConnected cath #> "#team hi" -- message is pending for bob @@ -1821,8 +1815,6 @@ testGroupDelayedModerationFullDelete ps = do -- imitate not implemented group forwarding -- (real client wouldn't have forwarding code, but tests use "current code" with configured version, -- and forwarding client doesn't check compatibility) - void $ withCCTransaction alice $ \db -> - DB.execute_ db "UPDATE group_member_intros SET intro_status='con'" updateGroupForwardingVectors alice "bob" "cath" MRConnected cath #> "#team hi" -- message is pending for bob @@ -2048,20 +2040,17 @@ testSendMultiManyBatches = (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) - aliceItemsCount <- withCCTransaction alice $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdAlice) :: IO [[Int]] - aliceItemsCount `shouldBe` [[300]] - - bobItemsCount <- withCCTransaction bob $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] - bobItemsCount `shouldBe` [[300]] - - cathItemsCount <- withCCTransaction cath $ \db -> - DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdCath) :: IO [[Int]] - cathItemsCount `shouldBe` [[300]] + checkItemCount alice msgIdAlice 300 + checkItemCount bob msgIdBob 300 + checkItemCount cath msgIdCath 300 + where + checkItemCount c msgId n = do + itemsCount <- withCCTransaction c $ \db -> + DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgId) :: IO [[Int]] + itemsCount `shouldBe` [[n]] testSharedMessageBody :: HasCallStack => TestParams -> IO () -testSharedMessageBody ps = +testSharedMessageBody ps' = withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do withSmpServer' serverCfg' $ withNewTestChatOpts ps opts' "bob" bobProfile $ \bob -> @@ -2070,9 +2059,7 @@ testSharedMessageBody ps = alice <## "disconnected 4 connections on server localhost" alice #> "#team hello" - bodiesCount1 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount1 `shouldBe` [[1]] + checkMsgBodyCount alice 1 withSmpServer' serverCfg' $ withTestChatOpts ps opts' "bob" $ \bob -> @@ -2084,12 +2071,15 @@ testSharedMessageBody ps = ] bob <# "#team alice> hello" cath <# "#team alice> hello" - bodiesCount2 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount2 `shouldBe` [[0]] +-- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body +#if !defined(dbPostgres) + threadDelay 500000 + checkMsgBodyCount alice 0 +#endif alice <## "disconnected 4 connections on server localhost" where + ps = ps' {printOutput = True} :: TestParams tmp = tmpPath ps serverCfg' = smpServerCfg @@ -2104,6 +2094,12 @@ testSharedMessageBody ps = } } +checkMsgBodyCount :: TestCC -> Int -> IO () +checkMsgBodyCount c n = do + bodiesCount <- withCCAgentTransaction c $ \db -> + DB.query_ db "SELECT count(1) FROM snd_message_bodies" + bodiesCount `shouldBe` [[n]] + testSharedBatchBody :: HasCallStack => TestParams -> IO () testSharedBatchBody ps = withNewTestChatOpts ps opts' "alice" aliceProfile $ \alice -> do @@ -2120,9 +2116,7 @@ testSharedBatchBody ps = _ <- getTermLine alice alice <## "300 messages sent" - bodiesCount1 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount1 `shouldBe` [[3]] + checkMsgBodyCount alice 3 withSmpServer' serverCfg' $ withTestChatOpts ps opts' "bob" $ \bob -> @@ -2136,9 +2130,10 @@ testSharedBatchBody ps = concurrently_ (bob <# ("#team alice> message " <> show i)) (cath <# ("#team alice> message " <> show i)) - bodiesCount2 <- withCCAgentTransaction alice $ \db -> - DB.query_ db "SELECT count(1) FROM snd_message_bodies" :: IO [[Int]] - bodiesCount2 `shouldBe` [[0]] +-- because of PostgreSQL concurrency deleteSndMsgDelivery fails to delete message body +#if !defined(dbPostgres) + checkMsgBodyCount alice 0 +#endif alice <## "disconnected 4 connections on server localhost" where @@ -3615,49 +3610,6 @@ testPlanGroupLinkConnecting ps = do bob <## "group link: known group #team" bob <## "use #team to send messages" -testPlanGroupLinkConnectingSlow :: HasCallStack => TestParams -> IO () -testPlanGroupLinkConnectingSlow ps = do - gLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - threadDelay 100000 - alice ##> "/g team" - alice <## "group #team is created" - alice <## "to add members use /a team or /create link #team" - alice ##> "/create link #team" - getGroupLinkNoShortLink alice "team" GRMember True - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 100000 - - bob ##> ("/c " <> gLink) - bob <## "connection request sent!" - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting, allowed to reconnect" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting, allowed to reconnect" - - threadDelay 100000 - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - alice - <### [ "subscribed 1 connections on server localhost", - "bob (Bob): accepting request to join group #team..." - ] - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - threadDelay 500000 - bob <## "subscribed 1 connections on server localhost" - bob <## "#team: joining the group..." - - bob ##> ("/_connect plan 1 " <> gLink) - bob <## "group link: connecting to group #team" - - let gLinkSchema2 = linkAnotherSchema gLink - bob ##> ("/_connect plan 1 " <> gLinkSchema2) - bob <## "group link: connecting to group #team" - - bob ##> ("/c " <> gLink) - bob <## "group link: connecting to group #team" - #if !defined(dbPostgres) testGroupMsgDecryptError :: HasCallStack => TestParams -> IO () testGroupMsgDecryptError ps = @@ -5049,15 +5001,6 @@ setupGroupForwarding host invitee1 invitee2 = do WHERE group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?) |] (Only invitee1Name) - void $ withCCTransaction host $ \db -> - DB.execute - db - [sql| - UPDATE group_member_intros SET intro_status='fwd' - WHERE re_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?) - AND to_group_member_id IN (SELECT group_member_id FROM group_members WHERE local_display_name = ?) - |] - (invitee1Name, invitee2Name) setupGroupForwardingVectors host invitee1 invitee2 @@ -5110,8 +5053,6 @@ testGroupMsgForwardDeduplicate = createGroup3 "team" alice bob cath threadDelay 1000000 -- delay so member relations don't get overwritten to connected - void $ withCCTransaction alice $ \db -> - DB.execute_ db "UPDATE group_member_intros SET intro_status='fwd'" setupGroupForwardingVectors alice bob cath bob #> "#team hi there" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a1ab8548ed..3fdadc3b64 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -61,7 +61,6 @@ chatProfileTests = do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn it "connecting via contact address" testPlanAddressConnecting - it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected it "contact via address" testPlanAddressContactViaAddress it "contact via short address" testPlanAddressContactViaShortAddress @@ -72,7 +71,6 @@ chatProfileTests = do it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation - it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito @@ -1110,46 +1108,6 @@ testPlanAddressConnecting ps = do bob <## "contact address: known contact alice" bob <## "use @alice to send messages" -testPlanAddressConnectingSlow :: HasCallStack => TestParams -> IO () -testPlanAddressConnectingSlow ps = do - cLink <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - alice ##> "/ad" - getContactLinkNoShortLink alice True - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 100000 - - bob ##> ("/c " <> cLink) - bob <## "connection request sent!" - - bob ##> ("/_connect plan 1 " <> cLink) - bob <## "contact address: connecting, allowed to reconnect" - - let cLinkSchema2 = linkAnotherSchema cLink - bob ##> ("/_connect plan 1 " <> cLinkSchema2) - bob <## "contact address: connecting, allowed to reconnect" - - threadDelay 100000 - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - alice <## "subscribed 1 connections on server localhost" - alice <## "bob (Bob) wants to connect to you!" - alice <## "to accept: /ac bob" - alice <## "to reject: /rc bob (the sender will NOT be notified)" - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - threadDelay 500000 - bob <## "subscribed 1 connections on server localhost" - bob @@@ [("@alice", "")] - bob ##> ("/_connect plan 1 " <> cLink) - bob <## "contact address: connecting to contact alice" - - let cLinkSchema2 = linkAnotherSchema cLink - bob ##> ("/_connect plan 1 " <> cLinkSchema2) - bob <## "contact address: connecting to contact alice" - - bob ##> ("/c " <> cLink) - bob <## "contact address: connecting to contact alice" - testPlanAddressContactDeletedReconnected :: HasCallStack => TestParams -> IO () testPlanAddressContactDeletedReconnected = testChat2 aliceProfile bobProfile $ @@ -1559,30 +1517,6 @@ testSetConnectionIncognitoProhibitedDuringNegotiation ps = do alice `hasContactProfiles` ["alice", "bob"] bob `hasContactProfiles` ["alice", "bob"] -testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => TestParams -> IO () -testSetConnectionIncognitoProhibitedDuringNegotiationSlow ps = do - inv <- withNewTestChatCfg ps testCfgSlow "alice" aliceProfile $ \alice -> do - threadDelay 250000 - alice ##> "/connect" - getInvitationNoShortLink alice - withNewTestChatCfg ps testCfgSlow "bob" bobProfile $ \bob -> do - threadDelay 250000 - bob ##> ("/c " <> inv) - bob <## "confirmation sent!" - withTestChatCfg ps testCfgSlow "alice" $ \alice -> do - threadDelay 250000 - alice <## "subscribed 1 connections on server localhost" - alice ##> "/_set incognito :1 on" - alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" - withTestChatCfg ps testCfgSlow "bob" $ \bob -> do - bob <## "subscribed 1 connections on server localhost" - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - alice <##> bob - alice `hasContactProfiles` ["alice", "bob"] - bob `hasContactProfiles` ["alice", "bob"] - testConnectionIncognitoUnchangedErrors :: HasCallStack => TestParams -> IO () testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -2022,8 +1956,14 @@ testChangePCCUser = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to other user +#if defined(dbPostgres) + alice ##> "/_set conn user :2 3" + alice <## "connection 2 changed from user alisa to user alisa2, new link:" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set conn user :1 3" alice <## "connection 1 changed from user alisa to user alisa2, new link:" +#endif alice <## "" _shortInv <- getTermLine alice alice <## "" @@ -2065,8 +2005,14 @@ testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection back to initial user +#if defined(dbPostgres) + alice ##> "/_set conn user :2 1" + alice <## "connection 2 changed from user alisa to user alice, new link:" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set conn user :1 1" alice <## "connection 1 changed from user alisa to user alice, new link:" +#endif alice <## "" _shortInv <- getTermLine alice alice <## "" @@ -2104,9 +2050,16 @@ testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ alice ##> "/user alisa" showActiveUser alice "alisa" -- Change connection to incognito and make sure it's attached to the newly created user profile +#if defined(dbPostgres) + alice ##> "/_set incognito :2 on" + _ <- getTermLine alice + alice <## "connection 2 changed to incognito" +#else + -- connection ID does not change in SQLite because table has no auto-increment alice ##> "/_set incognito :1 on" _ <- getTermLine alice alice <## "connection 1 changed to incognito" +#endif bob ##> ("/connect " <> inv) bob <## "confirmation sent!" alisaIncognito <- getTermLine alice @@ -2485,10 +2438,8 @@ testEnableTimedMessagesContact = alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")]) threadDelay 1000000 - alice <## "timed message deleted: hi" - alice <## "timed message deleted: hey" - bob <## "timed message deleted: hi" - bob <## "timed message deleted: hey" + alice <### ["timed message deleted: hi", "timed message deleted: hey"] + bob <### ["timed message deleted: hi", "timed message deleted: hey"] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) -- turn off, messages are not disappearing @@ -2580,10 +2531,8 @@ testTimedMessagesEnabledGlobally = alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)"), (0, "hi"), (1, "hey")]) threadDelay 1000000 - alice <## "timed message deleted: hi" - bob <## "timed message deleted: hi" - alice <## "timed message deleted: hey" - bob <## "timed message deleted: hey" + alice <### ["timed message deleted: hi", "timed message deleted: hey"] + bob <### ["timed message deleted: hi", "timed message deleted: hey"] alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "Disappearing messages: enabled (1 sec)")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "Disappearing messages: enabled (1 sec)")]) diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 87be15afe5..0e99b26bc5 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -81,19 +81,19 @@ businessProfile = mkProfile "biz" "Biz Inc" Nothing mkProfile :: T.Text -> T.Text -> Maybe ImageData -> Profile mkProfile displayName descr image = Profile {displayName, fullName = "", shortDescr = Just descr, image, contactLink = Nothing, peerType = Nothing, preferences = defaultPrefs} -it :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation)) +it :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) it name test = Hspec.it name $ \tmp -> timeout t (test tmp) >>= maybe (error "test timed out") pure where t = 90 * 1000000 -xit' :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation)) +xit' :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) xit' = if os == "linux" then xit else it xit'' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) xit'' = ifCI xit Hspec.it -xitMacCI :: HasCallStack => String -> (TestParams -> Expectation) -> SpecWith (Arg (TestParams -> Expectation)) +xitMacCI :: HasCallStack => String -> (ps -> Expectation) -> SpecWith (Arg (ps -> Expectation)) xitMacCI = ifCI (if os == "darwin" then xit else it) it xdescribe'' :: HasCallStack => String -> SpecWith a -> SpecWith a diff --git a/tests/PostgresSchemaDump.hs b/tests/PostgresSchemaDump.hs index 61ea5f8fdd..7df0beb2fa 100644 --- a/tests/PostgresSchemaDump.hs +++ b/tests/PostgresSchemaDump.hs @@ -76,5 +76,7 @@ postgresSchemaDumpTest migrations testDBOpts@DBOpts {connstr, schema = testDBSch skipComparisonForDownMigrations :: [String] skipComparisonForDownMigrations = [ -- via_group field moves - "20250922_remove_unused_connections" + "20250922_remove_unused_connections", + -- group_member_intro_id field moves + "20251128_migrate_member_relations" ] diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index 9790681f3b..51f4324bf0 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -2,6 +2,7 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} module RemoteTests where @@ -9,11 +10,13 @@ import ChatClient import ChatTests.DBUtils import ChatTests.Utils import Control.Logger.Simple +import Control.Monad import qualified Data.Aeson as J import qualified Data.ByteString as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.List (find, isPrefixOf) import qualified Data.Map.Strict as M -import Simplex.Chat.Controller (versionNumber) +import Simplex.Chat.Controller (ChatConfig (..), versionNumber) import qualified Simplex.Chat.Controller as Controller import Simplex.Chat.Mobile.File import Simplex.Chat.Remote (remoteFilesFolder) @@ -29,6 +32,13 @@ import UnliftIO.Directory remoteTests :: SpecWith TestParams remoteTests = describe "Remote" $ do + xdescribe "No compression" $ aroundWith (. ((False, False),)) runRemoteTests + xdescribe "Mobile offers compression" $ aroundWith (. ((True, False),)) runRemoteTests + xdescribe "Desktop offers compression" $ aroundWith (. ((False, True),)) runRemoteTests + describe "With compression" $ aroundWith (. ((True, True),)) runRemoteTests + +runRemoteTests :: SpecWith ((Bool, Bool), TestParams) +runRemoteTests = do describe "protocol handshake" $ do it "connects with new pairing (stops mobile)" $ remoteHandshakeTest False it "connects with new pairing (stops desktop)" $ remoteHandshakeTest True @@ -46,14 +56,14 @@ remoteTests = describe "Remote" $ do -- * Chat commands -remoteHandshakeTest :: HasCallStack => Bool -> TestParams -> IO () -remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do +remoteHandshakeTest :: HasCallStack => Bool -> ((Bool, Bool), TestParams) -> IO () +remoteHandshakeTest viaDesktop = testRemote $ \compress mobile desktop -> do desktop ##> "/list remote hosts" desktop <## "No remote hosts" mobile ##> "/list remote ctrls" mobile <## "No remote controllers" - startRemote mobile desktop + startRemote compress mobile desktop desktop ##> "/list remote hosts" desktop <## "Remote hosts:" @@ -75,14 +85,14 @@ remoteHandshakeTest viaDesktop = testChat2 aliceProfile aliceDesktopProfile $ \m mobile ##> "/list remote ctrls" mobile <## "No remote controllers" -remoteHandshakeStoredTest :: HasCallStack => TestParams -> IO () -remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do +remoteHandshakeStoredTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +remoteHandshakeStoredTest = testRemote $ \compress mobile desktop -> do logNote "Starting new session" - startRemote mobile desktop + startRemote compress mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) logNote "Starting stored session" - startRemoteStored mobile desktop + startRemoteStored compress mobile desktop stopDesktop mobile desktop `catchAny` (logError . tshow) desktop ##> "/list remote hosts" @@ -93,29 +103,30 @@ remoteHandshakeStoredTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile mobile <## "1. My desktop" logNote "Starting stored session again" - startRemoteStored mobile desktop + startRemoteStored compress mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) -remoteHandshakeDiscoverTest :: HasCallStack => TestParams -> IO () -remoteHandshakeDiscoverTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do +remoteHandshakeDiscoverTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +remoteHandshakeDiscoverTest = testRemote $ \compress mobile desktop -> do logNote "Preparing new session" - startRemote mobile desktop + startRemote compress mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) logNote "Starting stored session with multicast" - startRemoteDiscover mobile desktop + startRemoteDiscover compress mobile desktop stopMobile mobile desktop `catchAny` (logError . tshow) -remoteHandshakeRejectTest :: HasCallStack => TestParams -> IO () -remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop mobileBob -> do +remoteHandshakeRejectTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +remoteHandshakeRejectTest = testRemote3 $ \compress mobile desktop mobileBob -> do logNote "Starting new session" - startRemote mobile desktop + startRemote compress mobile desktop stopMobile mobile desktop mobileBob ##> "/set device name MobileBob" mobileBob <## "ok" desktop ##> "/start remote host 1" desktop <##. "remote host 1 started on " + desktop <##. "other addresses: " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobileBob ##> ("/connect remote ctrl " <> inv) @@ -132,19 +143,29 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil mobile <## "Compare session code with controller and use:" mobile <## ("/verify remote ctrl " <> sessId) mobile ##> ("/verify remote ctrl " <> sessId) - mobile <## "remote controller 1 session started with My desktop" - desktop <## "remote host 1 connected" + mobile <## ("remote controller 1 session started with My desktop" <> compress) + desktop <## ("remote host 1 connected" <> compress) stopMobile mobile desktop -storedBindingsTest :: HasCallStack => TestParams -> IO () -storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile desktop -> do +storedBindingsTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +storedBindingsTest = testRemote $ \compress mobile desktop -> do desktop ##> "/set device name My desktop" desktop <## "ok" mobile ##> "/set device name Mobile" mobile <## "ok" - desktop ##> "/start remote host new addr=127.0.0.1 iface=\"lo\" port=52230" - desktop <##. "new remote host started on 127.0.0.1:52230" -- TODO: show ip? + desktop ##> "/start remote host new" + desktop <##. "new remote host started on " + addrs <- words . dropStrPrefix "other addresses: " <$> getTermLine desktop + Just localAddress <- pure $ find ("127." `isPrefixOf`) addrs + desktop <## "Remote session invitation:" + void $ getTermLine desktop + desktop ##> "/stop remote host new" + desktop <## "ok" + + desktop ##> ("/start remote host new addr=" <> localAddress <> " iface=\"lo\" port=52230") + desktop <## ("new remote host started on " <> localAddress <> ":52230") + desktop <##. "other addresses: " desktop <## "Remote session invitation:" inv <- getTermLine desktop @@ -153,9 +174,9 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto desktop <## "new remote host connecting" mobile <## "new remote controller connected" verifyRemoteCtrl mobile desktop - mobile <## "remote controller 1 session started with My desktop" + mobile <## ("remote controller 1 session started with My desktop" <> compress) desktop <## "new remote host 1 added: Mobile" - desktop <## "remote host 1 connected" + desktop <## ("remote host 1 connected" <> compress) desktop ##> "/list remote hosts" desktop <## "Remote hosts:" @@ -167,9 +188,9 @@ storedBindingsTest = testChat2 aliceProfile aliceDesktopProfile $ \mobile deskto -- TODO: more parser tests -remoteMessageTest :: HasCallStack => TestParams -> IO () -remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do - startRemote mobile desktop +remoteMessageTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +remoteMessageTest = testRemote3 $ \compress mobile desktop bob -> do + startRemote compress mobile desktop contactBob desktop bob logNote "sending messages" @@ -193,9 +214,9 @@ remoteMessageTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob threadDelay 1000000 logNote "done" -remoteStoreFileTest :: HasCallStack => TestParams -> IO () +remoteStoreFileTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () remoteStoreFileTest = - testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> + testRemote3 $ \compress mobile desktop bob -> withXFTPServer $ do let mobileFiles = "./tests/tmp/mobile_files" mobile ##> ("/_files_folder " <> mobileFiles) @@ -210,7 +231,7 @@ remoteStoreFileTest = bob ##> ("/_files_folder " <> bobFiles) bob <## "ok" - startRemote mobile desktop + startRemote compress mobile desktop contactBob desktop bob rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop) @@ -323,8 +344,8 @@ remoteStoreFileTest = r `shouldStartWith` "remote host 1 error" r `shouldContain` err -remoteCLIFileTest :: HasCallStack => TestParams -> IO () -remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> withXFTPServer $ do +remoteCLIFileTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +remoteCLIFileTest = testRemote3 $ \compress mobile desktop bob -> withXFTPServer $ do let mobileFiles = "./tests/tmp/mobile_files" mobile ##> ("/_files_folder " <> mobileFiles) mobile <## "ok" @@ -334,7 +355,7 @@ remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob desktop ##> ("/remote_hosts_folder " <> desktopHostFiles) desktop <## "ok" - startRemote mobile desktop + startRemote compress mobile desktop contactBob desktop bob rhs <- readTVarIO (Controller.remoteHostSessions $ chatController desktop) @@ -392,9 +413,9 @@ remoteCLIFileTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mob stopMobile mobile desktop -switchRemoteHostTest :: TestParams -> IO () -switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \mobile desktop bob -> do - startRemote mobile desktop +switchRemoteHostTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +switchRemoteHostTest = testRemote3 $ \compress mobile desktop bob -> do + startRemote compress mobile desktop contactBob desktop bob desktop ##> "/contacts" @@ -418,10 +439,10 @@ switchRemoteHostTest = testChat3 aliceProfile aliceDesktopProfile bobProfile $ \ desktop <## "remote host 1 error: RHEInactive" desktop ##> "/contacts" -indicateRemoteHostTest :: TestParams -> IO () -indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do +indicateRemoteHostTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +indicateRemoteHostTest = testRemote4 $ \compress mobile desktop bob cath -> do connectUsers desktop cath - startRemote mobile desktop + startRemote compress mobile desktop contactBob desktop bob -- remote contact -> remote host bob #> "@alice hi" @@ -442,8 +463,8 @@ indicateRemoteHostTest = testChat4 aliceProfile aliceDesktopProfile bobProfile c desktop <##> cath cath <##> desktop -multipleProfilesTest :: TestParams -> IO () -multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cathProfile $ \mobile desktop bob cath -> do +multipleProfilesTest :: HasCallStack => ((Bool, Bool), TestParams) -> IO () +multipleProfilesTest = testRemote4 $ \compress mobile desktop bob cath -> do connectUsers desktop cath desktop ##> "/create user desk_bottom" @@ -453,7 +474,7 @@ multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cat desktop <## "alice_desktop (Alice Desktop)" desktop <## "desk_bottom (active)" - startRemote mobile desktop + startRemote compress mobile desktop contactBob desktop bob desktop ##> "/users" desktop <## "alice (Alice) (active)" @@ -489,14 +510,35 @@ multipleProfilesTest = testChat4 aliceProfile aliceDesktopProfile bobProfile cat -- * Utils -startRemote :: TestCC -> TestCC -> IO () -startRemote mobile desktop = do +testRemote :: HasCallStack => (String -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO () +testRemote test ((mobileCompression, desktopCompression), ps) = + withNewTestChatCfg ps testCfg {remoteCompression = mobileCompression} "mobile" aliceProfile $ \mobile -> + withNewTestChatCfg ps testCfg {remoteCompression = desktopCompression} "desktop" aliceDesktopProfile $ \desktop -> + let compress = " (" <> (if mobileCompression && desktopCompression then "with" else "no") <> " compression)" + in test compress mobile desktop + + +testRemote3 :: HasCallStack => (String -> TestCC -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO () +testRemote3 test ps = + testRemote + (\compress mobile desktop -> withNewTestChat (snd ps) "bob" bobProfile $ test compress mobile desktop) + ps + +testRemote4 :: HasCallStack => (String -> TestCC -> TestCC -> TestCC -> TestCC -> IO()) -> ((Bool, Bool), TestParams) -> IO () +testRemote4 test ps = + testRemote3 + (\compress mobile desktop bob -> withNewTestChat (snd ps) "cath" cathProfile $ test compress mobile desktop bob) + ps + +startRemote :: String -> TestCC -> TestCC -> IO () +startRemote compress mobile desktop = do desktop ##> "/set device name My desktop" desktop <## "ok" mobile ##> "/set device name Mobile" mobile <## "ok" desktop ##> "/start remote host new" desktop <##. "new remote host started on " + desktop <##. "other addresses: " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobile ##> ("/connect remote ctrl " <> inv) @@ -504,14 +546,15 @@ startRemote mobile desktop = do desktop <## "new remote host connecting" mobile <## "new remote controller connected" verifyRemoteCtrl mobile desktop - mobile <## "remote controller 1 session started with My desktop" + mobile <## ("remote controller 1 session started with My desktop" <> compress) desktop <## "new remote host 1 added: Mobile" - desktop <## "remote host 1 connected" + desktop <## ("remote host 1 connected" <> compress) -startRemoteStored :: TestCC -> TestCC -> IO () -startRemoteStored mobile desktop = do +startRemoteStored :: String -> TestCC -> TestCC -> IO () +startRemoteStored compress mobile desktop = do desktop ##> "/start remote host 1" desktop <##. "remote host 1 started on " + desktop <##. "other addresses: " desktop <## "Remote session invitation:" inv <- getTermLine desktop mobile ##> ("/connect remote ctrl " <> inv) @@ -519,13 +562,14 @@ startRemoteStored mobile desktop = do desktop <## "remote host 1 connecting" mobile <## "remote controller 1 connected" verifyRemoteCtrl mobile desktop - mobile <## "remote controller 1 session started with My desktop" - desktop <## "remote host 1 connected" + mobile <## ("remote controller 1 session started with My desktop" <> compress) + desktop <## ("remote host 1 connected" <> compress) -startRemoteDiscover :: TestCC -> TestCC -> IO () -startRemoteDiscover mobile desktop = do +startRemoteDiscover :: String -> TestCC -> TestCC -> IO () +startRemoteDiscover compress mobile desktop = do desktop ##> "/start remote host 1 multicast=on" desktop <##. "remote host 1 started on " + desktop <##. "other addresses: " desktop <## "Remote session invitation:" _inv <- getTermLine desktop -- will use multicast instead mobile ##> "/find remote ctrl" @@ -538,8 +582,8 @@ startRemoteDiscover mobile desktop = do desktop <## "remote host 1 connecting" mobile <## "remote controller 1 connected" verifyRemoteCtrl mobile desktop - mobile <## "remote controller 1 session started with My desktop" - desktop <## "remote host 1 connected" + mobile <## ("remote controller 1 session started with My desktop" <> compress) + desktop <## ("remote host 1 connected" <> compress) verifyRemoteCtrl :: TestCC -> TestCC -> IO () verifyRemoteCtrl mobile desktop = do diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index 8a716ddf11..5646031cc2 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -132,7 +132,9 @@ skipComparisonForDownMigrations = -- index moves down to the end of the file "20250721_indexes", -- indexes move down to the end of the file - "20250922_remove_unused_connections" + "20250922_remove_unused_connections", + -- group_member_intros table moves down to the end of the file + "20251128_migrate_member_relations" ] getSchema :: FilePath -> FilePath -> IO String diff --git a/tests/Test.hs b/tests/Test.hs index e1a5a58c7d..639708441e 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -55,7 +55,9 @@ main = do "src/Simplex/Chat/Store/Postgres/Migrations/chat_schema.sql" #else describe "Schema dump" schemaDumpTest +#if MIN_VERSION_base(4,18,0) describe "Bot API docs" apiDocsTest +#endif around tmpBracket $ describe "WebRTC encryption" webRTCTests #endif describe "SimpleX chat markdown" markdownTests diff --git a/website/langs/en.json b/website/langs/en.json index dc97d9ab66..eca5f5beb8 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -241,6 +241,7 @@ "docs-dropdown-11": "FAQ", "docs-dropdown-12": "Security", "docs-dropdown-14": "SimpleX for business", + "docs-dropdown-15": "Verify & reproduce builds", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index 94bc69f8f3..172145d86a 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -32,6 +32,10 @@ "title": "docs-dropdown-9", "url": "/downloads/" }, + { + "title": "docs-dropdown-15", + "url": "/reproduce/" + }, { "title": "docs-dropdown-10", "url": "/transparency/" diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index e370ccc078..f9b4d15b54 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -28,6 +28,7 @@ "WEBRTC.md", "XFTP-SERVER.md", "DOWNLOADS.md", + "REPRODUCE.md", "TRANSPARENCY.md", "SECURITY.md", "FAQ.md" diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index b5f7442a75..f628180bba 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -21,7 +21,7 @@ - + @@ -30,7 +30,7 @@
- + @@ -92,7 +92,7 @@ diff --git a/website/src/css/doc.css b/website/src/css/doc.css index 937268b166..20599f0afb 100644 --- a/website/src/css/doc.css +++ b/website/src/css/doc.css @@ -268,7 +268,7 @@ header { } .dark #doc main aside ul li a.active { - color: #70F0F9; + color: white; } #doc main aside p { diff --git a/website/src/index.html b/website/src/index.html index 443b4a3302..c044d5d17a 100644 --- a/website/src/index.html +++ b/website/src/index.html @@ -88,7 +88,7 @@ active_home: true - +