mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-14 12:35:21 +00:00
Merge branch 'master' into chat-relays
This commit is contained in:
@@ -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
|
||||
|
||||
+30
-7
@@ -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
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](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
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://raw.githubusercontent.com/simplex-chat/.github/refs/heads/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
|
||||
|
||||
@@ -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:")
|
||||
}
|
||||
|
||||
|
||||
@@ -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<EnvironmentValues, RefreshAction?>, 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()
|
||||
}
|
||||
|
||||
@@ -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<EnvironmentValues, RefreshAction?>, nil)
|
||||
}
|
||||
.alert(item: $alert) { a in
|
||||
return a.alert
|
||||
}
|
||||
@@ -471,5 +467,5 @@ struct DeletedChats: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
NewChatMenuButton()
|
||||
NewChatMenuButton(showNewChatSheet: Binding.constant(false))
|
||||
}
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.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 = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.5-C9YeXjshpBqGb2o75TqxUY.a"; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.5.0.7-CDRaHJn7uof5tglscSjQL5.a"; sourceTree = "<group>"; };
|
||||
64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
|
||||
+5
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: Contributing translations to SimpleX Chat
|
||||
title: Contributing SimpleX app translations
|
||||
revision: 19.03.2023
|
||||
---
|
||||
|
||||
|
||||
+1
-2
@@ -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
|
||||
<img src="./stun_3.png">
|
||||
|
||||
If results show `srflx` and `relay` candidates, everything is set up correctly!
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](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
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](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
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
|
||||
|
||||
+10
-13
@@ -32,11 +32,11 @@
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](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
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/testflight.png" alt="iOS TestFlight" height="41">](https://testflight.apple.com/join/DWuT2LQu)
|
||||
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk)
|
||||
[<img src="https://github.com/simplex-chat/.github/blob/master/profile/images/apk_icon.png" alt="APK" height="41">](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk)
|
||||
|
||||
@@ -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).
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
(
|
||||
|
||||
@@ -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";
|
||||
|
||||
+251
@@ -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 "$@"
|
||||
@@ -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}" \
|
||||
.
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
@@ -114,6 +114,7 @@ defaultChatConfig =
|
||||
deliveryWorkerDelay = 0,
|
||||
deliveryBucketSize = 10000,
|
||||
deviceNameForRemote = "",
|
||||
remoteCompression = True,
|
||||
chatHooks = defaultChatHooks
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+31
-14
@@ -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 #-}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+13
-13
@@ -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);
|
||||
|]
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-8
@@ -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);
|
||||
|]
|
||||
@@ -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=?)
|
||||
|
||||
@@ -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=?)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
+6
-14
@@ -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,
|
||||
|
||||
+28
-71
@@ -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 [<dir>/ | <path>] 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"
|
||||
|
||||
@@ -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)"
|
||||
]
|
||||
|
||||
|
||||
+27
-86
@@ -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 <message> 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 <name> 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"
|
||||
|
||||
+23
-74
@@ -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 <message> 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)")])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
+98
-54
@@ -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
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
"title": "docs-dropdown-9",
|
||||
"url": "/downloads/"
|
||||
},
|
||||
{
|
||||
"title": "docs-dropdown-15",
|
||||
"url": "/reproduce/"
|
||||
},
|
||||
{
|
||||
"title": "docs-dropdown-10",
|
||||
"url": "/transparency/"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"WEBRTC.md",
|
||||
"XFTP-SERVER.md",
|
||||
"DOWNLOADS.md",
|
||||
"REPRODUCE.md",
|
||||
"TRANSPARENCY.md",
|
||||
"SECURITY.md",
|
||||
"FAQ.md"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
|
||||
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
|
||||
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="absolute mt-[-100px]">
|
||||
<img class="" src="/img/new/contact_page_mobile.png" alt="">
|
||||
</div>
|
||||
|
||||
|
||||
<noscript class="z-10 flex flex-col items-center pt-[40px] ml-[-15px]">
|
||||
<p class="text-2xl font-medium text-center max-w-[234px] mb-32">{{ "please-enable-javascript" | i18n({}, lang ) | safe }}</p>
|
||||
</noscript>
|
||||
@@ -92,7 +92,7 @@
|
||||
<section class="hidden md:block bg-secondary-bg-light dark:bg-secondary-bg-dark py-[20px] d-none-if-js-disabled">
|
||||
<div class="container px-5">
|
||||
<div class="text-grey-black dark:text-white">
|
||||
|
||||
|
||||
{# For Tablet #}
|
||||
<div class="hidden md:block xl:hidden for-tablet">
|
||||
<div class="contact-tab">
|
||||
@@ -100,10 +100,10 @@
|
||||
<h2 class="text-xl font-bold">{{ "scan-the-qr-code-with-the-simplex-chat-app" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="contact-tab-content flex flex-col gap-10">
|
||||
<p class="text-base mb-5">
|
||||
{{ "scan-the-qr-code-with-the-simplex-chat-app-description" | i18n({}, lang ) | safe }}
|
||||
@@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# For Desktop #}
|
||||
<div class="hidden xl:block">
|
||||
<div class="contact-tab">
|
||||
@@ -120,17 +120,17 @@
|
||||
<h2 class="text-xl font-bold">{{ "installing-simplex-chat-to-terminal" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="contact-tab-content">
|
||||
<p class="text-base mb-4">{{ "use-this-command" | i18n({}, lang ) | safe }}</p>
|
||||
<p class="bg-white flex items-center justify-between rounded p-3 shadow-[inset_0px_2px_2px_rgba(0,0,0,0.15)] mb-8">
|
||||
<span class="text-grey-black font-light text-[14px] leading-6">curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash</span>
|
||||
<!-- <img class="content_copy" src="/img/new/content-copy.svg" /> -->
|
||||
</p>
|
||||
|
||||
|
||||
<p class="flex text-base font-medium mb-[76px]">{{ "see-simplex-chat" | i18n({}, lang ) | safe }}
|
||||
<a href="" class="flex gap-1 no-underline">
|
||||
<span class="text-primary-light dark:text-primary-dark underline underline-offset-4 text-base font-medium">{{ "github-repository" | i18n({}, lang ) | safe }}</span>
|
||||
@@ -143,17 +143,17 @@
|
||||
{{ "the-instructions--source-code" | i18n({}, lang ) | safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr class="block mb-7 dark:opacity-[0.2] w-full">
|
||||
|
||||
|
||||
<div class="contact-tab">
|
||||
<div class="flex items-center justify-between my-[40px] contact-tab-btn cursor-pointer">
|
||||
<h2 class="text-xl font-bold">{{ "if-you-already-installed-simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</h2>
|
||||
<svg class="fill-grey-black dark:fill-white" width="10" height="5" viewBox="0 0 10 5" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.40813 4.79332C8.69689 5.06889 9.16507 5.06889 9.45384 4.79332C9.7426 4.51775 9.7426 4.07097 9.45384 3.7954L5.69327 0.206676C5.65717 0.17223 5.61827 0.142089 5.57727 0.116255C5.29026 -0.064587 4.90023 -0.0344467 4.64756 0.206676L0.886983 3.7954C0.598219 4.07097 0.598219 4.51775 0.886983 4.79332C1.17575 5.06889 1.64393 5.06889 1.93269 4.79332L5.17041 1.70356L8.40813 4.79332Z"/>
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="contact-tab-content">
|
||||
<p class="text-base font-medium mb-[46px]">{{ "if-you-already-installed" | i18n({}, lang ) | safe }}
|
||||
<a href="" class="text-base font-medium">{{ "simplex-chat-for-the-terminal" | i18n({}, lang ) | safe }}</a>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
|
||||
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
|
||||
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
</a>
|
||||
|
||||
<ul class="sub-menu">
|
||||
{% if ("docs" in page.url) or ('downloads' in page.url) or ('security' in page.url) or ('transparency' in page.url) or ('jobs' in page.url) or ('faq' in page.url) %}
|
||||
{% if ("docs" in page.url) or ('downloads' in page.url) or ('security' in page.url) or ('reproduce' in page.url) or ('transparency' in page.url) or ('jobs' in page.url) or ('faq' in page.url) %}
|
||||
{% for supportedLang in supportedLangsForDoc %}
|
||||
{% for language in languages.languages %}
|
||||
{% if language.label == supportedLang %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<a href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank" title="Public iOS preview on TestFlight"><img class="h-[40px] w-auto" src="/img/new/google_play.svg" /></a>
|
||||
<a href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="SimpleX F-Droid Repository"><img class="h-[40px] w-auto" src="/img/new/f_droid.svg" /></a>
|
||||
<a href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="h-[40px] w-auto" src="/img/new/testflight.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
<a href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img class="h-[40px] w-auto" src="/img/new/apk_icon.png" /></a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -268,7 +268,7 @@ header {
|
||||
}
|
||||
|
||||
.dark #doc main aside ul li a.active {
|
||||
color: #70F0F9;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#doc main aside p {
|
||||
|
||||
@@ -88,7 +88,7 @@ active_home: true
|
||||
<a class="google-play-btn hidden" href="https://play.google.com/store/apps/details?id=chat.simplex.app" target="_blank"><img src="/img/new/google_play.svg"></a>
|
||||
<a class="f-droid-btn hidden" href="{{ '' if lang == 'en' else '/' ~ lang }}/fdroid" title="{{ 'index-f-droid-title' | i18n({}, lang) }}"><img src="/img/new/f_droid.svg"></a>
|
||||
<a class="testflight-btn hidden" href="https://testflight.apple.com/join/DWuT2LQu" target="_blank"><img class="no-border" src="/img/design_3/testflight-dark.png" title="{{ 'index-testflight-title' | i18n({}, lang) }}"></a>
|
||||
<a class="android-btn hidden" href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk" target="_blank"><img src="/img/design_3/android-dark.png"></a>
|
||||
<a class="android-btn hidden" href="https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-aarch64.apk" target="_blank"><img src="/img/design_3/android-dark.png"></a>
|
||||
</div>
|
||||
<div class="security-btns">
|
||||
<a title="{{ 'index-security-assessment-title' | i18n({}, lang) }}" href="https://www.trailofbits.com/about" target="_blank">
|
||||
|
||||
@@ -116,11 +116,7 @@ Server operators will receive up to 70% of the infrastructure payments. A higher
|
||||
|
||||
**What is technology design?**
|
||||
|
||||
[Early ideas about Community Vouchers](https://github.com/simplex-chat/simplex-chat/blob/master/docs/rfcs/2024-04-26-commercial-model.md).
|
||||
|
||||
[The most recent design](https://github.com/simplex-chat/simplex-chat/blob/master/docs/rfcs/2025-10-23-vouchers.md) based on zero-knowledge proofs.
|
||||
|
||||
[Previosly shared FAQ](https://github.com/simplex-chat/simplex-chat/blob/master/docs/rfcs/2025-12-17-community-vouchers-faq.md).
|
||||
[The conceptual design](https://github.com/simplex-chat/simplex-chat/blob/master/docs/rfcs/2025-12-10-vouchers-2.md) for Community Vouchers uses zero-knowledge proofs, making the purchase, assigning vouchers to groups and their redemptions unlinkable.
|
||||
|
||||
A whitepaper will be published in February 2026.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user