diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b0d5ad4b6..1fc1c18fe1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -305,7 +305,7 @@ jobs: sudo chown -R $(id -u):$(id -g) dist-newstyle ~/.cabal - name: Run tests - if: matrix.should_run == true + if: matrix.should_run == true && matrix.arch == 'x86_64' timeout-minutes: 120 shell: bash run: | diff --git a/.gitignore b/.gitignore index 645b55ec9d..4560272980 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ website/package-lock.json website/.cache website/test/stubs-layout-cache/_includes/*.js apps/android/app/release +apps/multiplatform/.kotlin/sessions diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 680986acfe..f07e90b953 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -97,7 +97,7 @@ func showInvalidLinkAlert(_ uri: String, error: String? = nil) { } func sanitizeUri(_ s: String) -> (url: (uri: URL, sanitizedUri: URL?)?, error: String?) { - let parsed = parseSanitizeUri(s) + let parsed = parseSanitizeUri(s, safe: false) return if let uri = URL(string: s), let uriInfo = parsed?.uriInfo { (url: (uri: uri, sanitizedUri: uriInfo.sanitized.flatMap { URL(string: $0) }), error: nil) } else { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index ac5e3b2cd7..683dea0f56 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -1547,13 +1547,13 @@ func sanitizeMessage(_ parsedMsg: [FormattedText]) -> (message: String, parsedMs var updated = ft switch ft.format { case .uri: - if let sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized { + if let sanitized = parseSanitizeUri(ft.text, safe: true)?.uriInfo?.sanitized { updated = FormattedText(text: sanitized, format: .uri) pos += updated.text.count sanitizedPos = pos } case let .hyperLink(text, uri): - if let sanitized = parseSanitizeUri(uri)?.uriInfo?.sanitized { + if let sanitized = parseSanitizeUri(uri, safe: true)?.uriInfo?.sanitized { let updatedText = if let text { "[\(text)](\(sanitized))" } else { sanitized } updated = FormattedText(text: updatedText, format: .hyperLink(showText: text, linkUri: sanitized)) pos += updated.text.count diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 00a8ea67ae..32d32aeb67 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -7239,6 +7239,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Опростен режим инкогнито diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 1f32d7010e..4bfe80e388 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -7002,6 +7002,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Zjednodušený inkognito režim diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 4e87adecfb..a1c1e86c8e 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -7714,6 +7714,10 @@ chat item action Die SimpleX-Protokolle wurden von Trail of Bits überprüft. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Vereinfachter Inkognito-Modus diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 7a6efcfba8..849e75a6ec 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -7714,6 +7714,11 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + SimpleX relay link + simplex link type + Simplified incognito mode Simplified incognito mode diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 0d115013fc..e8841af7c0 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -7710,6 +7710,10 @@ chat item action Protocolos de SimpleX auditados por Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Modo incógnito simplificado diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index de08d5f39e..8ab115b001 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -6967,6 +6967,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index bfc09b29ec..0d2de840e1 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -7570,6 +7570,10 @@ chat item action Protocoles SimpleX audité par Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Mode incognito simplifié diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index d6d38773f2..c11c17bfdf 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -7714,6 +7714,10 @@ chat item action A SimpleX protokollokat a Trail of Bits auditálta. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Egyszerűsített inkognitómód diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 6fa78f1e6a..d804a1ce0f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -7714,6 +7714,10 @@ chat item action Protocolli di SimpleX esaminati da Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Modalità incognito semplificata diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 2c34ae3499..2e71c2b37a 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -7045,6 +7045,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode シークレットモードの簡素化 diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 4079a6cd9c..079e4a8775 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -7654,6 +7654,10 @@ chat item action SimpleX-protocollen beoordeeld door Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Vereenvoudigde incognitomodus diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index a0a929ac63..0791436e94 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -7464,6 +7464,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Uproszczony tryb incognito diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 42a2d04c7e..1bd12beddb 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -7713,6 +7713,10 @@ chat item action Аудит SimpleX протоколов от Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Упрощенный режим Инкогнито diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index d2f4b979d8..7b4c27e61e 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -6941,6 +6941,10 @@ chat item action SimpleX protocols reviewed by Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 0e758dc1d7..c06e95ecaf 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -7694,6 +7694,10 @@ chat item action SimpleX protokolleri Trail of Bits tarafından incelenmiştir. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Basitleştirilmiş gizli mod diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 555baafd69..10cb209a8b 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -7699,6 +7699,10 @@ chat item action Протоколи SimpleX, розглянуті Trail of Bits. No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode Спрощений режим інкогніто diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 562b623217..a92005b8b7 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -7590,6 +7590,10 @@ chat item action SimpleX 协议由 Trail of Bits 审阅。 No comment provided by engineer. + + SimpleX relay link + simplex link type + Simplified incognito mode 简化的隐身模式 diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index 9f669cf97a..40cee93faf 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -186,9 +186,9 @@ struct ParsedServerAddress: Decodable { var parseError: String } -public func parseSanitizeUri(_ s: String) -> ParsedUri? { +public func parseSanitizeUri(_ s: String, safe: Bool) -> ParsedUri? { var c = s.cString(using: .utf8)! - if let cjson = chat_parse_uri(&c) { + if let cjson = chat_parse_uri(&c, safe ? 1 : 0) { if let d = dataFromCString(cjson) { do { return try jsonDecoder.decode(ParsedUri.self, from: d) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7a70c6b664..0e4b63a2e0 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -4666,6 +4666,7 @@ public enum SimplexLinkType: String, Decodable, Hashable { case invitation case group case channel + case relay public var description: String { switch self { @@ -4673,6 +4674,7 @@ public enum SimplexLinkType: String, Decodable, Hashable { case .invitation: return NSLocalizedString("SimpleX one-time invitation", comment: "simplex link type") case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type") case .channel: return NSLocalizedString("SimpleX channel link", comment: "simplex link type") + case .relay: return NSLocalizedString("SimpleX relay link", comment: "simplex link type") } } } diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 66f570f1b6..5a3541e06d 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -24,7 +24,7 @@ extern char *chat_send_cmd_retry(chat_ctrl ctl, char *cmd, int retryNum); extern char *chat_recv_msg_wait(chat_ctrl ctl, int wait); extern char *chat_parse_markdown(char *str); extern char *chat_parse_server(char *str); -extern char *chat_parse_uri(char *str); +extern char *chat_parse_uri(char *str, int safe); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_valid_name(char *name); extern int chat_json_length(char *str); diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 2bac5f7ada..8168c92bf8 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -5,10 +5,11 @@ plugins { id("org.jetbrains.compose") kotlin("android") id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlin.plugin.compose") } android { - compileSdk = 34 + compileSdk = 35 defaultConfig { applicationId = "chat.simplex.app" @@ -191,7 +192,7 @@ tasks { outputDir = outputs.files.files.last() } exec { - workingDir("../../../scripts/android") + workingDir("../../scripts/android") environment = mapOf("JAVA_HOME" to "$javaHome") commandLine = listOf( "./compress-and-sign-apk.sh", diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index ad86759ba9..90e51c7528 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -112,7 +112,7 @@ class SimplexService: Service() { val title = generalGetString(MR.strings.simplex_service_notification_title) val text = generalGetString(MR.strings.simplex_service_notification_text) notificationManager = createNotificationChannel() - val newNtf = createNotification(title, text) + val newNtf = createServiceNotification(title, text) serviceNotification = newNtf return newNtf } @@ -167,8 +167,8 @@ class SimplexService: Service() { } return null } - - private fun createNotification(title: String, text: String): Notification { + + private fun createServiceNotification(title: String, text: String): Notification { val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent -> PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE) } @@ -181,6 +181,7 @@ class SimplexService: Service() { .setContentIntent(pendingIntent) .setSilent(true) .setShowWhen(false) // no date/time + .setOngoing(true) // Starting SDK 33 / Android 13, foreground notifications can be swiped away // Shows a button which opens notification channel settings if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index e2927e4aaf..8b4dd0e4d7 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.jetbrains.compose") id("com.android.library") id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.kotlin.plugin.compose") id("dev.icerock.mobile.multiplatform-resources") id("com.github.gmazzo.buildconfig") version "5.3.5" } @@ -39,6 +40,8 @@ kotlin { api("com.russhwolf:multiplatform-settings:1.1.1") api("com.charleskorn.kaml:kaml:0.59.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") + implementation("org.jetbrains.compose.material:material-icons-core:1.7.3") + implementation("org.jetbrains.compose.material:material-icons-extended:1.7.3") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode api("org.boofcv:boofcv-core:1.1.3") @@ -125,7 +128,7 @@ kotlin { android { namespace = "chat.simplex.common" - compileSdk = 34 + compileSdk = 35 sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdk = 26 diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index 166f4ec355..22e53af849 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -709,9 +709,11 @@ fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIM .filterNotNull() .collect { while (callCommand.isNotEmpty()) { - val cmd = callCommand.removeFirst() + val cmd = callCommand.removeFirstOrNull() Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd") - processCommand(wv, cmd) + if (cmd != null) { + processCommand(wv, cmd) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c index cfbed65c76..fd7f71d49c 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -64,7 +64,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); -extern char *chat_parse_uri(const char *str); +extern char *chat_parse_uri(const char *str, const int safe); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -148,9 +148,9 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused j } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str) { +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, __unused jclass clazz, jstring str, jint safe) { const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str)); + jstring res = (*env)->NewStringUTF(env, chat_parse_uri(_str, safe)); (*env)->ReleaseStringUTFChars(env, str, _str); return res; } diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c index 076e323ca6..9844a5927d 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -37,7 +37,7 @@ extern char *chat_recv_msg(chat_ctrl ctrl); // deprecated extern char *chat_recv_msg_wait(chat_ctrl ctrl, const int wait); extern char *chat_parse_markdown(const char *str); extern char *chat_parse_server(const char *str); -extern char *chat_parse_uri(const char *str); +extern char *chat_parse_uri(const char *str, const int safe); extern char *chat_password_hash(const char *pwd, const char *salt); extern char *chat_valid_name(const char *name); extern int chat_json_length(const char *str); @@ -158,9 +158,9 @@ Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass cla } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, jclass clazz, jstring str) { +Java_chat_simplex_common_platform_CoreKt_chatParseUri(JNIEnv *env, jclass clazz, jstring str, jint safe) { const char *_str = encode_to_utf8_chars(env, str); - jstring res = decode_to_utf8_string(env, chat_parse_uri(_str)); + jstring res = decode_to_utf8_string(env, chat_parse_uri(_str, safe)); (*env)->ReleaseStringUTFChars(env, str, _str); return res; } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 1d00d7cdb0..c00507c0a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -26,6 +26,8 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlin.collections.removeAll as remAll import kotlinx.datetime.* +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.serialization.* import kotlinx.serialization.descriptors.* @@ -472,14 +474,17 @@ object ChatModel { } fun removeLastChatItems() { - val removed: Triple + val remIndex: Int + val rem: ChatItem? chatItems.value = SnapshotStateList().apply { addAll(chatItems.value) - val remIndex = lastIndex - val rem = removeLast() - removed = Triple(rem.id, remIndex, rem.isRcvNew) + remIndex = lastIndex + rem = removeLastOrNull() + } + if (rem != null) { + val removed = Triple(rem.id, remIndex, rem.isRcvNew) + chatState.itemsRemoved(listOf(removed), chatItems.value) } - chatState.itemsRemoved(listOf(removed), chatItems.value) } suspend fun addChatItem(rhId: Long?, chatInfo: ChatInfo, cItem: ChatItem) { @@ -4432,13 +4437,15 @@ enum class SimplexLinkType(val linkType: String) { contact("contact"), invitation("invitation"), group("group"), - channel("channel"); + channel("channel"), + relay("relay"); val description: String get() = generalGetString(when (this) { contact -> MR.strings.simplex_link_contact invitation -> MR.strings.simplex_link_invitation group -> MR.strings.simplex_link_group channel -> MR.strings.simplex_link_channel + relay -> MR.strings.simplex_link_relay }) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 93cae0b787..19a3cb264b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -4634,8 +4634,8 @@ data class ParsedServerAddress ( var parseError: String ) -fun parseSanitizeUri(s: String): ParsedUri? { - val parsed = chatParseUri(s) +fun parseSanitizeUri(s: String, safe: Boolean): ParsedUri? { + val parsed = chatParseUri(s, if (safe) 1 else 0) return runCatching { json.decodeFromString(ParsedUri.serializer(), parsed) } .onFailure { Log.d(TAG, "parseSanitizeUri decode error: $it") } .getOrNull() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index 780f8c25b4..7a96bd99d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -32,7 +32,7 @@ val databaseBackend: String = if (appPlatform == AppPlatform.ANDROID) "sqlite" e class FifoQueue(private var capacity: Int) : LinkedList() { override fun add(element: E): Boolean { - if(size > capacity) removeFirst() + if (size > capacity) removeFirstOrNull() return super.add(element) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 35194ba1e6..959e4749dc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -28,7 +28,7 @@ external fun chatRecvMsg(ctrl: ChatCtrl): String external fun chatRecvMsgWait(ctrl: ChatCtrl, timeout: Int): String external fun chatParseMarkdown(str: String): String external fun chatParseServer(str: String): String -external fun chatParseUri(str: String): String +external fun chatParseUri(str: String, safe: Int): String external fun chatPasswordHash(pwd: String, salt: String): String external fun chatValidName(name: String): String external fun chatJsonLength(str: String): Int diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt index 07cdc065a7..ed40150cb1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemsLoader.kt @@ -325,7 +325,8 @@ private fun removeDuplicatesAndUpperSplits( if (idsToTrim.last().isNotEmpty()) { // it has some elements to trim from currently visible range which means the items shouldn't be trimmed // Otherwise, the last set would be empty - idsToTrim.removeLast() + // note: removeLast() produce NoSuchMethodError on Android but removeLastOrNull() works + idsToTrim.removeLastOrNull() } val allItemsToDelete = idsToTrim.flatten() if (allItemsToDelete.isNotEmpty()) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 5746c76f18..b01d07d9b8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -876,7 +876,7 @@ fun ComposeView( var updated = ft when(ft.format) { is Format.Uri -> { - val sanitized = parseSanitizeUri(ft.text)?.uriInfo?.sanitized + val sanitized = parseSanitizeUri(ft.text, safe = true)?.uriInfo?.sanitized if (sanitized != null) { updated = FormattedText(text = sanitized, format = Format.Uri()) pos += updated.text.count() @@ -884,7 +884,7 @@ fun ComposeView( } } is Format.HyperLink -> { - val sanitized = parseSanitizeUri(ft.format.linkUri)?.uriInfo?.sanitized + val sanitized = parseSanitizeUri(ft.format.linkUri, safe = true)?.uriInfo?.sanitized if (sanitized != null) { val updatedText = if (ft.format.showText == null) sanitized else "[${ft.format.showText}]($sanitized)" updated = FormattedText(text = updatedText, format = Format.HyperLink(showText = ft.format.showText, linkUri = sanitized)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 075946c008..60595fc255 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -427,7 +427,7 @@ fun showInvalidLinkAlert(uri: String, error: String? = null) { } fun sanitizeUri(s: String): Pair?, String?> { - val parsed = parseSanitizeUri(s) + val parsed = parseSanitizeUri(s, safe = false) return if (parsed?.uriInfo != null) { (true to parsed.uriInfo.sanitized) to null } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt index 5fa097fb6b..3d21cfaf0f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProcessedErrors.kt @@ -58,6 +58,7 @@ class ProcessedErrors (val interval: Long) { text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr), ) } + else -> {} } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index 40d664a257..8c38070c98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -33,7 +33,7 @@ import chat.simplex.res.MR import java.text.DecimalFormat @Composable -fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> Unit, close: () -> Unit) { +fun ModalData.AdvancedNetworkSettingsView(showModal: (@Composable ModalData.() -> Unit) -> Unit, close: () -> Unit) { val currentRemoteHost by remember { chatModel.currentRemoteHost } val developerTools = remember { appPrefs.developerTools.get() } @@ -216,7 +216,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U updateSessionMode: (TransportSessionMode) -> Unit, updateSMPProxyMode: (SMPProxyMode) -> Unit, updateSMPProxyFallback: (SMPProxyFallback) -> Unit, - showModal: (ModalData.() -> Unit) -> Unit, + showModal: (@Composable ModalData.() -> Unit) -> Unit, resetDisabled: Boolean, reset: () -> Unit, saveDisabled: Boolean, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 0b53b8d577..b1a9e39157 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -98,6 +98,7 @@ SimpleX one-time invitation SimpleX group link SimpleX channel link + SimpleX relay link via %1$s SimpleX links Description diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index dfffb826f5..136a883035 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -171,7 +171,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires LaunchedEffect(toast, toasts.size) { delay(toast.second) - simplexWindowState.toasts.removeFirst() + simplexWindowState.toasts.removeFirstOrNull() } } var windowFocused by remember { simplexWindowState.windowFocused } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 7a2a1dff0a..696e0efde8 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -93,7 +93,7 @@ actual fun LazyColumnWithScrollBar( } val modifier = if (fillMaxSize) Modifier.fillMaxSize().then(modifier) else modifier Box(Modifier.copyViewToAppBar(remember { appPrefs.appearanceBarsBlurRadius.state }.value, LocalAppBarHandler.current?.graphicsLayer).nestedScroll(connection)) { - LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content = content) ScrollBar(reverseLayout, state, scrollBarAlpha, scrollJob, scrollBarDraggingState, additionalBarOffset, additionalTopBar, chatBottomBar) } } @@ -138,7 +138,7 @@ actual fun LazyColumnWithScrollBarNoAppBar( // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) val scrollBarDraggingState = remember { mutableStateOf(false) } Box(contentAlignment = containerAlignment) { - LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content = content) Box(if (maxHeight?.value != null) Modifier.height(maxHeight.value).fillMaxWidth() else Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index e3b0642547..9be10a584b 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -195,9 +195,11 @@ fun WebRTCController(callCommand: SnapshotStateList, onResponse: ( delay(100) } while (callCommand.isNotEmpty()) { - val cmd = callCommand.removeFirst() + val cmd = callCommand.removeFirstOrNull() Log.d(TAG, "WebRTCController LaunchedEffect executing $cmd") - processCommand(cmd) + if (cmd != null) { + processCommand(cmd) + } } } } diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index e39ba48a0b..60ff535e88 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -1,9 +1,10 @@ +import org.gradle.internal.extensions.stdlib.toDefaultLowerCase import org.jetbrains.compose.desktop.application.dsl.TargetFormat -import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly plugins { kotlin("multiplatform") id("org.jetbrains.compose") + id("org.jetbrains.kotlin.plugin.compose") id("io.github.tomtzook.gradle-cmake") version "1.2.2" } @@ -89,7 +90,7 @@ compose { } } } - val os = System.getProperty("os.name", "generic").toLowerCaseAsciiOnly() + val os = System.getProperty("os.name", "generic").toDefaultLowerCase() if (os.contains("mac") || os.contains("win")) { packageName = "SimpleX" } else { diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 9c64695b00..078138c078 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -32,9 +32,9 @@ android.bundle=false desktop.version_name=6.4.3.1 desktop.version_code=117 -kotlin.version=1.9.23 -gradle.plugin.version=8.2.0 -compose.version=1.7.0 +kotlin.version=2.1.20 +gradle.plugin.version=8.7.0 +compose.version=1.8.2 # Choose sqlite or postgres backend database.backend=sqlite diff --git a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties index 4e4a6a3f29..6183001f7b 100644 --- a/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties +++ b/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Feb 14 14:23:51 GMT 2022 +#Fri Mar 21 20:38:56 ICT 2025 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/multiplatform/settings.gradle.kts b/apps/multiplatform/settings.gradle.kts index ba047edf1e..40446f1958 100644 --- a/apps/multiplatform/settings.gradle.kts +++ b/apps/multiplatform/settings.gradle.kts @@ -12,6 +12,7 @@ pluginManagement { id("com.android.application").version(extra["gradle.plugin.version"] as String) id("com.android.library").version(extra["gradle.plugin.version"] as String) id("org.jetbrains.compose").version(extra["compose.version"] as String) + id("org.jetbrains.kotlin.plugin.compose").version(extra["kotlin.version"] as String) id("org.jetbrains.kotlin.plugin.serialization").version(extra["kotlin.version"] as String) } } diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 74e761107f..449d6a9294 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -3111,6 +3111,7 @@ A_QUEUE: - "invitation" - "group" - "channel" +- "relay" --- diff --git a/cabal.project b/cabal.project index c31fba995f..05457262b7 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 931c533a3ddb86345e95ac54e24df5474d9a349b + tag: a2d35281b2e20022e75a4dcbecf16a7970d52e8f source-repository-package type: git diff --git a/docs/FAQ.md b/docs/FAQ.md index 890fa608c0..eb68e948ee 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -7,6 +7,7 @@ revision: 23.04.2024 # Frequently Asked Questions [How to use it](#how-to-use-it) +- [How do I add contacts?](#how-do-i-add-contacts) - [I have nobody to chat with! Where can I find any groups?](#i-have-nobody-to-chat-with-where-can-i-find-any-groups) - [What is database? What can I do with it?](#what-is-database-what-can-i-do-with-it) - [Can I send files over SimpleX? ](#can-i-send-files-over-simplex) @@ -29,6 +30,7 @@ revision: 23.04.2024 - [Audio or video calls without e2e encryption](#audio-or-video-calls-without-e2e-encryption) - [I clicked the link to connect, but could not connect](#i-clicked-the-link-to-connect-but-could-not-connect) - [I do not know my database passphrase](#i-do-not-know-my-database-passphrase) +- [My mobile app does not connect to desktop app](#my-mobile-app-does-not-connect-to-desktop-app) [Privacy and security](#privacy-and-security) - [Does SimpleX support post quantum cryptography?](#does-simplex-support-post-quantum-cryptography) @@ -46,6 +48,16 @@ revision: 23.04.2024 ## How to use it +### How do I connect to people? + +Tap "pencil" button in the right corner, then "Create 1-time link". Share the link with the person you want to connect to. Your contact has to paste the link in the app's search bar. The link will can also be opened via the browser, once the app is installed. + +Alternatively, you can show the QR code when meeting in person or in a video call. + +It is safe to share this link over any communication channel, it contains only public keys and can only be used once. + +If you want to share your address publicly, so that many people can connect to you, use your SimpleX address instead of 1-time links. Tap your profile image/avatar, then Your SimpleX address. Once you create the address, you can share it in social media profiles, email signature, etc. See [the comparison of SimpleX address with 1-time links](./guide/making-connections.md#comparison-of-1-time-invitation-links-and-simplex-contact-addresses). + ### I have nobody to chat with! Where can I find any groups? Please check our [Groups Directory](./DIRECTORY.md) in the first place. You might find some interesting groups and meet even more interesting people. @@ -251,6 +263,19 @@ You can resolve it by deleting the app's database: (WARNING: this results in del - on Linux/Mac, delete directories `~/.local/share/simplex` and `~/.config/simplex`, where `~` represents your home directory (/home/user) - on Flatpak, delete directory `~/.var/app/chat.simplex.simplex`. +### My mobile app does not connect to desktop app + +1. Check that both devices are connected to the same network (e.g., it won't work if mobile is connected to mobile Internet and desktop to WiFi). +2. If you use VPN on mobile, allow connections to local network in you VPN settings, or disable VPN. +3. Allow SimpleX Chat on desktop to accept network connections in system firewall settings. You may choose a specific port in desktop app to accept connections, by default it uses a random port every time. +4. Check that your WiFi router allows connections between devices (e.g., it may have an option for "device isolation", or similar). +5. If you see an error "certificate expired", please check that your device clocks are synchronized within a few seconds. +6. If iOS app fails to connect and shows an error containing "no route", check that local network connections are allowed for the app in system settings. + +Also see this post: https://simplex.chat/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.html#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol + +If none of the suggestions work for you, you can create a separate profile on each device and create a small group inviting both of your device profiles and your contact. + ## Privacy and security ### Does SimpleX support post quantum cryptography? diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index 5480e169b4..6b6af1032b 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,25 @@ + + https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html + +

New in v6.4.3.1:

+
    +
  • UI support for bot commands.
  • +
  • support markdown hyperlinks, such as [click here](https://example.com).
  • +
  • option to remove tracking parameters from the links.
  • +
+

New in v6.4-6.4.2:

+
    +
  • new UX to connect.
  • +
  • review new group members.
  • +
  • chat with group admins.
  • +
  • new UI languages: Catalan, Indonesian, Romanian and Vietnamese.
  • +
  • Linux app builds for aarch64 CPUs
  • +
+
+
https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index e47200a34f..df4ed32f05 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."931c533a3ddb86345e95ac54e24df5474d9a349b" = "03s3gnb21fnlnmayy654aq56q4kwva48mfs3qacvr7asm8fpk2p3"; + "https://github.com/simplex-chat/simplexmq.git"."a2d35281b2e20022e75a4dcbecf16a7970d52e8f" = "0sbq5mmsp8q4p527b286anbjidk2mlfdjsqrbiw90bc1rqwjwpm2"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 1a057d94fd..6d5d431867 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.3.1 +version: 6.4.4.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 0d10204ffd..a17af4b5c9 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2634,6 +2634,9 @@ processChatCommand vr nm = \case withInvitationLock "connect" (strEncode cReq) $ do subMode <- chatReadVar subscriptionMode case activeConn of + -- Nothing is legacy branch for exisiting contacts without prepared connection; + -- for new member contacts connection is prepared immediately (on xGrpDirectInv), + -- so incognito profile can be attached to it and be visible in UI before accepting Nothing -> joinNewConn subMode Just conn@Connection {connStatus} -> case connStatus of ConnPrepared -> joinPreparedConn subMode conn @@ -3531,6 +3534,7 @@ processChatCommand vr nm = \case Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' CCTChannel -> throwCmdError "channel links are not supported in this version" + CCTRelay -> throwCmdError "chat relay links are not supported in this version" connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan | connectionPlanProceed plan = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 320f81e497..e911dc8d03 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -22,9 +22,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader -import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B -import qualified Data.ByteString.Lazy.Char8 as LB import Data.Either (lefts, partitionEithers, rights) import Data.Functor (($>)) import Data.Int (Int64) @@ -353,6 +351,8 @@ processAgentMsgRcvFile _corrId aFileId msg = do agentXFTPDeleteRcvFile aFileId fileId toView $ CEvtRcvFileError user aci_ e ft +type ShouldDeleteGroupConns = Bool + processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, @@ -480,7 +480,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = atomically $ modifyTVar' tags ("error" :) logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e eToView (ChatError . CEException $ "error parsing chat message: " <> e) - checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent + withRcpt <- checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent + pure (withRcpt, False) where aChatMsgs = parseChatMessages msgBody processEvent :: Contact -> Connection -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () @@ -488,7 +489,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let tag = toCMEventTag chatMsgEvent atomically $ modifyTVar' tags (tshow tag :) logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo - (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody chatMsg + let body = chatMsgToBody chatMsg + (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta body chatMsg let ct'' = ct' {activeConn = Just conn''} :: Contact case event of XMsgNew mc -> newContentMessage ct'' mc msg msgMeta @@ -896,12 +898,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- possible improvement is to choose scope based on event (some events specify scope) (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m checkIntegrityCreateItem (CDGroupRcv gInfo' scopeInfo m') msgMeta `catchChatError` \_ -> pure () - fwdScopesMsgs <- foldM (processAChatMsg gInfo' m' tags eInfo) M.empty aChatMsgs - let GroupMember {memberRole = membershipMemRole} = membership - when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ - forM_ (M.assocs fwdScopesMsgs) $ \(groupForwardScope, fwdMsgs) -> - forwardMsgs groupForwardScope (L.reverse fwdMsgs) `catchChatError` eToView - checkSendRcpt $ rights aChatMsgs + (fwdScopesMsgs, shouldDelConns) <- foldM (processAChatMsg gInfo' m' tags eInfo) (M.empty, False) aChatMsgs + when (isUserGrpFwdRelay gInfo') $ do + unless (blockedByAdmin m) $ + forM_ (M.assocs fwdScopesMsgs) $ \(groupForwardScope, fwdMsgs) -> + forwardMsgs groupForwardScope (L.reverse fwdMsgs) `catchChatError` eToView + when shouldDelConns $ deleteGroupConnections gInfo' True + withRcpt <- checkSendRcpt $ rights aChatMsgs + pure (withRcpt, shouldDelConns) where aChatMsgs = parseChatMessages msgBody brokerTs = metaBrokerTs msgMeta @@ -910,68 +914,72 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -> GroupMember -> TVar [Text] -> Text - -> Map GroupForwardScope (NonEmpty (ChatMessage 'Json)) + -> (Map GroupForwardScope (NonEmpty (ChatMessage 'Json)), ShouldDeleteGroupConns) -> Either String AChatMessage - -> CM (Map GroupForwardScope (NonEmpty (ChatMessage 'Json))) - processAChatMsg gInfo' m' tags eInfo fwdScopeMap = \case + -> CM (Map GroupForwardScope (NonEmpty (ChatMessage 'Json)), ShouldDeleteGroupConns) + processAChatMsg gInfo' m' tags eInfo (fwdScopeMap, shouldDelConns) = \case Right (ACMsg SJson chatMsg) -> do - cmFwdScope_ <- processEvent gInfo' m' tags eInfo chatMsg `catchChatError` \e -> eToView e $> Nothing - case cmFwdScope_ of - Nothing -> pure fwdScopeMap - Just cmFwdScope -> - pure $ M.alter (Just . maybe [chatMsg] (chatMsg <|)) cmFwdScope fwdScopeMap + (cmFwdScope_, cmShouldDelConns) <- + processEvent gInfo' m' tags eInfo chatMsg `catchChatError` \e -> eToView e $> (Nothing, False) + let fwdScopeMap' = + case cmFwdScope_ of + Nothing -> fwdScopeMap + Just cmFwdScope -> M.alter (Just . maybe [chatMsg] (chatMsg <|)) cmFwdScope fwdScopeMap + shouldDelConns' = shouldDelConns || cmShouldDelConns + pure (fwdScopeMap', shouldDelConns') Right (ACMsg SBinary chatMsg) -> do void (processEvent gInfo' m' tags eInfo chatMsg) `catchChatError` \e -> eToView e - pure fwdScopeMap + pure (fwdScopeMap, shouldDelConns) Left e -> do atomically $ modifyTVar' tags ("error" :) logInfo $ "group msg=error " <> eInfo <> " " <> tshow e eToView (ChatError . CEException $ "error parsing chat message: " <> e) - pure fwdScopeMap - processEvent :: GroupInfo -> GroupMember -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM (Maybe GroupForwardScope) + pure (fwdScopeMap, shouldDelConns) + processEvent :: forall e. MsgEncodingI e => GroupInfo -> GroupMember -> TVar [Text] -> Text -> ChatMessage e -> CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) processEvent gInfo' m' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do let tag = toCMEventTag chatMsgEvent atomically $ modifyTVar' tags (tshow tag :) logInfo $ "group msg=" <> tshow tag <> " " <> eInfo - (m'', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta msgBody chatMsg + let body = chatMsgToBody chatMsg + (m'', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m' conn msgMeta body chatMsg -- ! see isForwardedGroupMsg: processing functions should return GroupForwardScope for same events case event of - XMsgNew mc -> memberCanSend m'' scope $ newGroupContentMessage gInfo' m'' mc msg brokerTs False + XMsgNew mc -> memberCanSend m'' scope $ (,False) <$> newGroupContentMessage gInfo' m'' mc msg brokerTs False where ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope - XMsgFileDescr sharedMsgId fileDescr -> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live - XMsgDel sharedMsgId memberId scope_ -> groupMessageDelete gInfo' m'' sharedMsgId memberId scope_ msg brokerTs - XMsgReact sharedMsgId (Just memberId) scope_ reaction add -> groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs + XMsgFileDescr sharedMsgId fileDescr -> (,False) <$> groupMessageFileDescription gInfo' m'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> memberCanSend m'' msgScope $ (,False) <$> groupMessageUpdate gInfo' m'' sharedMsgId mContent mentions msgScope msg brokerTs ttl live + XMsgDel sharedMsgId memberId scope_ -> (,False) <$> groupMessageDelete gInfo' m'' sharedMsgId memberId scope_ msg brokerTs + XMsgReact sharedMsgId (Just memberId) scope_ reaction add -> (,False) <$> groupMsgReaction gInfo' m'' sharedMsgId memberId scope_ reaction add msg brokerTs -- TODO discontinue XFile - XFile fInv -> Nothing <$ processGroupFileInvitation' gInfo' m'' fInv msg brokerTs - XFileCancel sharedMsgId -> xFileCancelGroup gInfo' m'' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> Nothing <$ xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName - XInfo p -> xInfoMember gInfo' m'' p brokerTs - XGrpLinkMem p -> Nothing <$ xGrpLinkMem gInfo' m'' conn' p - XGrpLinkAcpt acceptance role memberId -> Nothing <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs - XGrpMemNew memInfo msgScope -> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs - XGrpMemIntro memInfo memRestrictions_ -> Nothing <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ - XGrpMemInv memId introInv -> Nothing <$ xGrpMemInv gInfo' m'' memId introInv - XGrpMemFwd memInfo introInv -> Nothing <$ xGrpMemFwd gInfo' m'' memInfo introInv - XGrpMemRole memId memRole -> xGrpMemRole gInfo' m'' memId memRole msg brokerTs - XGrpMemRestrict memId memRestrictions -> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs - XGrpMemCon memId -> Nothing <$ xGrpMemCon gInfo' m'' memId - -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) - XGrpMemDel memId withMessages -> xGrpMemDel gInfo' m'' memId withMessages msg brokerTs - XGrpLeave -> xGrpLeave gInfo' m'' msg brokerTs - -- TODO there should be a special logic - host should forward before deleting connections - XGrpDel -> Just <$> xGrpDel gInfo' m'' msg brokerTs - XGrpInfo p' -> xGrpInfo gInfo' m'' p' msg brokerTs - XGrpPrefs ps' -> xGrpPrefs gInfo' m'' ps' + XFile fInv -> (Nothing, False) <$ processGroupFileInvitation' gInfo' m'' fInv msg brokerTs + XFileCancel sharedMsgId -> (,False) <$> xFileCancelGroup gInfo' m'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> (Nothing, False) <$ xFileAcptInvGroup gInfo' m'' sharedMsgId fileConnReq_ fName + XInfo p -> (,False) <$> xInfoMember gInfo' m'' p brokerTs + XGrpLinkMem p -> (Nothing, False) <$ xGrpLinkMem gInfo' m'' conn' p + XGrpLinkAcpt acceptance role memberId -> (Nothing, False) <$ xGrpLinkAcpt gInfo' m'' acceptance role memberId msg brokerTs + XGrpMemNew memInfo msgScope -> (,False) <$> xGrpMemNew gInfo' m'' memInfo msgScope msg brokerTs + XGrpMemIntro memInfo memRestrictions_ -> (Nothing, False) <$ xGrpMemIntro gInfo' m'' memInfo memRestrictions_ + XGrpMemInv memId introInv -> (Nothing, False) <$ xGrpMemInv gInfo' m'' memId introInv + XGrpMemFwd memInfo introInv -> (Nothing, False) <$ xGrpMemFwd gInfo' m'' memInfo introInv + XGrpMemRole memId memRole -> (,False) <$> xGrpMemRole gInfo' m'' memId memRole msg brokerTs + XGrpMemRestrict memId memRestrictions -> (,False) <$> xGrpMemRestrict gInfo' m'' memId memRestrictions msg brokerTs + XGrpMemCon memId -> (Nothing, False) <$ xGrpMemCon gInfo' m'' memId + XGrpMemDel memId withMessages -> case encoding @e of + SJson -> xGrpMemDel gInfo' m'' memId withMessages chatMsg msg brokerTs False + SBinary -> pure (Nothing, False) -- impossible + XGrpLeave -> (,False) <$> xGrpLeave gInfo' m'' msg brokerTs + XGrpDel -> (Just GFSAll, True) <$ xGrpDel gInfo' m'' msg brokerTs + XGrpInfo p' -> (,False) <$> xGrpInfo gInfo' m'' p' msg brokerTs + XGrpPrefs ps' -> (,False) <$> xGrpPrefs gInfo' m'' ps' -- TODO [knocking] why don't we forward these messages? - XGrpDirectInv connReq mContent_ msgScope -> memberCanSend m'' msgScope $ Nothing <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs - XGrpMsgForward memberId msg' msgTs -> Nothing <$ xGrpMsgForward gInfo' m'' memberId msg' msgTs - XInfoProbe probe -> Nothing <$ xInfoProbe (COMGroupMember m'') probe - XInfoProbeCheck probeHash -> Nothing <$ xInfoProbeCheck (COMGroupMember m'') probeHash - XInfoProbeOk probe -> Nothing <$ xInfoProbeOk (COMGroupMember m'') probe - BFileChunk sharedMsgId chunk -> Nothing <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta - _ -> Nothing <$ messageError ("unsupported message: " <> tshow event) + XGrpDirectInv connReq mContent_ msgScope -> memberCanSend m'' msgScope $ (Nothing, False) <$ xGrpDirectInv gInfo' m'' conn' connReq mContent_ msg brokerTs + XGrpMsgForward memberId msg' msgTs -> (Nothing, False) <$ xGrpMsgForward gInfo' m'' memberId msg' msgTs + XInfoProbe probe -> (Nothing, False) <$ xInfoProbe (COMGroupMember m'') probe + XInfoProbeCheck probeHash -> (Nothing, False) <$ xInfoProbeCheck (COMGroupMember m'') probeHash + XInfoProbeOk probe -> (Nothing, False) <$ xInfoProbeOk (COMGroupMember m'') probe + BFileChunk sharedMsgId chunk -> (Nothing, False) <$ bFileChunkGroup gInfo' sharedMsgId chunk msgMeta + _ -> (Nothing, False) <$ messageError ("unsupported message: " <> tshow event) checkSendRcpt :: [AChatMessage] -> CM Bool checkSendRcpt aMsgs = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo @@ -1472,12 +1480,16 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p xContactId_ rjctReason toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason - memberCanSend :: GroupMember -> Maybe MsgScope -> CM (Maybe GroupForwardScope) -> CM (Maybe GroupForwardScope) + memberCanSend :: + GroupMember -> + Maybe MsgScope -> + CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) -> + CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) memberCanSend m@GroupMember {memberRole} msgScope a = case msgScope of Just MSMember {} -> a Nothing | memberRole > GRObserver || memberPending m -> a - | otherwise -> messageError "member is not allowed to send messages" $> Nothing + | otherwise -> messageError "member is not allowed to send messages" $> (Nothing, False) processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () processConnMERR connEntity conn err = do @@ -1534,9 +1546,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withAckMessage' :: Text -> ConnId -> MsgMeta -> CM () -> CM () withAckMessage' label cId msgMeta action = do - withAckMessage label cId msgMeta False Nothing $ \_ -> action $> False + withAckMessage label cId msgMeta False Nothing $ \_ -> action $> (False, False) - withAckMessage :: Text -> ConnId -> MsgMeta -> Bool -> Maybe (TVar [Text]) -> (Text -> CM Bool) -> CM () + withAckMessage :: Text -> ConnId -> MsgMeta -> Bool -> Maybe (TVar [Text]) -> (Text -> CM (Bool, ShouldDeleteGroupConns)) -> CM () withAckMessage label cId msgMeta showCritical tags action = do -- [async agent commands] command should be asynchronous -- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user). @@ -1547,8 +1559,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = eInfo <- eventInfo logInfo $ label <> ": " <> eInfo tryChatError (action eInfo) >>= \case - Right withRcpt -> - withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing + Right (withRcpt, shouldDelConns) -> + unless shouldDelConns $ withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing -- If showCritical is True, then these errors don't result in ACK and show user visible alert -- This prevents losing the message that failed to be processed. Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing @@ -2962,46 +2974,67 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> updateStatus introId GMIntroReConnected updateStatus introId status = withStore' $ \db -> updateIntroStatus db introId status - xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) - xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages msg brokerTs = do + xGrpMemDel :: GroupInfo -> GroupMember -> MemberId -> Bool -> ChatMessage 'Json -> RcvMessage -> UTCTime -> Bool -> CM (Maybe GroupForwardScope, ShouldDeleteGroupConns) + xGrpMemDel gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId withMessages chatMsg msg brokerTs forwarded = do let GroupMember {memberId = membershipMemId} = membership if membershipMemId == memId then checkRole membership $ do deleteGroupLinkIfExists user gInfo - -- member records are not deleted to keep history - members <- withStore' $ \db -> getGroupMembers db vr user gInfo - deleteMembersConnections user members + unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections gInfo False withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved - when withMessages $ deleteMessages membership SMDSnd + let membership' = membership {memberStatus = GSMemRemoved} + when withMessages $ deleteMessages gInfo membership' SMDSnd deleteMemberItem RGEUserDeleted - toView $ CEvtDeletedMemberUser user gInfo {membership = membership {memberStatus = GSMemRemoved}} m withMessages - pure Nothing -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) + toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages + pure (Just GFSAll, True) else withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case - Left _ -> messageError "x.grp.mem.del with unknown member ID" $> Just GFSAll - Right member@GroupMember {groupMemberId, memberProfile} -> - checkRole member $ do + Left _ -> messageError "x.grp.mem.del with unknown member ID" $> (Just GFSAll, False) + Right deletedMember@GroupMember {groupMemberId, memberProfile} -> + checkRole deletedMember $ do -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave - deleteMemberConnection member + if isUserGrpFwdRelay gInfo && not forwarded + then do + -- Special case: forward before deleting connection. + -- It allows us to avoid adding logic in forwardMsgs to circumvent member filtering. + forwardToMember deletedMember + deleteMemberConnection' deletedMember True + else deleteMemberConnection deletedMember -- undeleted "member connected" chat item will prevent deletion of member record - gInfo' <- deleteOrUpdateMemberRecord user gInfo member - when withMessages $ deleteMessages member SMDRcv + gInfo' <- deleteOrUpdateMemberRecord user gInfo deletedMember + let deletedMember' = deletedMember {memberStatus = GSMemRemoved} + when withMessages $ deleteMessages gInfo' deletedMember' SMDRcv deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CEvtDeletedMember user gInfo' m member {memberStatus = GSMemRemoved} withMessages - pure $ memberEventForwardScope member + toView $ CEvtDeletedMember user gInfo' m deletedMember' withMessages + pure (memberEventForwardScope deletedMember, False) where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = - messageError "x.grp.mem.del with insufficient member permissions" $> Nothing + messageError "x.grp.mem.del with insufficient member permissions" $> (Nothing, False) | otherwise = a deleteMemberItem gEvent = do (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci - deleteMessages :: MsgDirectionI d => GroupMember -> SMsgDirection d -> CM () - deleteMessages delMem msgDir - | groupFeatureMemberAllowed SGFFullDelete m gInfo = deleteGroupMemberCIs user gInfo delMem m msgDir - | otherwise = markGroupMemberCIsDeleted user gInfo delMem m + deleteMessages :: MsgDirectionI d => GroupInfo -> GroupMember -> SMsgDirection d -> CM () + deleteMessages gInfo' delMem msgDir + | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem m msgDir + | otherwise = markGroupMemberCIsDeleted user gInfo' delMem m + forwardToMember :: GroupMember -> CM () + forwardToMember member = do + let GroupMember {memberId} = m + event = XGrpMsgForward memberId chatMsg brokerTs + sendGroupMemberMessage gInfo member event Nothing (pure ()) + + isUserGrpFwdRelay :: GroupInfo -> Bool + isUserGrpFwdRelay GroupInfo {membership = GroupMember {memberRole}} = + memberRole >= GRAdmin + + deleteGroupConnections :: GroupInfo -> Bool -> CM () + deleteGroupConnections gInfo waitDelivery = do + -- member records are not deleted to keep history + members <- withStore' $ \db -> getGroupMembers db vr user gInfo + deleteMembersConnections' user members waitDelivery xGrpLeave :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) xGrpLeave gInfo m msg brokerTs = do @@ -3018,20 +3051,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtLeftMember user gInfo'' m' {memberStatus = GSMemLeft} pure $ memberEventForwardScope m - xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM GroupForwardScope + xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do when (memberRole /= GROwner) $ throwChatError $ CEGroupUserRole gInfo GROwner - ms <- withStore' $ \db -> do - members <- getGroupMembers db vr user gInfo - updateGroupMemberStatus db userId membership GSMemGroupDeleted - pure members - -- member records are not deleted to keep history - deleteMembersConnections user ms + withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemGroupDeleted + unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections gInfo False (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEGroupDeleted) groupMsgToView cInfo ci toView $ CEvtGroupDeleted user gInfo'' {membership = membership {memberStatus = GSMemGroupDeleted}} m' - pure GFSAll xGrpInfo :: GroupInfo -> GroupMember -> GroupProfile -> RcvMessage -> UTCTime -> CM (Maybe GroupForwardScope) xGrpInfo g@GroupInfo {groupProfile = p, businessChat} m@GroupMember {memberRole} p' msg brokerTs @@ -3105,8 +3133,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = securityCodeChanged mCt' createItems mCt' m | otherwise = do + acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv + void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode getContact db vr user mContactId securityCodeChanged mCt' createInternalChatItem user (CDDirectRcv mCt') (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing @@ -3123,8 +3153,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createItems mCt m' | otherwise = do + acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connReq PQSupportOff (mCt, m') <- withStore $ \db -> do (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv + void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode mCt <- getContact db vr user mContactId pure (mCt, m') createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) @@ -3150,34 +3182,34 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing xGrpMsgForward :: GroupInfo -> GroupMember -> MemberId -> ChatMessage 'Json -> UTCTime -> CM () - xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId msg msgTs = do + xGrpMsgForward gInfo@GroupInfo {groupId} m@GroupMember {memberRole, localDisplayName} memberId chatMsg msgTs = do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole localDisplayName) withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case - Right author -> processForwardedMsg author msg + Right author -> processForwardedMsg author Left (SEGroupMemberNotFoundByMemberId _) -> do unknownAuthor <- createUnknownMember gInfo memberId toView $ CEvtUnknownMemberCreated user gInfo m unknownAuthor - processForwardedMsg unknownAuthor msg + processForwardedMsg unknownAuthor Left e -> throwError $ ChatErrorStore e where -- ! see isForwardedGroupMsg: forwarded group events should include msgId to be deduplicated - processForwardedMsg :: GroupMember -> ChatMessage 'Json -> CM () - processForwardedMsg author chatMsg = do - let body = LB.toStrict $ J.encode msg + processForwardedMsg :: GroupMember -> CM () + processForwardedMsg author = do + let body = chatMsgToBody chatMsg rcvMsg@RcvMessage {chatMsgEvent = ACME _ event} <- saveGroupFwdRcvMsg user groupId m author body chatMsg case event of - XMsgNew mc -> void $ memberCanSend author scope $ newGroupContentMessage gInfo author mc rcvMsg msgTs True + XMsgNew mc -> void $ memberCanSend author scope $ (,False) <$> newGroupContentMessage gInfo author mc rcvMsg msgTs True where ExtMsgContent {scope} = mcExtMsgContent mc -- file description is always allowed, to allow sending files to support scope XMsgFileDescr sharedMsgId fileDescr -> void $ groupMessageFileDescription gInfo author sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> void $ memberCanSend author msgScope $ groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live + XMsgUpdate sharedMsgId mContent mentions ttl live msgScope -> void $ memberCanSend author msgScope $ (,False) <$> groupMessageUpdate gInfo author sharedMsgId mContent mentions msgScope rcvMsg msgTs ttl live XMsgDel sharedMsgId memId scope_ -> void $ groupMessageDelete gInfo author sharedMsgId memId scope_ rcvMsg msgTs XMsgReact sharedMsgId (Just memId) scope_ reaction add -> void $ groupMsgReaction gInfo author sharedMsgId memId scope_ reaction add rcvMsg msgTs XFileCancel sharedMsgId -> void $ xFileCancelGroup gInfo author sharedMsgId XInfo p -> void $ xInfoMember gInfo author p msgTs XGrpMemNew memInfo msgScope -> void $ xGrpMemNew gInfo author memInfo msgScope rcvMsg msgTs XGrpMemRole memId memRole -> void $ xGrpMemRole gInfo author memId memRole rcvMsg msgTs - XGrpMemDel memId withMessages -> void $ xGrpMemDel gInfo author memId withMessages rcvMsg msgTs + XGrpMemDel memId withMessages -> void $ xGrpMemDel gInfo author memId withMessages chatMsg rcvMsg msgTs True XGrpLeave -> void $ xGrpLeave gInfo author rcvMsg msgTs XGrpDel -> void $ xGrpDel gInfo author rcvMsg msgTs XGrpInfo p' -> void $ xGrpInfo gInfo author p' rcvMsg msgTs diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index 8f34b740d7..fb44b416aa 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -72,7 +72,7 @@ mentionedNames = mapMaybe (\(FormattedText f _) -> mentionedName =<< f) Mention name -> Just name _ -> Nothing -data SimplexLinkType = XLContact | XLInvitation | XLGroup | XLChannel +data SimplexLinkType = XLContact | XLInvitation | XLGroup | XLChannel | XLRelay deriving (Eq, Show) colored :: Color -> Format @@ -326,6 +326,7 @@ markdownP = mconcat <$> A.many' fragmentP CCTGroup -> XLGroup CCTChannel -> XLChannel CCTContact -> XLContact + CCTRelay -> XLRelay strEncodeText :: StrEncoding a => a -> Text strEncodeText = safeDecodeUtf8 . strEncode @@ -345,48 +346,80 @@ parseUri s = case U.parseURI U.laxURIParserOptions s of -- 2) also allow whitelisted parameters, -- 3) remove all other parameters. -- *page name: lowercase latin in snake-case or hyphen-case, allowing for sinlge leading or trailing hyphen or underscore. -sanitizeUri :: U.URI -> Maybe U.URI -sanitizeUri uri@U.URI {uriAuthority, uriPath, uriQuery = U.Query originalQS} = +sanitizeUri :: Bool -> U.URI -> Maybe U.URI +sanitizeUri safe uri@U.URI {uriAuthority, uriPath, uriQuery = U.Query originalQS} = let sanitizedQS + | safe = filter (not . isSafeBlacklisted . fst) originalQS | isNamePath = case originalQS of - p : ps -> (if isBlacklisted (fst p) then id else (p :)) $ filter (isWhitelisted . fst) ps + p@(n, _) : ps -> (if isWhitelisted n || not (isBlacklisted n) then (p :) else id) $ filter (isWhitelisted . fst) ps [] -> [] | otherwise = filter (isWhitelisted . fst) originalQS in if length sanitizedQS == length originalQS then Nothing else Just $ uri {U.uriQuery = U.Query sanitizedQS} where - isBlacklisted p = any ($ p) qsBlacklist + isSafeBlacklisted p = any (`B.isPrefixOf` p) qsSafeBlacklist + isBlacklisted p = isSafeBlacklisted p || any ($ p) qsBlacklist isWhitelisted p = any (\(f, ps) -> f host && p `elem` ps) qsWhitelist host = maybe "" (\U.Authority {authorityHost = U.Host h} -> h) uriAuthority isNamePath = B.all (\c -> (c >= 'a' && c <= 'z') || c == '_' || c == '-' || c == '/') uriPath qsWhitelist :: [(ByteString -> Bool, [ByteString])] qsWhitelist = - [ (const True, ["q", "search"]), - (dom "youtube.com", ["v", "t"]), + [ (const True, ["q", "search", "search_query", "lang", "list", "page", "text", "type"]), + (dom "aliexpress.com", ["SearchText", "catId", "minPrice", "maxPrice"]), + (dom "amazon.com", ["i", "rh", "k"]), -- department, filter, keyword + (dom "baidu.com", ["wd"]), -- search string + (dom "bing.com", ["mkt"]), -- localized results + (dom "github.com", ["author", "diff", "ref", "w"]), -- author in search result, PR parameters + (dom "play.google.com", ["id"]), + (dom "reddit.com", ["t"]), -- search type, time range + (dom "wikipedia.com", ["oldid", "uselang"]), -- to show old page revision and chosen user language + (dom "x.com", ["f"]), -- feed type + (dom "yahoo.com", ["p"]), -- search string + (dom "youtube.com", ["v", "t"]), -- video ID and timestamp (dom "youtu.be", ["t"]) ] dom d h = d == h || (('.' `B.cons` d) `B.isSuffixOf` h) qsBlacklist :: [ByteString -> Bool] qsBlacklist = [ (B.any (== '_')), - ("ad" `B.isPrefixOf`), - ("af" `B.isPrefixOf`), - ("dc" `B.isPrefixOf`), - ("fb" `B.isPrefixOf`), - ("gc" `B.isPrefixOf`), - ("li" `B.isPrefixOf`), - ("ref" `B.isPrefixOf`), - ("si" `B.isPrefixOf`), - ("tw" `B.isPrefixOf`), - ("utm" `B.isPrefixOf`), - ("camp" `B.isInfixOf`), - ("cmp" `B.isInfixOf`), - ("dev" `B.isInfixOf`), - ("id" `B.isInfixOf`), - ("prom" `B.isInfixOf`), - ("source" `B.isInfixOf`), - ("src" `B.isInfixOf`) + ("id" `B.isSuffixOf`), + ("source" `B.isPrefixOf`) + ] + qsSafeBlacklist :: [ByteString] + qsSafeBlacklist = + [ "ad", + "af", + "camp", + "cmp", + "dc", + "dev", + "ef_", + "fb", + "gad_", + "gc", + "gdf", + "hsa_", + "igsh", + "li", + "matomo_", + "mc_", + "mkwid", + "msc", + "mtm_", + "pcrid", + "piwik_", + "pk_", + "prom", + "ref", + "s_kw", + "si", + "src", + "srs", + "trk_", + "tw", + "utm", + "ycl" ] markdownText :: FormattedText -> Text diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 23712cf992..f18a3900da 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -128,7 +128,7 @@ foreign export ccall "chat_parse_markdown" cChatParseMarkdown :: CString -> IO C foreign export ccall "chat_parse_server" cChatParseServer :: CString -> IO CJSONString -foreign export ccall "chat_parse_uri" cChatParseUri :: CString -> IO CJSONString +foreign export ccall "chat_parse_uri" cChatParseUri :: CString -> CInt -> IO CJSONString foreign export ccall "chat_password_hash" cChatPasswordHash :: CString -> CString -> IO CString @@ -220,8 +220,8 @@ cChatParseServer :: CString -> IO CJSONString cChatParseServer s = newCStringFromLazyBS . chatParseServer =<< B.packCString s -- | parse web URI - returns ParsedUri JSON -cChatParseUri :: CString -> IO CJSONString -cChatParseUri s = newCStringFromLazyBS . chatParseUri =<< B.packCString s +cChatParseUri :: CString -> CInt -> IO CJSONString +cChatParseUri s safe = newCStringFromLazyBS . chatParseUri (safe /= 0) =<< B.packCString s cChatPasswordHash :: CString -> CString -> IO CString cChatPasswordHash cPwd cSalt = do @@ -366,11 +366,11 @@ chatParseServer = J.encode . toServerAddress . strDecode enc :: StrEncoding a => a -> String enc = B.unpack . strEncode -chatParseUri :: ByteString -> JSONByteString -chatParseUri s = J.encode $ case parseUri s of +chatParseUri :: Bool -> ByteString -> JSONByteString +chatParseUri safe s = J.encode $ case parseUri s of Left e -> ParsedUri Nothing e Right uri@U.URI {uriScheme = U.Scheme sch} -> - let sanitized = safeDecodeUtf8 . U.serializeURIRef' <$> sanitizeUri uri + let sanitized = safeDecodeUtf8 . U.serializeURIRef' <$> sanitizeUri safe uri uriInfo = UriInfo {scheme = safeDecodeUtf8 sch, sanitized} in ParsedUri (Just uriInfo) "" diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 84d5cc8c3d..165d376814 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -385,9 +385,9 @@ isForwardedGroupMsg ev = case ev of XGrpMemNew {} -> True XGrpMemRole {} -> True XGrpMemRestrict {} -> True - XGrpMemDel {} -> True -- TODO there should be a special logic when deleting host member (e.g., host forwards it before deleting connections) + XGrpMemDel {} -> True XGrpLeave -> True - XGrpDel -> True -- TODO there should be a special logic - host should forward before deleting connections + XGrpDel -> True XGrpInfo _ -> True XGrpPrefs _ -> True _ -> False @@ -1152,18 +1152,13 @@ appJsonToCM AppMessageJson {v, msgId, event, params} = do key .=? value = maybe id ((:) . (key .=)) value chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e -chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of - SBinary -> - let (binaryMsgId, body) = toBody chatMsgEvent - in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body} +chatToAppMessage chatMsg@ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of + SBinary -> AMBinary AppMessageBinary {msgId = Nothing, tag = B.head $ strEncode tag, body = chatMsgBinaryToBody chatMsg} SJson -> AMJson AppMessageJson {v = Just $ ChatVersionRange chatVRange, msgId, event = textEncode tag, params = params chatMsgEvent} where tag = toCMEventTag chatMsgEvent o :: [(J.Key, J.Value)] -> J.Object o = JM.fromList - toBody :: ChatMsgEvent 'Binary -> (Maybe SharedMsgId, ByteString) - toBody = \case - BFileChunk (SharedMsgId msgId') chunk -> (Nothing, smpEncode (msgId', IFC chunk)) params :: ChatMsgEvent 'Json -> J.Object params = \case XMsgNew container -> msgContainerJSON container @@ -1212,6 +1207,15 @@ chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @ XOk -> JM.empty XUnknown _ ps -> ps +chatMsgBinaryToBody :: ChatMessage 'Binary -> ByteString +chatMsgBinaryToBody ChatMessage {chatMsgEvent} = case chatMsgEvent of + BFileChunk (SharedMsgId msgId) chunk -> smpEncode (msgId, IFC chunk) + +chatMsgToBody :: forall e. MsgEncodingI e => ChatMessage e -> ByteString +chatMsgToBody chatMsg = case encoding @e of + SBinary -> chatMsgBinaryToBody chatMsg + SJson -> LB.toStrict $ J.encode chatMsg + instance ToJSON (ChatMessage 'Json) where toJSON = (\(AMJson msg) -> toJSON msg) . chatToAppMessage diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index 2ae9e106cb..7c25890bf2 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 4, 1, 0] +minRemoteCtrlVersion = AppVersion [6, 4, 3, 0] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 0, 5, 1] +minRemoteHostVersion = AppVersion [6, 4, 3, 0] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 549d2bd860..8930acf3c1 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -99,9 +99,9 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections - WHERE user_id = ? AND agent_conn_id = ? + WHERE user_id = ? AND agent_conn_id = ? AND conn_status != ? |] - (userId, agentConnId) + (userId, agentConnId, ConnDeleted) getContactRec_ :: Int64 -> Connection -> ExceptT StoreError IO Contact getContactRec_ contactId c = ExceptT $ do chatTags <- getDirectChatTags db contactId @@ -116,9 +116,9 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id - WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 + WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 |] - (userId, contactId) + (userId, contactId, CSActive) toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL)) = let profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, peerType, preferences, localAlias} @@ -163,8 +163,9 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do JOIN group_members mu ON g.group_id = mu.group_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + AND mu.member_status NOT IN (?,?,?) |] - (groupMemberId, userId, userContactId) + (groupMemberId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) liftIO $ bitraverse (addGroupChatTags db) pure gm toGroupAndMember :: Connection -> GroupInfoRow :. GroupMemberRow -> (GroupInfo, GroupMember) toGroupAndMember c (groupInfoRow :. memberRow) = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index a603551934..a1e06e42ee 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -85,6 +85,7 @@ Query: JOIN group_members mu ON g.group_id = mu.group_id JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE m.group_member_id = ? AND g.user_id = ? AND mu.contact_id = ? + AND mu.member_status NOT IN (?,?,?) Plan: SEARCH m USING INTEGER PRIMARY KEY (rowid=?) @@ -392,7 +393,7 @@ Query: c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id - WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 + WHERE c.user_id = ? AND c.contact_id = ? AND c.contact_status = ? AND c.deleted = 0 Plan: SEARCH c USING INTEGER PRIMARY KEY (rowid=?) @@ -615,7 +616,7 @@ Query: created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections - WHERE user_id = ? AND agent_conn_id = ? + WHERE user_id = ? AND agent_conn_id = ? AND conn_status != ? Plan: SEARCH connections USING INDEX sqlite_autoindex_connections_1 (agent_conn_id=?) diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index ffac4a8f6f..2ab46ee657 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -148,6 +148,7 @@ chatGroupTests = do it "sends and updates profile when creating contact" testMemberContactProfileUpdate it "re-create member contact after deletion, many groups" testRecreateMemberContactManyGroups it "manually accept contact with group member" testMemberContactAccept + it "manually accept contact with group member incognito" testMemberContactAcceptIncognito describe "group message forwarding" $ do it "forward messages between invitee and introduced (x.msg.new)" testGroupMsgForward it "forward reports to moderators, don't forward to members (x.msg.new, MCReport)" testGroupMsgForwardReport @@ -159,6 +160,9 @@ chatGroupTests = do it "forward role change (x.grp.mem.role)" testGroupMsgForwardChangeRole it "forward new member announcement (x.grp.mem.new)" testGroupMsgForwardNewMember it "forward member leaving (x.grp.leave)" testGroupMsgForwardLeave + it "forward member removal (x.grp.mem.del)" testGroupMsgForwardMemberRemoval + it "forward admin removal (x.grp.mem.del, relay forwards it was removed)" testGroupMsgForwardAdminRemoval + it "forward group deletion (x.grp.del)" testGroupMsgForwardGroupDeletion describe "group history" $ do it "text messages" testGroupHistory it "history is sent when joining via group link" testGroupHistoryGroupLink @@ -208,6 +212,9 @@ chatGroupTests = do it "should forward group wide message (x.grp.info) to all members, including in review" testScopedSupportForwardAll it "should not forward messages between support scopes" testScopedSupportDontForwardBetweenScopes it "should forward file inside support scope" testScopedSupportForwardFile + it "should forward member removal in support scope in review (x.grp.mem.del)" testScopedSupportForwardMemberRemoval + it "should forward admin removal in support scope in review (x.grp.mem.del, relay forwards it was removed)" testScopedSupportForwardAdminRemoval + it "should forward group deletion in support scope in review (x.grp.del)" testScopedSupportForwardGroupDeletion it "should send messages to admins and members" testSupportCLISendCommand it "should correctly maintain unread stats for support chats on reading chat items" testScopedSupportUnreadStatsOnRead it "should correctly maintain unread stats for support chats on deleting chat items" testScopedSupportUnreadStatsOnDelete @@ -4640,6 +4647,109 @@ testMemberContactAccept = bob <##> cath +testMemberContactAcceptIncognito :: HasCallStack => TestParams -> IO () +testMemberContactAcceptIncognito = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + -- create group, bob joins incognito + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + bob ##> ("/c i " <> gLink) + bobIncognito <- getTermLine bob + bob <## "connection request sent incognito!" + alice <## (bobIncognito <> ": accepting request to join group #team...") + concurrentlyN_ + [ alice <## ("#team: " <> bobIncognito <> " joined the group"), + do + bob <## "#team: joining the group..." + bob <## ("#team: you joined the group incognito as " <> bobIncognito) + ] + -- cath joins incognito + cath ##> ("/c i " <> gLink) + cathIncognito <- getTermLine cath + cath <## "connection request sent incognito!" + alice <## (cathIncognito <> ": accepting request to join group #team...") + concurrentlyN_ + [ alice <## ("#team: " <> cathIncognito <> " joined the group"), + do + cath <## "#team: joining the group..." + cath <## ("#team: you joined the group incognito as " <> cathIncognito) + cath <## ("#team: member " <> bobIncognito <> " is connected"), + do + bob <## ("#team: alice added " <> cathIncognito <> " to the group (connecting...)") + bob <## ("#team: new member " <> cathIncognito <> " is connected") + ] + + threadDelay 1000000 + + -- bob and cath connect + bob ##> "/_create member contact #1 3" + bob <## ("contact for member #team " <> cathIncognito <> " is created") + + bob ##> "/_invite member contact @2" + bob <## ("sent invitation to connect directly to member #team " <> cathIncognito) + cath <## ("#team " <> bobIncognito <> " requests to create direct contact with you") + cath <## ("to accept: /accept_member_contact @" <> bobIncognito) + cath <## ("to reject: /delete @" <> bobIncognito <> " (the sender will NOT be notified)") + + -- check correct incognito profiles are used + bob @@@ [("@" <> cathIncognito, "chat banner"), ("#team", "connected")] + + bob ##> ("/i " <> cathIncognito) + bob <## "contact ID: 2" + bob <##. "receiving messages via" + bob <## ("you've shared incognito profile with this contact: " <> bobIncognito) + bob <## "connection not verified, use /code command to see security code" + bob <## currentChatVRangeInfo + + cath @@@ [("@" <> bobIncognito, "requested connection from group team"), ("#team", "started direct connection with you")] + cath #$> ("/_get chat @2 count=1", chat, [(0, "requested connection from group team")]) + + cath ##> ("/i " <> bobIncognito) + cath <## "contact ID: 2" + cath <## ("you've shared incognito profile with this contact: " <> cathIncognito) + cath <## "connection not verified, use /code command to see security code" + cath <## currentChatVRangeInfo + + -- accept connection + cath ##> ("/accept_member_contact @" <> bobIncognito) + cath <## ("contact " <> bobIncognito <> " is accepted, starting connection") + _ <- getTermLine bob + _ <- getTermLine cath + concurrentlyN_ + [ do + bob <## (cathIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## ("use /i " <> cathIncognito <> " to print out this incognito profile again"), + do + cath <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> cathIncognito) + cath <## ("use /i " <> bobIncognito <> " to print out this incognito profile again") + ] + + bob ?#> ("@" <> cathIncognito <> " hi") + cath ?<# (bobIncognito <> "> hi") + cath ?#> ("@" <> bobIncognito <> " hey") + bob ?<# (cathIncognito <> "> hey") + + -- if group is deleted, bob and cath keep contact with each other + alice ##> "/d #team" + concurrentlyN_ + [ alice <## "#team: you deleted the group", + do + bob <## "#team: alice deleted the group" + bob <## "use /d #team to delete the local copy of the group", + do + cath <## "#team: alice deleted the group" + cath <## "use /d #team to delete the local copy of the group" + ] + + bob ?#> ("@" <> cathIncognito <> " hi") + cath ?<# (bobIncognito <> "> hi") + cath ?#> ("@" <> bobIncognito <> " hey") + bob ?<# (cathIncognito <> "> hey") + testGroupMsgForward :: HasCallStack => TestParams -> IO () testGroupMsgForward = testChat3 aliceProfile bobProfile cathProfile $ @@ -4951,6 +5061,108 @@ testGroupMsgForwardLeave = alice <## "#team: bob left the group" cath <## "#team: bob left the group" +testGroupMsgForwardMemberRemoval :: HasCallStack => TestParams -> IO () +testGroupMsgForwardMemberRemoval = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3' "team" alice (bob, GRAdmin) (cath, GRMember) + setupGroupForwarding alice bob cath + + -- remove member + bob ##> "/rm team cath" + concurrentlyN_ + [ bob <## "#team: you removed cath from the group", + alice <## "#team: bob removed cath from the group", + do + cath <## "#team: bob removed you from the group" + cath <## "use /d #team to delete the group" + ] + bob #> "#team hi" + concurrently_ + (alice <# "#team bob> hi") + (cath "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath "#team hello" + cath <## "bad chat command: not current member" + +testGroupMsgForwardAdminRemoval :: HasCallStack => TestParams -> IO () +testGroupMsgForwardAdminRemoval = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3' "team" alice (bob, GROwner) (cath, GRMember) + setupGroupForwarding alice bob cath + + -- alice forwards messages between bob and cath + bob #> "#team hi there" + alice <# "#team bob> hi there" + cath <# "#team bob> hi there [>>]" + + cath #> "#team hey" + alice <# "#team cath> hey" + bob <# "#team cath> hey [>>]" + + -- if alice is removed, she forwards message of her own removal + bob ##> "/rm team alice" + concurrentlyN_ + [ bob <## "#team: you removed alice from the group", + do + alice <## "#team: bob removed you from the group" + alice <## "use /d #team to delete the group", + cath <## "#team: bob removed alice from the group" + ] + + -- there is no forwarding admin anymore between bob and cath, so messages don't get delivered + -- (this is not a desired behavior, just a test demonstration/proof of current implementation) + bob #> "#team hi" + concurrently_ + (cath "#team hello" + concurrently_ + (bob "#team hello" + alice <## "bad chat command: not current member" + +testGroupMsgForwardGroupDeletion :: HasCallStack => TestParams -> IO () +testGroupMsgForwardGroupDeletion = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3' "team" alice (bob, GROwner) (cath, GRMember) + setupGroupForwarding alice bob cath + + -- alice forwards messages between bob and cath + bob #> "#team hi there" + alice <# "#team bob> hi there" + cath <# "#team bob> hi there [>>]" + + cath #> "#team hey" + alice <# "#team cath> hey" + bob <# "#team cath> hey [>>]" + + -- if bob deletes the group, alice forwards it to cath + bob ##> "/d #team" + concurrentlyN_ + [ bob <## "#team: you deleted the group", + do + alice <## "#team: bob deleted the group" + alice <## "use /d #team to delete the local copy of the group", + do + cath <## "#team: bob deleted the group" + cath <## "use /d #team to delete the local copy of the group" + ] + + alice ##> "/groups" + alice <## "#team (group deleted, delete local copy: /d #team)" + bob ##> "/groups" + bob <## "you have no groups!" + bob <## "to create: /g " + cath ##> "/groups" + cath <## "#team (group deleted, delete local copy: /d #team)" + testGroupHistory :: HasCallStack => TestParams -> IO () testGroupHistory = testChat3 aliceProfile bobProfile cathProfile $ @@ -7369,6 +7581,172 @@ testScopedSupportForwardFile = ] dan <## "completed receiving file 1 (test.jpg) from bob" +testScopedSupportForwardMemberRemoval :: HasCallStack => TestParams -> IO () +testScopedSupportForwardMemberRemoval = + testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $ + \alice bob cath dan eve -> do + createGroup4 "team" alice (bob, GRAdmin) (cath, GRMember) (dan, GRModerator) + setupReviewForward alice bob cath dan eve + + -- bob removes eve, eve and dan receive member removal message + bob ##> "/_remove #1 5" + concurrentlyN_ + [ bob <## "#team: you removed eve from the group", + alice <## "#team: bob removed eve from the group", + dan <## "#team: bob removed eve from the group", + do + eve <## "#team: bob removed you from the group" + eve <## "use /d #team to delete the group" + ] + + alice ##> "#team (support: eve) hi" + alice <## "bad chat command: support member not current or pending" + bob ##> "#team (support: eve) hi" + bob <## "bad chat command: support member not current or pending" + dan ##> "#team (support: eve) hi" + dan <## "bad chat command: support member not current or pending" + eve ##> "/groups" + eve <## "#team (you are removed, delete local copy: /d #team)" + +setupReviewForward :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> IO () +setupReviewForward alice bob cath dan eve = do + alice ##> "/set admission review #team all" + alice <## "changed member admission rules" + concurrentlyN_ + [ do + bob <## "alice updated group #team:" + bob <## "changed member admission rules", + do + cath <## "alice updated group #team:" + cath <## "changed member admission rules", + do + dan <## "alice updated group #team:" + dan <## "changed member admission rules" + ] + + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + eve ##> ("/c " <> gLink) + eve <## "connection request sent!" + alice <## "eve (Eve): accepting request to join group #team..." + concurrentlyN_ + [ alice <## "#team: eve connected and pending review", + eve + <### [ "#team: alice accepted you to the group, pending review", + "#team: joining the group...", + "#team: you joined the group, connecting to group moderators for admission to group", + "#team: member bob (Bob) is connected", + "#team: member dan (Daniel) is connected" + ], + do + bob <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + bob <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member", + do + dan <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 to accept member" + dan <## "#team: new member eve is connected and pending review, use /_accept member #1 5 to accept member" + ] + + setupGroupForwarding alice bob eve + setupGroupForwarding alice bob dan + + -- alice forwards messages between bob and eve, bob and dan + eve #> "#team (support) 3" + [alice, dan] *<# "#team (support: eve) eve> 3" + bob <# "#team (support: eve) eve> 3 [>>]" + + dan #> "#team (support: eve) 4" + alice <# "#team (support: eve) dan> 4" + bob <# "#team (support: eve) dan> 4 [>>]" + eve <# "#team (support) dan> 4" + + bob #> "#team (support: eve) 5" + alice <# "#team (support: eve) bob> 5" + dan <# "#team (support: eve) bob> 5 [>>]" + eve <# "#team (support) bob> 5 [>>]" + +testScopedSupportForwardAdminRemoval :: HasCallStack => TestParams -> IO () +testScopedSupportForwardAdminRemoval = + testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $ + \alice bob cath dan eve -> do + createGroup4 "team" alice (bob, GROwner) (cath, GRMember) (dan, GRModerator) + setupReviewForward alice bob cath dan eve + + -- bob removes eve, eve and dan receive member removal message + bob ##> "/rm team alice" + concurrentlyN_ + [ bob <## "#team: you removed alice from the group", + do + alice <## "#team: bob removed you from the group" + alice <## "use /d #team to delete the group", + cath <## "#team: bob removed alice from the group", + dan <## "#team: bob removed alice from the group", + eve <## "#team: bob removed alice from the group" + ] + + -- there is no forwarding admin anymore between bob and cath, + -- so messages between bob and eve, bob and dan don't get delivered + -- (this is not a desired behavior, just a test demonstration/proof of current implementation) + eve #> "#team (support) hi" + concurrentlyN_ + [ dan <# "#team (support: eve) eve> hi", + (bob "#team (support: eve) hey" + concurrentlyN_ + [ eve <# "#team (support) dan> hey", + (bob "#team (support: eve) hello" + concurrentlyN_ + [ (eve "/groups" + alice <## "#team (you are removed, delete local copy: /d #team)" + +testScopedSupportForwardGroupDeletion :: HasCallStack => TestParams -> IO () +testScopedSupportForwardGroupDeletion = + testChat5 aliceProfile bobProfile cathProfile danProfile eveProfile $ + \alice bob cath dan eve -> do + createGroup4 "team" alice (bob, GROwner) (cath, GRMember) (dan, GRModerator) + setupReviewForward alice bob cath dan eve + + -- if bob deletes the group, alice forwards it to eve and dan + bob ##> "/d #team" + concurrentlyN_ + [ bob <## "#team: you deleted the group", + do + alice <## "#team: bob deleted the group" + alice <## "use /d #team to delete the local copy of the group", + do + cath <## "#team: bob deleted the group" + cath <## "use /d #team to delete the local copy of the group", + do + dan <## "#team: bob deleted the group" + dan <## "use /d #team to delete the local copy of the group", + do + eve <## "#team: bob deleted the group" + eve <## "use /d #team to delete the local copy of the group" + ] + + alice ##> "/groups" + alice <## "#team (group deleted, delete local copy: /d #team)" + bob ##> "/groups" + bob <## "you have no groups!" + bob <## "to create: /g " + cath ##> "/groups" + cath <## "#team (group deleted, delete local copy: /d #team)" + dan ##> "/groups" + dan <## "#team (group deleted, delete local copy: /d #team)" + eve ##> "/groups" + eve <## "#team (group deleted, delete local copy: /d #team)" + testSupportCLISendCommand :: HasCallStack => TestParams -> IO () testSupportCLISendCommand = testChat2 aliceProfile bobProfile $ \alice bob -> do diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index e246a7a989..ecf923901f 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -378,9 +378,18 @@ testSanitizeUri = describe "sanitizeUri" $ do "https://www.youtube.com/watch?v=abc&t=123" `sanitized` Nothing "https://www.youtube.com/watch?ref=456&v=abc&t=123" `sanitized` Just "https://www.youtube.com/watch?v=abc&t=123" it "should only allow whitelisted parameters if path contains IDs" $ do - "https://example.com/page/a123?name=abc" `sanitized` Just "https://example.com/page/a123" "https://youtu.be/a123?si=456" `sanitized` Just "https://youtu.be/a123" "https://youtu.be/a123?t=456" `sanitized` Nothing "https://youtu.be/a123?si=456&t=789" `sanitized` Just "https://youtu.be/a123?t=789" + it "should allow some parameters in safe mode, but sanitize in unsafe" $ do + "https://example.com/page/a123?source=abc" `eagerSanitized` Just "https://example.com/page/a123" + "https://example.com/page/a123?source=abc" `safeSanitized` Nothing -- source is in unsafe blacklist + "https://example.com/page/a123?name=abc" `eagerSanitized` Just "https://example.com/page/a123" + "https://example.com/page/a123?name=abc" `safeSanitized` Nothing -- name is not in a whitelist where - s `sanitized` res = (U.serializeURIRef' <$$> (sanitizeUri <$> parseUri s)) `shouldBe` Right res + s `eagerSanitized` res = sanitized_ False s res + s `safeSanitized` res = sanitized_ True s res + s `sanitized` res = do + s `eagerSanitized` res + s `safeSanitized` res + sanitized_ safe s res = (U.serializeURIRef' <$$> (sanitizeUri safe <$> parseUri s)) `shouldBe` Right res