Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2025-08-18 17:44:44 +01:00
63 changed files with 801 additions and 201 deletions
+1 -1
View File
@@ -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: |
+1
View File
@@ -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
@@ -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 {
@@ -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
@@ -7239,6 +7239,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Опростен режим инкогнито</target>
@@ -7002,6 +7002,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Zjednodušený inkognito režim</target>
@@ -7714,6 +7714,10 @@ chat item action</note>
<target>Die SimpleX-Protokolle wurden von Trail of Bits überprüft.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Vereinfachter Inkognito-Modus</target>
@@ -7714,6 +7714,11 @@ chat item action</note>
<target>SimpleX protocols reviewed by Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<target>SimpleX relay link</target>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Simplified incognito mode</target>
@@ -7710,6 +7710,10 @@ chat item action</note>
<target>Protocolos de SimpleX auditados por Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Modo incógnito simplificado</target>
@@ -6967,6 +6967,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<note>No comment provided by engineer.</note>
@@ -7570,6 +7570,10 @@ chat item action</note>
<target>Protocoles SimpleX audité par Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Mode incognito simplifié</target>
@@ -7714,6 +7714,10 @@ chat item action</note>
<target>A SimpleX protokollokat a Trail of Bits auditálta.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Egyszerűsített inkognitómód</target>
@@ -7714,6 +7714,10 @@ chat item action</note>
<target>Protocolli di SimpleX esaminati da Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Modalità incognito semplificata</target>
@@ -7045,6 +7045,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>シークレットモードの簡素化</target>
@@ -7654,6 +7654,10 @@ chat item action</note>
<target>SimpleX-protocollen beoordeeld door Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Vereenvoudigde incognitomodus</target>
@@ -7464,6 +7464,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Uproszczony tryb incognito</target>
@@ -7713,6 +7713,10 @@ chat item action</note>
<target>Аудит SimpleX протоколов от Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Упрощенный режим Инкогнито</target>
@@ -6941,6 +6941,10 @@ chat item action</note>
<source>SimpleX protocols reviewed by Trail of Bits.</source>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<note>No comment provided by engineer.</note>
@@ -7694,6 +7694,10 @@ chat item action</note>
<target>SimpleX protokolleri Trail of Bits tarafından incelenmiştir.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Basitleştirilmiş gizli mod</target>
@@ -7699,6 +7699,10 @@ chat item action</note>
<target>Протоколи SimpleX, розглянуті Trail of Bits.</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>Спрощений режим інкогніто</target>
@@ -7590,6 +7590,10 @@ chat item action</note>
<target>SimpleX 协议由 Trail of Bits 审阅。</target>
<note>No comment provided by engineer.</note>
</trans-unit>
<trans-unit id="SimpleX relay link" xml:space="preserve">
<source>SimpleX relay link</source>
<note>simplex link type</note>
</trans-unit>
<trans-unit id="Simplified incognito mode" xml:space="preserve">
<source>Simplified incognito mode</source>
<target>简化的隐身模式</target>
+2 -2
View File
@@ -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)
+2
View File
@@ -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")
}
}
}
+1 -1
View File
@@ -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);
+3 -2
View File
@@ -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",
@@ -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) {
+4 -1
View File
@@ -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
@@ -709,9 +709,11 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, 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)
}
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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<Long, Int, Boolean>
val remIndex: Int
val rem: ChatItem?
chatItems.value = SnapshotStateList<ChatItem>().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
})
}
@@ -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()
@@ -32,7 +32,7 @@ val databaseBackend: String = if (appPlatform == AppPlatform.ANDROID) "sqlite" e
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
if (size > capacity) removeFirstOrNull()
return super.add(element)
}
}
@@ -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
@@ -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()) {
@@ -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))
@@ -427,7 +427,7 @@ fun showInvalidLinkAlert(uri: String, error: String? = null) {
}
fun sanitizeUri(s: String): Pair<Pair<Boolean, String?>?, String?> {
val parsed = parseSanitizeUri(s)
val parsed = parseSanitizeUri(s, safe = false)
return if (parsed?.uriInfo != null) {
(true to parsed.uriInfo.sanitized) to null
} else {
@@ -58,6 +58,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr),
)
}
else -> {}
}
}
}
@@ -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,
@@ -98,6 +98,7 @@
<string name="simplex_link_invitation">SimpleX one-time invitation</string>
<string name="simplex_link_group">SimpleX group link</string>
<string name="simplex_link_channel">SimpleX channel link</string>
<string name="simplex_link_relay">SimpleX relay link</string>
<string name="simplex_link_connection">via %1$s</string>
<string name="simplex_link_mode">SimpleX links</string>
<string name="simplex_link_mode_description">Description</string>
@@ -171,7 +171,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState<Boolean>) {
// 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 }
@@ -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)
}
@@ -195,9 +195,11 @@ fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, 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)
}
}
}
}
+3 -2
View File
@@ -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 {
+3 -3
View File
@@ -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
@@ -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
+1
View File
@@ -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)
}
}
+1
View File
@@ -3111,6 +3111,7 @@ A_QUEUE:
- "invitation"
- "group"
- "channel"
- "relay"
---
+1 -1
View File
@@ -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
+25
View File
@@ -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?
@@ -38,6 +38,25 @@
</description>
<releases>
<release version="6.4.3.1" date="2025-08-10">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.3.1:</p>
<ul>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
</ul>
<p>New in v6.4-6.4.2:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
</ul>
</description>
</release>
<release version="6.4.2" date="2025-08-02">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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
+4
View File
@@ -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
+130 -98
View File
@@ -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
+57 -24
View File
@@ -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
+6 -6
View File
@@ -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) ""
+13 -9
View File
@@ -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
+2 -2
View File
@@ -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
+6 -5
View File
@@ -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) =
@@ -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=?)
+378
View File
@@ -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 <name> 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 </)
alice #> "#team hello"
concurrently_
(bob <# "#team alice> hello")
(cath </)
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 </)
(alice </)
cath #> "#team hello"
concurrently_
(bob </)
(alice </)
alice ##> "#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 <name>"
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 <role> to accept member"
bob <## "#team: new member eve is connected and pending review, use /_accept member #1 5 <role> to accept member",
do
dan <## "#team: alice added eve (Eve) to the group (connecting and pending review...), use /_accept member #1 5 <role> to accept member"
dan <## "#team: new member eve is connected and pending review, use /_accept member #1 5 <role> 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 </),
(alice </)
]
dan #> "#team (support: eve) hey"
concurrentlyN_
[ eve <# "#team (support) dan> hey",
(bob </),
(alice </)
]
bob #> "#team (support: eve) hello"
concurrentlyN_
[ (eve </),
(dan </),
(alice </)
]
alice ##> "/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 <name>"
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
+11 -2
View File
@@ -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