mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-03 04:42:21 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -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: |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-2
@@ -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;
|
||||
}
|
||||
|
||||
+13
-6
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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()
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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()) {
|
||||
|
||||
+2
-2
@@ -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))
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
@@ -58,6 +58,7 @@ class ProcessedErrors <T: AgentErrorType>(val interval: Long) {
|
||||
text = generalGetString(MR.strings.agent_internal_error_desc).format(error.internalErr),
|
||||
)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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 }
|
||||
|
||||
+2
-2
@@ -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)
|
||||
}
|
||||
|
||||
+4
-2
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3111,6 +3111,7 @@ A_QUEUE:
|
||||
- "invitation"
|
||||
- "group"
|
||||
- "channel"
|
||||
- "relay"
|
||||
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 931c533a3ddb86345e95ac54e24df5474d9a349b
|
||||
tag: a2d35281b2e20022e75a4dcbecf16a7970d52e8f
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
+25
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=?)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user