diff --git a/.github/workflows/benchmarks-check.yml b/.github/workflows/benchmarks-check.yml index 91941fd98f9..336eaa3ac78 100644 --- a/.github/workflows/benchmarks-check.yml +++ b/.github/workflows/benchmarks-check.yml @@ -59,7 +59,7 @@ jobs: unset REPORT_FULL_PATH_REF - name: Install bencher - uses: bencherdev/bencher@v0.4.17 + uses: bencherdev/bencher@main - name: Run Bencher on PR if: github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request' diff --git a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml index f6dbeda7d91..9dfb71c505a 100644 --- a/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml +++ b/.github/workflows/cherry-pick-pr-to-newer-release-cycle.yml @@ -43,7 +43,7 @@ jobs: fetch-depth: 0 - name: Cherry pick to `develop` - uses: wireapp/action-auto-cherry-pick@v1.0.0 + uses: wireapp/action-auto-cherry-pick@v1.0.2 with: target-branch: develop pr-title-suffix: '🍒' diff --git a/.github/workflows/generate-dokka.yml b/.github/workflows/generate-dokka.yml index 30c7414c965..01e2e6e5aaa 100644 --- a/.github/workflows/generate-dokka.yml +++ b/.github/workflows/generate-dokka.yml @@ -25,7 +25,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy docs 🚀 - uses: JamesIves/github-pages-deploy-action@v4.2.5 + uses: JamesIves/github-pages-deploy-action@v4.6.9 with: branch: gh-pages clean: false diff --git a/.github/workflows/gradle-android-instrumented-tests.yml b/.github/workflows/gradle-android-instrumented-tests.yml index 365b30453cc..434a797d9b9 100644 --- a/.github/workflows/gradle-android-instrumented-tests.yml +++ b/.github/workflows/gradle-android-instrumented-tests.yml @@ -104,7 +104,7 @@ jobs: ./**/build/outputs/androidTest-results/**/*.xml - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0 + uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1 if: always() with: files: | @@ -125,7 +125,7 @@ jobs: steps: - name: Download tests results - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.7 continue-on-error: true with: name: test-results diff --git a/.github/workflows/gradle-android-unit-tests.yml b/.github/workflows/gradle-android-unit-tests.yml index 409fcd50812..672144cd5aa 100644 --- a/.github/workflows/gradle-android-unit-tests.yml +++ b/.github/workflows/gradle-android-unit-tests.yml @@ -56,7 +56,7 @@ jobs: ./**/build/test-results/**/*.xml - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0 + uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1 if: always() with: files: | @@ -77,7 +77,7 @@ jobs: steps: - name: Download tests results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.1.7 continue-on-error: true with: name: test-results diff --git a/.github/workflows/gradle-ios-tests.yml b/.github/workflows/gradle-ios-tests.yml index 4693d88411a..8745f8bcc8b 100644 --- a/.github/workflows/gradle-ios-tests.yml +++ b/.github/workflows/gradle-ios-tests.yml @@ -60,7 +60,7 @@ jobs: ./**/build/test-results/**/*.xml - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0 + uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1 if: always() with: files: | @@ -81,7 +81,7 @@ jobs: steps: - name: Download tests results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.1.7 continue-on-error: true with: name: test-results diff --git a/.github/workflows/gradle-jvm-tests.yml b/.github/workflows/gradle-jvm-tests.yml index 6335dd09489..295aefb1da5 100644 --- a/.github/workflows/gradle-jvm-tests.yml +++ b/.github/workflows/gradle-jvm-tests.yml @@ -80,14 +80,14 @@ jobs: sudo apt-get install -y python3-pip - name: Publish Unit Test Results - uses: EnricoMi/publish-unit-test-result-action/composite@v2.11.0 + uses: EnricoMi/publish-unit-test-result-action/composite@v2.17.1 if: always() with: files: | **/build/test-results/**/*.xml - name: Upload test report to codecov - uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 with: token: ${{ secrets.CODECOV_TOKEN }} files: "build/reports/kover/report.xml" @@ -112,7 +112,7 @@ jobs: steps: - name: Download tests results - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v4.1.7 continue-on-error: true - name: Display structure of downloaded files run: ls -R diff --git a/.github/workflows/semantic-commit-lint.yml b/.github/workflows/semantic-commit-lint.yml index 88783c2b437..eab76bf3322 100644 --- a/.github/workflows/semantic-commit-lint.yml +++ b/.github/workflows/semantic-commit-lint.yml @@ -19,7 +19,7 @@ jobs: # Please look up the latest version from # https://github.com/amannn/action-semantic-pull-request/releases - name: Run Semantic Commint Linter - uses: amannn/action-semantic-pull-request@v5.5.2 + uses: amannn/action-semantic-pull-request@v5.5.3 with: # Configure which types are allowed. # Default: https://github.com/commitizen/conventional-commit-types diff --git a/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt b/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt index 3819da44213..7d24aa9b05e 100644 --- a/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt +++ b/android/src/main/kotlin/com/wire/kalium/KaliumApplication.kt @@ -25,7 +25,6 @@ import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logic.CoreLogger import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.featureFlags.KaliumConfigs -import com.wire.kalium.logic.sync.ForegroundNotificationDetailsProvider import com.wire.kalium.logic.sync.WrapperWorkerFactory import java.io.File @@ -50,13 +49,7 @@ class KaliumApplication : Application(), Configuration.Provider { override val workManagerConfiguration: Configuration get() { - val myWorkerFactory = WrapperWorkerFactory( - coreLogic, - object : ForegroundNotificationDetailsProvider { - override fun getSmallIconResId(): Int = R.drawable.ic_launcher_foreground - } - ) - + val myWorkerFactory = WrapperWorkerFactory(coreLogic) { R.drawable.ic_launcher_foreground } return Configuration.Builder() .setWorkerFactory(myWorkerFactory) .build() diff --git a/build.gradle.kts b/build.gradle.kts index aff4e98c0e8..c5387e7e8e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -143,3 +143,82 @@ moduleGraphConfig { readmePath.set("./README.md") heading.set("#### Dependency Graph") } + +tasks.register("runAllUnitTests") { + description = "Runs all Unit Tests." + + rootProject.subprojects { + if (tasks.findByName("testDebugUnitTest") != null) { + println("Adding $name to runUnitTests") + dependsOn(":$name:testDebugUnitTest") + } + if (name != "cryptography") { + if (tasks.findByName("jvmTest") != null) { + println("Adding $name to jvmTest") + dependsOn(":$name:jvmTest") + } + } + } +} + +tasks.register("aggregateTestResults") { + description = "Aggregates all Unit Test results into a single report." + + doLast { + val testResultsDir = rootProject.layout.buildDirectory.dir("testResults").get().asFile + testResultsDir.deleteRecursively() + testResultsDir.mkdirs() + + val indexHtmlFile = File(testResultsDir, "index.html") + indexHtmlFile.writeText( + """ + + + Aggregated Test Reports + + +

Aggregated Test Reports

+ + + + """.trimIndent() + ) + + // Print the location of the aggregated test results directory + // relative to the current terminal working dir + val currentWorkingDir = File(System.getProperty("user.dir")) + val relativePath = testResultsDir.relativeTo(currentWorkingDir).path + println("Aggregated test reports are available at: $relativePath") + } +} + +tasks.wrapper { + distributionType = Wrapper.DistributionType.ALL +} diff --git a/calling/build.gradle.kts b/calling/build.gradle.kts index 2851cc4187d..b3587ad4842 100644 --- a/calling/build.gradle.kts +++ b/calling/build.gradle.kts @@ -67,3 +67,15 @@ kotlin { } } } + +android { + defaultConfig { + ndk { + abiFilters.apply { + add("armeabi-v7a") + add("arm64-v8a") + add("x86_64") + } + } + } +} diff --git a/calling/consumer-proguard-rules.pro b/calling/consumer-proguard-rules.pro index eed5182980c..3825933e243 100644 --- a/calling/consumer-proguard-rules.pro +++ b/calling/consumer-proguard-rules.pro @@ -6,6 +6,9 @@ -keep class com.waz.call.CaptureDevice { *; } -keep class com.waz.media.manager.** { *; } -keep class com.waz.service.call.** { *; } +-dontwarn org.webrtc.CalledByNative +-dontwarn org.webrtc.JniCommon +-dontwarn org.webrtc.audio.AudioDeviceModule # Avs SoundLink -keep class com.waz.soundlink.SoundLinkAPI { *; } diff --git a/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt b/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt index 479d858a0ab..242b1b5a27a 100644 --- a/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt +++ b/calling/src/commonJvmAndroid/kotlin/com.wire.kalium.calling/Calling.kt @@ -214,6 +214,6 @@ interface Calling : Library { ) companion object { - val INSTANCE by lazy { Native.load("avs", Calling::class.java)!! } + val INSTANCE: Calling by lazy { Native.load("avs", Calling::class.java)!! } } } diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 2aebe850ecb..244c656e822 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -75,6 +75,8 @@ kotlin { implementation(libs.coroutines.core) implementation(libs.ktxDateTime) implementation(libs.mordant) + implementation(libs.ktxSerialization) + implementation(libs.ktxIO) } } val jvmMain by getting { diff --git a/cli/src/appleMain/kotlin/main.kt b/cli/src/appleMain/kotlin/main.kt index a486a8d7298..fd6410e1da8 100644 --- a/cli/src/appleMain/kotlin/main.kt +++ b/cli/src/appleMain/kotlin/main.kt @@ -21,6 +21,7 @@ import com.wire.kalium.cli.CLIApplication import com.wire.kalium.cli.commands.AddMemberToGroupCommand import com.wire.kalium.cli.commands.CreateGroupCommand import com.wire.kalium.cli.commands.DeleteClientCommand +import com.wire.kalium.cli.commands.GenerateEventsCommand import com.wire.kalium.cli.commands.ListenGroupCommand import com.wire.kalium.cli.commands.LoginCommand import com.wire.kalium.cli.commands.MarkAsReadCommand @@ -39,6 +40,7 @@ fun main(args: Array) = CLIApplication().subcommands( RefillKeyPackagesCommand(), MarkAsReadCommand(), InteractiveCommand(), - UpdateSupportedProtocolsCommand() + UpdateSupportedProtocolsCommand(), + GenerateEventsCommand() ) ).main(args) diff --git a/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt new file mode 100644 index 00000000000..c3adb8a5672 --- /dev/null +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt @@ -0,0 +1,99 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.cli.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.PrintMessage +import com.github.ajalt.clikt.core.requireObject +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.int +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedClientID +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.UserSessionScope +import com.wire.kalium.logic.data.event.EventGenerator +import com.wire.kalium.logic.functional.getOrFail +import com.wire.kalium.network.api.authenticated.notification.NotificationResponse +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import kotlinx.io.Buffer +import kotlinx.io.buffered +import kotlinx.io.files.Path +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.io.files.SystemFileSystem +import kotlinx.io.writeString + +class GenerateEventsCommand : CliktCommand(name = "generate-events") { + + private val userSession by requireObject() + private val targetUserId: String by option(help = "Target User which events will be generator for.").required() + private val targetClientId: String by option(help = "Target Client which events will be generator for.").required() + private val conversationId: String by option(help = "Target conversation which which will receive the events").required() + private val eventLimit: Int by argument("Number of events to generate").int() + private val outputFile: String by argument("Output file for the generated events") + + private var json = Json { + prettyPrint = true + } + + override fun run() = runBlocking { + val selfUserId = userSession.users.getSelfUser().first().id + val selfClientId = userSession.clientIdProvider().getOrFail { throw PrintMessage("No self client is registered") } + val targetUserId = UserId(value = targetUserId, domain = selfUserId.domain) + val targetClientId = ClientId(targetClientId) + + userSession.debug.establishSession( + userId = targetUserId, + clientId = targetClientId + ) + val generator = EventGenerator( + selfClient = QualifiedClientID( + clientId = selfClientId, + userId = selfUserId + ), + targetClient = QualifiedClientID( + clientId = targetClientId, + userId = targetUserId + ), + proteusClient = userSession.proteusClientProvider.getOrCreate() + ) + val events = generator.generateEvents( + limit = eventLimit, + conversationId = ConversationId(conversationId, domain = selfUserId.domain) + ) + val response = NotificationResponse( + time = Clock.System.now().toString(), + hasMore = false, + notifications = events.toList() + ) + + val sink = SystemFileSystem.sink(Path(outputFile)).buffered() + val buffer = Buffer() + buffer.writeString(json.encodeToString(response)) + sink.write(buffer, buffer.size) + sink.close() + + echo("Generated $eventLimit event(s) written into $outputFile") + } +} diff --git a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt index 79404e19a8f..54c196bf100 100644 --- a/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt +++ b/cli/src/jvmMain/kotlin/com/wire/kalium/cli/main.kt @@ -26,6 +26,7 @@ import com.wire.kalium.cli.commands.ListenGroupCommand import com.wire.kalium.cli.commands.LoginCommand import com.wire.kalium.cli.commands.MarkAsReadCommand import com.wire.kalium.cli.commands.ConsoleCommand +import com.wire.kalium.cli.commands.GenerateEventsCommand import com.wire.kalium.cli.commands.RefillKeyPackagesCommand import com.wire.kalium.cli.commands.RemoveMemberFromGroupCommand import com.wire.kalium.cli.commands.UpdateSupportedProtocolsCommand @@ -40,6 +41,7 @@ fun main(args: Array) = CLIApplication().subcommands( ConsoleCommand(), RefillKeyPackagesCommand(), MarkAsReadCommand(), - UpdateSupportedProtocolsCommand() + UpdateSupportedProtocolsCommand(), + GenerateEventsCommand() ) ).main(args) diff --git a/cryptography/build.gradle.kts b/cryptography/build.gradle.kts index 3d86ef000c6..b4eda1de6ac 100644 --- a/cryptography/build.gradle.kts +++ b/cryptography/build.gradle.kts @@ -110,4 +110,13 @@ android { testOptions.unitTests.all { it.enabled = false } + defaultConfig { + ndk { + abiFilters.apply { + add("armeabi-v7a") + add("arm64-v8a") + add("x86_64") + } + } + } } diff --git a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index 4414b91a749..3af5b2e36af 100644 --- a/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/androidMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -110,18 +110,24 @@ class ProteusClientCryptoBoxImpl constructor( } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray = lock.withLock { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T = lock.withLock { withContext(defaultContext) { val session = box.tryGetSession(sessionId.value) wrapException { if (session != null) { val decryptedMessage = session.decrypt(message) - session.save() - decryptedMessage + handleDecryptedMessage(decryptedMessage).also { + session.save() + } } else { val result = box.initSessionFromMessage(sessionId.value, message) - result.session.save() - result.message + handleDecryptedMessage(result.message).also { + result.session.save() + } } } } @@ -181,9 +187,9 @@ class ProteusClientCryptoBoxImpl constructor( try { return b() } catch (e: CryptoException) { - throw ProteusException(e.message, fromCryptoException(e), e.cause) + throw ProteusException(e.message, fromCryptoException(e), e.code.ordinal, e.cause) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) } } diff --git a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt index 27f355f6151..3e92d06cef7 100644 --- a/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/appleMain/kotlin/com/wire/kalium/cryptography/ProteusClientCoreCryptoImpl.kt @@ -31,7 +31,8 @@ import platform.Foundation.URLByAppendingPathComponent @Suppress("TooManyFunctions") class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: CoreCrypto) : ProteusClient { @Suppress("EmptyFunctionBlock") - override suspend fun close() {} + override suspend fun close() { + } override fun getIdentity(): ByteArray { return ByteArray(0) @@ -72,18 +73,21 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, toUByteList(preKeyCrypto.encodedData.decodeBase64Bytes())) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val sessionExists = doesSessionExist(sessionId) return wrapException { - if (sessionExists) { - val decryptedMessage = toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message))) - coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage + val decryptedMessage = if (sessionExists) { + toByteArray(coreCrypto.proteusDecrypt(sessionId.value, toUByteList(message))) } else { - val decryptedMessage = toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message))) + toByteArray(coreCrypto.proteusSessionFromMessage(sessionId.value, toUByteList(message))) + } + handleDecryptedMessage(decryptedMessage).also { coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage } } } @@ -98,7 +102,10 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map { return wrapException { - coreCrypto.proteusEncryptBatched(sessionIds.map { it.value }, toUByteList((message))).mapNotNull { entry -> + coreCrypto.proteusEncryptBatched( + sessionId = sessionIds.map { it.value }, + plaintext = toUByteList((message)) + ).mapNotNull { entry -> CryptoSessionId.fromEncodedString(entry.key)?.let { sessionId -> sessionId to toByteArray(entry.value) } @@ -126,14 +133,14 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co } @Suppress("TooGenericExceptionCaught") - private fun wrapException(b: () -> T): T { + private inline fun wrapException(b: () -> T): T { try { return b() } catch (e: CryptoException) { // TODO underlying proteus error is not exposed atm - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, null) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, null) } } @@ -187,9 +194,9 @@ class ProteusClientCoreCryptoImpl private constructor(private val coreCrypto: Co coreCrypto.proteusInit() return ProteusClientCoreCryptoImpl(coreCrypto) } catch (e: CryptoException) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) } } } diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt index eda576f6d24..89c70247f9e 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/CoreCryptoCentral.kt @@ -33,9 +33,7 @@ actual suspend fun coreCryptoCentral( File(rootDir).mkdirs() val coreCrypto = coreCryptoDeferredInit( path = path, - key = databaseKey, - ciphersuites = emptyList(), - nbKeyPackage = null + key = databaseKey ) coreCrypto.setCallbacks(Callbacks()) return CoreCryptoCentralImpl( @@ -45,14 +43,13 @@ actual suspend fun coreCryptoCentral( } private class Callbacks : CoreCryptoCallbacks { - - override fun authorize(conversationId: ByteArray, clientId: ClientId): Boolean { + override suspend fun authorize(conversationId: ByteArray, clientId: ClientId): Boolean { // We always return true because our BE is currently enforcing that this constraint is always true return true } - override fun clientIsExistingGroupUser( - conversationId: ConversationId, + override suspend fun clientIsExistingGroupUser( + conversationId: ByteArray, clientId: ClientId, existingClients: List, parentConversationClients: List? @@ -61,11 +58,7 @@ private class Callbacks : CoreCryptoCallbacks { return true } - override fun userAuthorize( - conversationId: ConversationId, - externalClientId: ClientId, - existingClients: List - ): Boolean { + override suspend fun userAuthorize(conversationId: ByteArray, externalClientId: ClientId, existingClients: List): Boolean { // We always return true because our BE is currently enforcing that this constraint is always true return true } diff --git a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt index 9fbcd75953e..fdc9bc2b69c 100644 --- a/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt +++ b/cryptography/src/commonJvmAndroid/kotlin/com.wire.kalium.cryptography/ProteusClientCoreCryptoImpl.kt @@ -22,8 +22,11 @@ import com.wire.crypto.CoreCrypto import com.wire.crypto.CoreCryptoException import com.wire.crypto.client.toByteArray import com.wire.kalium.cryptography.exceptions.ProteusException +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import io.ktor.util.decodeBase64Bytes import io.ktor.util.encodeBase64 +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.io.File @Suppress("TooManyFunctions") @@ -31,6 +34,9 @@ class ProteusClientCoreCryptoImpl private constructor( private val coreCrypto: CoreCrypto, ) : ProteusClient { + private val mutex = Mutex() + private val existingSessionsCache = mutableSetOf() + override suspend fun close() { coreCrypto.close() } @@ -46,6 +52,7 @@ class ProteusClientCoreCryptoImpl private constructor( override suspend fun remoteFingerPrint(sessionId: CryptoSessionId): ByteArray = wrapException { coreCrypto.proteusFingerprintRemote(sessionId.value).toByteArray() } + override suspend fun getFingerprintFromPreKey(preKey: PreKeyCrypto): ByteArray = wrapException { coreCrypto.proteusFingerprintPrekeybundle(preKey.encodedData.decodeBase64Bytes()).toByteArray() } @@ -62,9 +69,16 @@ class ProteusClientCoreCryptoImpl private constructor( return wrapException { toPreKey(coreCrypto.proteusLastResortPrekeyId().toInt(), coreCrypto.proteusLastResortPrekey()) } } - override suspend fun doesSessionExist(sessionId: CryptoSessionId): Boolean { - return wrapException { + override suspend fun doesSessionExist(sessionId: CryptoSessionId): Boolean = mutex.withLock { + if (existingSessionsCache.contains(sessionId)) { + return@withLock true + } + wrapException { coreCrypto.proteusSessionExists(sessionId.value) + }.also { exists -> + if (exists) { + existingSessionsCache.add(sessionId) + } } } @@ -72,19 +86,20 @@ class ProteusClientCoreCryptoImpl private constructor( wrapException { coreCrypto.proteusSessionFromPrekey(sessionId.value, preKeyCrypto.encodedData.decodeBase64Bytes()) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val sessionExists = doesSessionExist(sessionId) return wrapException { - if (sessionExists) { - val decryptedMessage = coreCrypto.proteusDecrypt(sessionId.value, message) - coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage + val decryptedMessage = if (sessionExists) { + coreCrypto.proteusDecrypt(sessionId.value, message) } else { - val decryptedMessage = coreCrypto.proteusSessionFromMessage(sessionId.value, message) - coreCrypto.proteusSessionSave(sessionId.value) - decryptedMessage + coreCrypto.proteusSessionFromMessage(sessionId.value, message) } + handleDecryptedMessage(decryptedMessage) } } @@ -119,7 +134,8 @@ class ProteusClientCoreCryptoImpl private constructor( } } - override suspend fun deleteSession(sessionId: CryptoSessionId) { + override suspend fun deleteSession(sessionId: CryptoSessionId) = mutex.withLock { + existingSessionsCache.remove(sessionId) wrapException { coreCrypto.proteusSessionDelete(sessionId.value) } @@ -130,9 +146,15 @@ class ProteusClientCoreCryptoImpl private constructor( try { return b() } catch (e: CoreCryptoException) { - throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e) + val proteusLastErrorCode = coreCrypto.proteusLastErrorCode() + throw ProteusException( + e.message, + ProteusException.fromProteusCode(proteusLastErrorCode.toInt()), + proteusLastErrorCode.toInt(), + e + ) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e) } } @@ -157,30 +179,43 @@ class ProteusClientCoreCryptoImpl private constructor( acc && File(rootDir).resolve(file).deleteRecursively() } - private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { - if (cryptoBoxFilesExists(File(rootDir))) { - kaliumLogger.i("migrating from crypto box at: $rootDir") - coreCrypto.proteusCryptoboxMigrate(rootDir) - kaliumLogger.i("migration successful") - - if (deleteCryptoBoxFiles(rootDir)) { - kaliumLogger.i("successfully deleted old crypto box files") - } else { - kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") - } - } - } - - @Suppress("TooGenericExceptionCaught") + @Suppress("TooGenericExceptionCaught", "ThrowsCount") suspend operator fun invoke(coreCrypto: CoreCrypto, rootDir: String): ProteusClientCoreCryptoImpl { try { migrateFromCryptoBoxIfNecessary(coreCrypto, rootDir) coreCrypto.proteusInit() return ProteusClientCoreCryptoImpl(coreCrypto) + } catch (exception: ProteusStorageMigrationException) { + throw exception } catch (e: CoreCryptoException) { - throw ProteusException(e.message, ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), e.cause) + throw ProteusException( + message = e.message, + code = ProteusException.fromProteusCode(coreCrypto.proteusLastErrorCode().toInt()), + intCode = coreCrypto.proteusLastErrorCode().toInt(), + cause = e.cause + ) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) + } + } + + @Suppress("TooGenericExceptionCaught") + private suspend fun migrateFromCryptoBoxIfNecessary(coreCrypto: CoreCrypto, rootDir: String) { + try { + if (cryptoBoxFilesExists(File(rootDir))) { + kaliumLogger.i("migrating from crypto box at: $rootDir") + coreCrypto.proteusCryptoboxMigrate(rootDir) + kaliumLogger.i("migration successful") + + if (deleteCryptoBoxFiles(rootDir)) { + kaliumLogger.i("successfully deleted old crypto box files") + } else { + kaliumLogger.e("Failed to deleted old crypto box files at $rootDir") + } + } + } catch (exception: Exception) { + kaliumLogger.e("Failed to migrate from crypto box to core crypto, exception: $exception") + throw ProteusStorageMigrationException("Failed to migrate from crypto box at $rootDir", exception) } } } diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt index e57fb8c4fe2..055ecc27841 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/ProteusClient.kt @@ -81,8 +81,18 @@ interface ProteusClient { @Throws(ProteusException::class, CancellationException::class) suspend fun createSession(preKeyCrypto: PreKeyCrypto, sessionId: CryptoSessionId) + /** + * Decrypts a message. + * In case of success, calls [handleDecryptedMessage] with the decrypted bytes. + * @throws ProteusException in case of failure + * @throws CancellationException + */ @Throws(ProteusException::class, CancellationException::class) - suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray + suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T @Throws(ProteusException::class, CancellationException::class) suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray diff --git a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt index ca85a709fd5..547d0deefa9 100644 --- a/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt +++ b/cryptography/src/commonMain/kotlin/com/wire/kalium/cryptography/exceptions/ProteusException.kt @@ -18,9 +18,14 @@ package com.wire.kalium.cryptography.exceptions -class ProteusException(message: String?, val code: Code, cause: Throwable? = null) : Exception(message, cause) { +open class ProteusException(message: String?, val code: Code, val intCode: Int?, cause: Throwable? = null) : Exception(message, cause) { - constructor(message: String?, code: Int, cause: Throwable? = null) : this(message, fromNativeCode(code), cause) + constructor(message: String?, code: Int, cause: Throwable? = null) : this( + message, + fromNativeCode(code), + code, + cause + ) enum class Code { /** @@ -149,6 +154,9 @@ class ProteusException(message: String?, val code: Code, cause: Throwable? = nul } companion object { + + const val SESSION_NOT_FOUND_INT = 2 + @Suppress("MagicNumber") fun fromNativeCode(code: Int): Code { return when (code) { @@ -191,3 +199,6 @@ class ProteusException(message: String?, val code: Code, cause: Throwable? = nul } } } + +class ProteusStorageMigrationException(override val message: String, val rootCause: Throwable? = null) : + ProteusException(message, Int.MIN_VALUE, null) diff --git a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt index ec4f95be564..a27624f024a 100644 --- a/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt +++ b/cryptography/src/commonTest/kotlin/com/wire/kalium/cryptography/ProteusClientTest.kt @@ -1,20 +1,20 @@ - /* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ +/* +* Wire +* Copyright (C) 2024 Wire Swiss GmbH +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see http://www.gnu.org/licenses/. +*/ package com.wire.kalium.cryptography @@ -93,7 +93,7 @@ class ProteusClientTest : BaseProteusClientTest() { val message = "Hi Alice!" val aliceKey = aliceClient.newPreKeys(0, 10).first() val encryptedMessage = bobClient.encryptWithPreKey(message.encodeToByteArray(), aliceKey, aliceSessionId) - val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) + val decryptedMessage = aliceClient.decrypt(encryptedMessage, bobSessionId) { it } assertEquals(message, decryptedMessage.decodeToString()) } @@ -105,11 +105,11 @@ class ProteusClientTest : BaseProteusClientTest() { val aliceKey = aliceClient.newPreKeys(0, 10).first() val message1 = "Hi Alice!" val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} val message2 = "Hi again Alice!" val encryptedMessage2 = bobClient.encrypt(message2.encodeToByteArray(), aliceSessionId) - val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) + val decryptedMessage2 = aliceClient.decrypt(encryptedMessage2, bobSessionId) { it } assertEquals(message2, decryptedMessage2.decodeToString()) } @@ -124,10 +124,10 @@ class ProteusClientTest : BaseProteusClientTest() { val aliceKey = aliceClient.newPreKeys(0, 10).first() val message1 = "Hi Alice!" val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} val exception: ProteusException = assertFailsWith { - aliceClient.decrypt(encryptedMessage1, bobSessionId) + aliceClient.decrypt(encryptedMessage1, bobSessionId) {} } assertEquals(ProteusException.Code.DUPLICATE_MESSAGE, exception.code) } @@ -188,8 +188,44 @@ class ProteusClientTest : BaseProteusClientTest() { } } + // TODO: Implement on CoreCrypto as well once it supports transactions + @IgnoreJS + @IgnoreJvm + @IgnoreIOS + @Test + fun givenNonEncryptedClient_whenThrowingDuringTransaction_thenShouldNotSaveSessionAndBeAbleToDecryptAgain() = runTest { + val aliceRef = createProteusStoreRef(alice.id) + val failedAliceClient = createProteusClient(aliceRef) + val bobClient = createProteusClient(createProteusStoreRef(bob.id)) + + val aliceKey = failedAliceClient.newPreKeys(0, 10).first() + val message1 = "Hi Alice!" + + var decryptedCount = 0 + + val encryptedMessage1 = bobClient.encryptWithPreKey(message1.encodeToByteArray(), aliceKey, aliceSessionId) + try { + failedAliceClient.decrypt(encryptedMessage1, bobSessionId) { + decryptedCount++ + throw NullPointerException("") + } + } catch (ignore: Throwable) { + /** No-op **/ + } + // Assume that the app crashed after decrypting but before saving session. + // Trying to decrypt again should succeed. + + val secondAliceClient = createProteusClient(aliceRef) + + val result = secondAliceClient.decrypt(encryptedMessage1, bobSessionId) { result -> + decryptedCount++ + result + } + assertEquals(message1, result.decodeToString()) + assertEquals(2, decryptedCount) + } + companion object { val PROTEUS_DB_SECRET = ProteusDBSecret("secret") } - } diff --git a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index f07272f107e..cfa765f1e9c 100644 --- a/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/jsMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -80,7 +80,7 @@ class ProteusClientCryptoBoxImpl : ProteusClient { if (preKey != null) { return toPreKey(box.getIdentity().public_key, preKey) } else { - throw ProteusException("Local identity doesn't exist", ProteusException.Code.UNKNOWN_ERROR) + throw ProteusException("Local identity doesn't exist", ProteusException.Code.UNKNOWN_ERROR, null, null) } } @@ -102,12 +102,13 @@ class ProteusClientCryptoBoxImpl : ProteusClient { box.session_from_prekey(sessionId.value, preKeyBundle.toArrayBuffer()).await() } - override suspend fun decrypt( + override suspend fun decrypt( message: ByteArray, - sessionId: CryptoSessionId - ): ByteArray { + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T { val decryptedMessage = box.decrypt(sessionId.value, message.toArrayBuffer()).await() - return Int8Array(decryptedMessage.buffer).unsafeCast() + return handleDecryptedMessage(Int8Array(decryptedMessage.buffer).unsafeCast()) } override suspend fun encrypt( diff --git a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt index 33dbbde607a..125f41ed96b 100644 --- a/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt +++ b/cryptography/src/jvmMain/kotlin/com/wire/kalium/cryptography/ProteusClientCryptoBoxImpl.kt @@ -27,17 +27,13 @@ import java.util.Base64 import kotlin.coroutines.CoroutineContext @Suppress("TooManyFunctions") -class ProteusClientCryptoBoxImpl constructor( +class ProteusClientCryptoBoxImpl( rootDir: String ) : ProteusClient { - private val path: String + private val path: String = rootDir private lateinit var box: CryptoBox - init { - path = rootDir - } - fun openOrCreate() { val directory = File(path) box = wrapException { @@ -84,14 +80,18 @@ class ProteusClientCryptoBoxImpl constructor( wrapException { box.encryptFromPreKeys(sessionId.value, toPreKey(preKeyCrypto), ByteArray(0)) } } - override suspend fun decrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { - return wrapException { box.decrypt(sessionId.value, message) } + override suspend fun decrypt( + message: ByteArray, + sessionId: CryptoSessionId, + handleDecryptedMessage: suspend (decryptedMessage: ByteArray) -> T + ): T = wrapException { + handleDecryptedMessage(box.decrypt(sessionId.value, message)) } override suspend fun encrypt(message: ByteArray, sessionId: CryptoSessionId): ByteArray { return wrapException { box.encryptFromSession(sessionId.value, message) - }?.let { it } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND) + } ?: throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND, ProteusException.SESSION_NOT_FOUND_INT) } override suspend fun encryptBatched(message: ByteArray, sessionIds: List): Map { @@ -121,13 +121,13 @@ class ProteusClientCryptoBoxImpl constructor( } @Suppress("TooGenericExceptionCaught") - private fun wrapException(b: () -> T): T { + private inline fun wrapException(b: () -> T): T { try { return b() } catch (e: CryptoException) { throw ProteusException(e.message, e.code.ordinal, e.cause) } catch (e: Exception) { - throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause) + throw ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause) } } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt index fb0adcd8b76..5bc04b0eee8 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Call.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.call import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID enum class CallStatus { STARTED, @@ -39,7 +40,7 @@ data class Call( val isMuted: Boolean, val isCameraOn: Boolean, val isCbrEnabled: Boolean, - val callerId: String, + val callerId: QualifiedID, val conversationName: String?, val conversationType: Conversation.Type, val callerName: String?, diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt index 7545545157e..0c2f3bb9b79 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallClient.kt @@ -18,21 +18,55 @@ package com.wire.kalium.logic.data.call +import com.wire.kalium.util.serialization.LenientJsonSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder @Serializable data class CallClient( @SerialName("userid") val userId: String, @SerialName("clientid") val clientId: String, - @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false + @SerialName("in_subconv") val isMemberOfSubconversation: Boolean = false, + @SerialName("quality") + @Serializable(with = CallQuality.CallQualityAsIntSerializer::class) + val quality: CallQuality = CallQuality.LOW ) @Serializable data class CallClientList( @SerialName("clients") val clients: List ) { - // TODO(optimization): Use a shared Json instance instead of creating one every time. - fun toJsonString(): String = Json { isLenient = true }.encodeToString(serializer(), this) + fun toJsonString(): String = LenientJsonSerializer.json.encodeToString(serializer(), this) +} + +enum class CallQuality { + ANY, + LOW, + HIGH; + + data object CallQualityAsIntSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("quality", PrimitiveKind.INT).nullable + + override fun serialize(encoder: Encoder, value: CallQuality) { + encoder.encodeInt(value.ordinal) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): CallQuality { + val value = if (decoder.decodeNotNullMark()) decoder.decodeInt() else 0 + return when (value) { + 1 -> LOW + 2 -> HIGH + else -> ANY + } + } + } } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt index 203f744a4e6..dd306c7b382 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallMetadataProfile.kt @@ -20,9 +20,11 @@ package com.wire.kalium.logic.data.call import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.OtherUserMinimized import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType +import kotlinx.datetime.Instant data class CallMetadataProfile( val data: Map @@ -31,6 +33,7 @@ data class CallMetadataProfile( } data class CallMetadata( + val callerId: QualifiedID, val isMuted: Boolean, val isCameraOn: Boolean, val isCbrEnabled: Boolean, @@ -44,11 +47,12 @@ data class CallMetadata( val maxParticipants: Int = 0, // Was used for tracking val protocol: Conversation.ProtocolInfo, val activeSpeakers: Map> = mapOf(), - val users: List = listOf() + val users: List = listOf(), + val screenShareMetadata: CallScreenSharingMetadata = CallScreenSharingMetadata() ) { fun getFullParticipants(): List = participants.map { participant -> val user = users.firstOrNull { it.id == participant.userId } - val isSpeaking = (activeSpeakers[participant.id]?.contains(participant.clientId) ?: false) && !participant.isMuted + val isSpeaking = (activeSpeakers[participant.userId]?.contains(participant.clientId) ?: false) && !participant.isMuted Participant( id = participant.id, clientId = participant.clientId, @@ -59,7 +63,19 @@ data class CallMetadata( isSharingScreen = participant.isSharingScreen, hasEstablishedAudio = participant.hasEstablishedAudio, avatarAssetId = user?.completePicture, - userType = user?.userType ?: UserType.NONE + userType = user?.userType ?: UserType.NONE, + accentId = user?.accentId ?: 0 ) } } + +/** + * [activeScreenShares] - map of user ids that share screen with the start timestamp + * [completedScreenShareDurationInMillis] - total time of already ended screen shares in milliseconds + * [uniqueSharingUsers] - set of users that were sharing a screen at least once + */ +data class CallScreenSharingMetadata( + val activeScreenShares: Map = emptyMap(), + val completedScreenShareDurationInMillis: Long = 0L, + val uniqueSharingUsers: Set = emptySet() +) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt index 83ff455e796..0a8e1cf43f1 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/call/Participant.kt @@ -33,6 +33,7 @@ data class Participant( val hasEstablishedAudio: Boolean, val avatarAssetId: UserAssetId? = null, val userType: UserType = UserType.NONE, + val accentId: Int ) data class ParticipantMinimized( diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt index 9e19076a14b..b07cf80f009 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/Conversation.kt @@ -30,12 +30,16 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.message.MessagePreview import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.User import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.util.serialization.toJsonElement import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlin.time.Duration /** @@ -78,7 +82,8 @@ data class Conversation( val archivedDateTime: Instant?, val mlsVerificationStatus: VerificationStatus, val proteusVerificationStatus: VerificationStatus, - val legalHoldStatus: LegalHoldStatus + val legalHoldStatus: LegalHoldStatus, + val mlsPublicKeys: MLSPublicKeys? = null ) { companion object { @@ -255,11 +260,23 @@ data class Conversation( fun toLogMap(): Map } - data class Member(val id: UserId, val role: Role) { + @Serializable + data class Member( + @SerialName("id") val id: UserId, + @SerialName("role") val role: Role + ) { + + @Serializable sealed class Role { + + @Serializable data object Member : Role() + + @Serializable data object Admin : Role() - data class Unknown(val name: String) : Role() + + @Serializable + data class Unknown(@SerialName("name") val name: String) : Role() override fun toString(): String = when (this) { @@ -290,18 +307,15 @@ sealed class ConversationDetails(open val conversation: Conversation) { override val conversation: Conversation, val otherUser: OtherUser, val userType: UserType, - val unreadEventCount: UnreadEventCount, - val lastMessage: MessagePreview? + val isFavorite: Boolean = false ) : ConversationDetails(conversation) data class Group( override val conversation: Conversation, val hasOngoingCall: Boolean = false, - val unreadEventCount: UnreadEventCount, - val lastMessage: MessagePreview?, val isSelfUserMember: Boolean, - val isSelfUserCreator: Boolean, - val selfRole: Conversation.Member.Role? + val selfRole: Conversation.Member.Role?, + val isFavorite: Boolean = false // val isTeamAdmin: Boolean, TODO kubaz ) : ConversationDetails(conversation) @@ -341,6 +355,64 @@ sealed class ConversationDetails(open val conversation: Conversation) { ) } +data class ConversationDetailsWithEvents( + val conversationDetails: ConversationDetails, + val unreadEventCount: UnreadEventCount = emptyMap(), + val lastMessage: MessagePreview? = null, + val hasNewActivitiesToShow: Boolean = false, +) + +fun ConversationDetails.interactionAvailability(): InteractionAvailability { + val availability = when (this) { + is ConversationDetails.Connection -> InteractionAvailability.DISABLED + is ConversationDetails.Group -> { + if (isSelfUserMember) InteractionAvailability.ENABLED + else InteractionAvailability.NOT_MEMBER + } + + is ConversationDetails.OneOne -> when { + otherUser.defederated -> InteractionAvailability.DISABLED + otherUser.deleted -> InteractionAvailability.DELETED_USER + otherUser.connectionStatus == ConnectionState.BLOCKED -> InteractionAvailability.BLOCKED_USER + conversation.legalHoldStatus == Conversation.LegalHoldStatus.DEGRADED -> + InteractionAvailability.LEGAL_HOLD + + else -> InteractionAvailability.ENABLED + } + + is ConversationDetails.Self, is ConversationDetails.Team -> InteractionAvailability.DISABLED + } + return availability +} + +enum class InteractionAvailability { + /**User is able to send messages and make calls */ + ENABLED, + + /**Self user is no longer conversation member */ + NOT_MEMBER, + + /**Other user is blocked by self user */ + BLOCKED_USER, + + /**Other team member or public user has been removed */ + DELETED_USER, + + /** + * This indicates that the conversation is using a protocol that self user does not support. + */ + UNSUPPORTED_PROTOCOL, + + /**Conversation type doesn't support messaging */ + DISABLED, + + /** + * One of conversation members is under legal hold and self user is not able to interact with it. + * This applies to 1:1 conversations only when other member is a guest. + */ + LEGAL_HOLD +} + data class MembersInfo(val self: Conversation.Member, val otherMembers: List) data class MemberDetails(val user: User, val role: Conversation.Member.Role) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt new file mode 100644 index 00000000000..f1ca2a7bf39 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFilter.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation + +enum class ConversationFilter { + ALL, + FAVORITES, + GROUPS, + ONE_ON_ONE +} diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt new file mode 100644 index 00000000000..675e9f5794f --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationFolder.kt @@ -0,0 +1,38 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation + +import com.wire.kalium.logic.data.id.QualifiedID + +data class ConversationFolder( + val id: String, + val name: String, + val type: FolderType +) + +data class FolderWithConversations( + val id: String, + val name: String, + val type: FolderType, + val conversationIdList: List +) + +enum class FolderType { + USER, + FAVORITE, +} diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt index f8b451f6eee..445bec974c1 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationStatus.kt @@ -18,22 +18,29 @@ package com.wire.kalium.logic.data.conversation +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + /** * Conversation muting settings type */ -sealed class MutedConversationStatus(open val status: Int = 0) { +@Serializable +sealed class MutedConversationStatus(@SerialName("status") open val status: Int = 0) { /** * 0 -> All notifications are displayed */ + @Serializable data object AllAllowed : MutedConversationStatus(0) /** * 1 -> Only mentions and replies are displayed (normal messages muted) */ + @Serializable data object OnlyMentionsAndRepliesAllowed : MutedConversationStatus(1) /** * 3 -> No notifications are displayed */ + @Serializable data object AllMuted : MutedConversationStatus(3) } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt index a8f19d888f1..d6996ecb13f 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/id/QualifiedId.kt @@ -33,7 +33,8 @@ data class QualifiedID( @SerialName("domain") val domain: String ) { - override fun toString(): String = if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain" + override fun toString(): String = + if (domain.isEmpty()) value else "$value$VALUE_DOMAIN_SEPARATOR$domain" fun toLogString(): String = if (domain.isEmpty()) { value.obfuscateId() @@ -43,6 +44,16 @@ data class QualifiedID( fun toPlainID(): PlainId = PlainId(value) + /** + * This checks if the domain in either instances is blank. If it is, it will compare only the value. + * To be used when when of the instance do not have domain due to the API limitations. + */ + fun equalsIgnoringBlankDomain(other: QualifiedID): Boolean { + if (domain.isBlank() || other.domain.isBlank()) { + return value == other.value + } + return this == other + } } const val VALUE_DOMAIN_SEPARATOR = '@' diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt index 578ce4d1617..3cfd14e6525 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/logout/LogoutReason.kt @@ -46,5 +46,11 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + * This will trigger a cleanup of the local client data and prepare for a fresh start without losing data. + */ + MIGRATION_TO_CC_FAILED } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt index 1c77a460c34..3b1fe3cde5a 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/AssetContent.kt @@ -34,7 +34,7 @@ data class AssetContent( } // We should not display Preview Assets (assets w/o valid encryption keys sent by Mac/Web clients) unless they include image metadata - val shouldBeDisplayed = !isPreviewMessage || hasValidImageMetadata + val isAssetDataComplete = !isPreviewMessage || hasValidImageMetadata sealed class AssetMetadata { data class Image(val width: Int, val height: Int) : AssetMetadata() diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt index d28cfa435f4..5ea75254462 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/Message.kt @@ -29,6 +29,8 @@ import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import com.wire.kalium.util.serialization.toJsonElement import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlin.time.Duration @@ -120,6 +122,7 @@ sealed interface Message { is MessageContent.FailedDecryption -> { mutableMapOf( typeKey to "failedDecryption", + "code" to "${content.errorCode}", "size" to "${content.encodedData?.size}", ) } @@ -472,15 +475,20 @@ sealed interface Message { } } + @Serializable data class ExpirationData( - val expireAfter: Duration, - val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted + @SerialName("expire_after") val expireAfter: Duration, + @SerialName("self_deletion_status") val selfDeletionStatus: SelfDeletionStatus = SelfDeletionStatus.NotStarted ) { + @Serializable sealed class SelfDeletionStatus { + + @Serializable data object NotStarted : SelfDeletionStatus() - data class Started(val selfDeletionEndDate: Instant) : SelfDeletionStatus() + @Serializable + data class Started(@SerialName("self_deletion_end_date") val selfDeletionEndDate: Instant) : SelfDeletionStatus() fun toLogMap(): Map = when (this) { is NotStarted -> mutableMapOf( diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt index f38a9b074cb..56103c2371f 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContent.kt @@ -350,6 +350,7 @@ sealed interface MessageContent { data class FailedDecryption( val encodedData: ByteArray? = null, + val errorCode: Int? = null, val isDecryptionResolved: Boolean, val senderUserId: UserId, val clientId: ClientId? = null diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt index 9fcb19f0f22..419b9d502e8 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/UserSummary.kt @@ -31,5 +31,6 @@ data class UserSummary( val userType: UserType, val isUserDeleted: Boolean, val connectionStatus: ConnectionState, - val availabilityStatus: UserAvailabilityStatus + val availabilityStatus: UserAvailabilityStatus, + val accentId: Int ) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt index 504fac6903c..bb0b8069452 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/mention/MessageMention.kt @@ -19,10 +19,13 @@ package com.wire.kalium.logic.data.message.mention import com.wire.kalium.logic.data.user.UserId +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +@Serializable data class MessageMention( - val start: Int, - val length: Int, - val userId: UserId, - val isSelfMention: Boolean + @SerialName("start") val start: Int, + @SerialName("length") val length: Int, + @SerialName("userId") val userId: UserId, + @SerialName("isSelfMention") val isSelfMention: Boolean ) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt new file mode 100644 index 00000000000..0475345d7fd --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/mls/MLSPublicKeys.kt @@ -0,0 +1,22 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.mls + +data class MLSPublicKeys( + val removal: Map? +) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt new file mode 100644 index 00000000000..622d28ad298 --- /dev/null +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/CreateUserTeam.kt @@ -0,0 +1,3 @@ +package com.wire.kalium.logic.data.user + +data class CreateUserTeam(val teamId: String, val teamName: String) diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt index 4abbcffcf71..31ae4050545 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserModel.kt @@ -145,6 +145,7 @@ data class OtherUserMinimized( val name: String?, val completePicture: UserAssetId?, val userType: UserType, + val accentId: Int ) data class OtherUser( diff --git a/detekt/baseline.xml b/detekt/baseline.xml index 05f36ee07dc..5ef15ecdce9 100644 --- a/detekt/baseline.xml +++ b/detekt/baseline.xml @@ -2,6 +2,7 @@ MatchingDeclarationName:Widgets.kt$CustomScrollRegion : Widget + UnusedPrivateProperty:build.gradle.kts$val commonMain by sourceSets.getting { dependencies { implementation(project(":network")) implementation(project(":cryptography")) implementation(project(":logic")) implementation(project(":util")) implementation(libs.cliKt) implementation(libs.ktor.utils) implementation(libs.coroutines.core) implementation(libs.ktxDateTime) implementation(libs.mordant) implementation(libs.ktxSerialization) implementation(libs.ktxIO) } } AnnotationSpacing:HttpClientConnectionSpecsTest.kt$HttpClientConnectionSpecsTest$@Test diff --git a/docs/notebooks/README.md b/docs/notebooks/README.md new file mode 100644 index 00000000000..e23e2f87603 --- /dev/null +++ b/docs/notebooks/README.md @@ -0,0 +1,3 @@ +A place to store [Jupyter Notebook](https://jupyter.org/) files of ongoing studies. + +You can use Kotlin and run it directly in the IDE, thanks to [Kotlin Notebook](https://kotlinlang.org/docs/kotlin-notebook-overview.html). diff --git a/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb b/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb new file mode 100644 index 00000000000..7bc2901111d --- /dev/null +++ b/docs/notebooks/event-performance-investigation/PerformanceImprovements.ipynb @@ -0,0 +1,5158 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# Event Processing Analysis\n", + "\n", + "### What's the goal?\n", + "Understand where Kalium is spending more time during event processing, in order to find possible performance improvements.\n", + "\n", + "\n", + "### Current context\n", + "- We've added [logs that measure how much time we're taking to process individual events](https://github.com/wireapp/kalium/pull/2827).\n", + "- The average time to process events oscilates every day between 368ms all the way up to 1280ms;\n", + "- 97,3% of the time spent processing events was processing end-to-end encrypted messages;\n", + "- 51% of the time spent processing messages was in order to process Text messages, even though they only account for 24,04% of the messages;\n", + "- `LastRead` is another interesting one: 18,57% of the count, but takes 24,3% of the time\n", + "\n", + "## What even is measured within \"Event Processing\"?\n", + "We measure all the time taken from the beginning of `EventReceiver.onEvent` until the event is handled completely.\n", + "For some Event Types, this can be super quick, like handling in-conversation Typing indication. Typing indication consists of just emitting to an in-memory flow and the event is fully processed.\n", + "For some events, this can be tricky. All End-To-End messages require parsing the encrypted blob, parsing the Protobuf, and then \"actually\" handling the different message types. Different message types often need completely different logic, so they can be as diverse as different event types.\n", + "\n", + "## Starting plan of action\n", + "Collect data locally, by adding more logs and generating events.\n", + "\n", + "My first fail was using an emulator. Running the debug of the Android app on the emulator resulted in nothing but weird looks. DataDog was giving hundreds of milliseconds as the average for event processing, while my emulator was taking about 20~30ms. \n", + "\n", + "How could that be? Running on an actual device – with the screen locked – made it right. An emulator has almost no apps installed competing for resources with the app being tested, and isn't prone to battery saving measures like CPU speed reduction, etc.\n", + " \n", + "## Lesson #1\n", + "We can focus on \"background performance\": receive a push notification, sync, decrypt, show notifications.\n", + "Or on \"foreground performance\": open the app, sync, decrypt, show notifications and unread indicators.\n", + "\n", + "The main difference is how the app might be subject to battery saving measures taken by the OS of a real device, and the time to perform the same actions might be completely different between the two scenarios.\n", + "Proposed action:\n", + "- Investigate further if we can add battery, back/fore-ground information, etc. to logs; This way we can track performance indication separately.\n", + "\n", + "## Keep digging \n", + "\n", + "Considering how there are multiple steps to \"event processing\", I've added logs to gather more information about these steps and their \"substeps\".\n", + "Also, considering 97,3% of the time is spent during Messages processing, I focused on these.\n", + "\n", + "Time spent during message events processing can be seen defined below in `TimeTaken`. \n", + " " + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Just adding some dependencies for manipulating data and Kotlin's datetime" + }, + { + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2024-09-02T08:30:22.641327700Z", + "start_time": "2024-09-02T08:30:19.253500300Z" + } + }, + "cell_type": "code", + "source": [ + "%use kandy\n", + "%use dataframe" + ], + "outputs": [], + "execution_count": 13 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:22.690290300Z", + "start_time": "2024-09-02T08:30:22.645582300Z" + } + }, + "cell_type": "code", + "source": "@file:DependsOn(\"org.jetbrains.kotlinx:kotlinx-datetime:0.6.1\")", + "outputs": [], + "execution_count": 14 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Defining a way of representing event processing measurements and how to import data from the `logs`:" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:23.007982300Z", + "start_time": "2024-09-02T08:30:22.695901100Z" + } + }, + "cell_type": "code", + "source": [ + "\n", + "import kotlin.time.Duration\n", + "import kotlin.time.Duration.Companion.milliseconds\n", + "\n", + "data class ProteusDecryption(\n", + " val checkingSession: Duration,\n", + " val decrypting: Duration,\n", + " val savingSession: Duration,\n", + ") {\n", + " val total: Duration = checkingSession + decrypting + savingSession\n", + "}\n", + "\n", + "data class ProcessingMessage(\n", + " /**\n", + " * Dealing with possibility of LegalHold\n", + " */\n", + " val checkingLegalhold: Duration,\n", + " /**\n", + " * Actually handling the content of the message,\n", + " * if LastRead, marking as read in the DB,\n", + " * if Text, inserting into the DB, emitting notifications,\n", + " * ...\n", + " */\n", + " val handlingMessageContent: Duration,\n", + " /**\n", + " * Other actions we take after the message is handled, like enqueueing delivery receipt\n", + " * and enqueueing message deletion if the message is SelfDeleting\n", + " */\n", + " val postHandling: Duration\n", + ") {\n", + " val total: Duration = checkingLegalhold + handlingMessageContent + postHandling\n", + "}\n", + "\n", + "data class TimeTaken(\n", + " val messageType: String,\n", + " val total: Duration,\n", + " val proteus: ProteusDecryption,\n", + " val processingMessage: ProcessingMessage\n", + ") {\n", + " /**\n", + " * Unknown time can be things like parsing the Protobuf, converting the blob from Base64, etc.\n", + " * In general: it shouldn't be slow, and it's less likely we can speed it up.\n", + " */\n", + " val unknownTime = total - proteus.total - processingMessage.total\n", + "}\n", + "\n", + "fun getDataFromLogs(logFileName: String): List {\n", + " val file = File(\"logs/$logFileName\")\n", + " val groups = file.readText().split(\"\"\"^$\"\"\".toRegex(RegexOption.MULTILINE))\n", + "\n", + " val allData = groups.map {\n", + " it.trim().split(\"\\n\").filter { it.isNotBlank() }\n", + " }.filter {\n", + " it.isNotEmpty()\n", + " }.map { lines ->\n", + " val logLine = lines[0]\n", + " val measurements = logLine.split(\";\").map { it.split(\" \").last() }\n", + " val proteusDuration = ProteusDecryption(\n", + " Duration.parse(measurements[0].trim()),\n", + " Duration.parse(measurements[1].trim()),\n", + " Duration.parse(measurements[2].trim()),\n", + " )\n", + "\n", + " val processingLine = lines[1]\n", + " val processingMeasurements = processingLine.split(\";\").map { it.split(\" \").last() }\n", + " val processingDuration = ProcessingMessage(\n", + " Duration.parse(processingMeasurements[0].trim()),\n", + " Duration.parse(processingMeasurements[1].trim()),\n", + " Duration.parse(processingMeasurements[2].trim()),\n", + " )\n", + " val mainInfo = lines[2].split(\" \")\n", + " val timeTaken = mainInfo[0].toLong().milliseconds\n", + " val messageType = mainInfo[1]\n", + " TimeTaken(messageType, timeTaken, proteusDuration, processingDuration)\n", + " }\n", + " return allData\n", + "}" + ], + "outputs": [], + "execution_count": 15 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "Ok, so let's do a simple analysis of how much time is spent during Proteus, Message handling and other." + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:23.933356Z", + "start_time": "2024-09-02T08:30:23.018790400Z" + } + }, + "cell_type": "code", + "source": [ + "@file:DependsOn(\"org.jetbrains.kotlinx:kotlinx-datetime:0.6.1\")\n", + "\n", + "val initialData = getDataFromLogs(\"InitialData.txt\")\n", + "\n", + "val pairs =\n", + " initialData.map { it.proteus.total.inWholeMicroseconds to \"Proteus\" } +\n", + " initialData.map { it.processingMessage.total.inWholeMicroseconds to \"Processing\" } +\n", + " initialData.map { it.unknownTime.inWholeMicroseconds to \"Other\" }\n", + "\n", + "val value by column(pairs.map { it.first })\n", + "val stepName by column(pairs.map { it.second })\n", + "val dataSet = dataFrameOf(value, stepName)\n", + "dataSet.groupBy(stepName).aggregate {\n", + " sum(value) into \"total\"\n", + "}.plot {\n", + " pie {\n", + " slice(\"total\")\n", + " fillColor(\"stepName\")\n", + " }\n", + " layout {\n", + " title = \"Time spent unpacking and processing message events\"\n", + " style(Style.Void)\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Time spent unpacking and processing message events\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " stepName\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Proteus\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Processing\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Other\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "ggtitle": { + "text": "Time spent unpacking and processing message events" + }, + "mapping": {}, + "data": { + "total": [ + 3753588.0, + 4459976.0, + 804380.0 + ], + "stepName": [ + "Proteus", + "Processing", + "Other" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "fill", + "discrete": true + } + ], + "layers": [ + { + "mapping": { + "slice": "total", + "fill": "stepName" + }, + "stat": "identity", + "sampling": "none", + "position": "identity", + "geom": "pie" + } + ], + "theme": { + "name": "classic", + "axis": { + "blank": true + }, + "line": { + "blank": true + }, + "axis_ontop": false, + "axis_ontop_y": false, + "axis_ontop_x": false + } + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 16 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "While Processing clearly is the slowest part, the Proteus part is still interesting. Because it affects _all_ message types.\n", + "Processing can be very different between messages and requires an investigation for _each_ message type to find bottlenecks and improvement opportunities.\n", + "\n", + "With that in mind, let's dive a bit deeper into Proteus. Let's split it into smaller chunks and see what we can improve there." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:24.568994Z", + "start_time": "2024-09-02T08:30:23.971615300Z" + } + }, + "cell_type": "code", + "source": [ + "val initialProteusMeasurements = initialData.map { it.proteus }\n", + "val checkingSession by column(initialProteusMeasurements.map { it.checkingSession.inWholeMilliseconds })\n", + "val decrypting by column(initialProteusMeasurements.map { it.decrypting.inWholeMilliseconds })\n", + "val savingSession by column(initialProteusMeasurements.map { it.savingSession.inWholeMilliseconds })\n", + "val eventNumber by column(buildList { repeat(initialProteusMeasurements.size) { i -> add(i + 1)} })\n", + "val detailedFrame = dataFrameOf(eventNumber, checkingSession, decrypting, savingSession)\n", + " .gather(checkingSession, decrypting, savingSession)\n", + " .into(\"measurements\", \"timeSpent\")\n", + "\n", + "detailedFrame.plot {\n", + " layout.title = \"Time taken in each phase of Proteus Unpacking, for different events\"\n", + " points {\n", + " x(\"measurements\")\n", + " y(\"timeSpent\")\n", + " color(\"measurements\")\n", + " position = Position.jitter()\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " checkingSession\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " decrypting\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " savingSession\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 20\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 40\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 60\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 80\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 100\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 120\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Time taken in each phase of Proteus Unpacking, for different events\n", + " \n", + " \n", + " \n", + " \n", + " timeSpent\n", + " \n", + " \n", + " \n", + " \n", + " measurements\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " measurements\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " checkingSession\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " decrypting\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " savingSession\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "ggtitle": { + "text": "Time taken in each phase of Proteus Unpacking, for different events" + }, + "mapping": {}, + "data": { + "timeSpent": [ + 8.0, + 63.0, + 30.0, + 10.0, + 36.0, + 21.0, + 19.0, + 25.0, + 49.0, + 10.0, + 30.0, + 50.0, + 40.0, + 77.0, + 21.0, + 5.0, + 20.0, + 46.0, + 9.0, + 20.0, + 20.0, + 37.0, + 117.0, + 34.0, + 129.0, + 61.0, + 12.0, + 9.0, + 69.0, + 19.0, + 34.0, + 106.0, + 57.0, + 16.0, + 10.0, + 76.0, + 7.0, + 85.0, + 56.0, + 11.0, + 60.0, + 13.0, + 6.0, + 13.0, + 85.0, + 13.0, + 28.0, + 30.0, + 6.0, + 12.0, + 14.0, + 18.0, + 56.0, + 10.0, + 5.0, + 22.0, + 43.0, + 22.0, + 15.0, + 18.0, + 7.0, + 11.0, + 25.0, + 37.0, + 50.0, + 31.0, + 6.0, + 10.0, + 17.0, + 7.0, + 77.0, + 60.0, + 6.0, + 76.0, + 12.0, + 9.0, + 17.0, + 10.0, + 7.0, + 10.0, + 8.0, + 14.0, + 28.0, + 92.0, + 18.0, + 14.0, + 35.0, + 9.0, + 10.0, + 32.0, + 58.0, + 15.0, + 46.0, + 48.0, + 82.0, + 15.0, + 4.0, + 34.0, + 17.0, + 27.0, + 28.0, + 18.0, + 27.0, + 66.0, + 30.0, + 48.0, + 90.0, + 57.0, + 44.0, + 53.0, + 49.0, + 12.0, + 35.0, + 9.0 + ], + "measurements": [ + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession", + "checkingSession", + "decrypting", + "savingSession" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "x", + "discrete": true + }, + { + "aesthetic": "y", + "limits": [ + null, + null + ] + }, + { + "aesthetic": "color", + "discrete": true + } + ], + "layers": [ + { + "mapping": { + "x": "measurements", + "y": "timeSpent", + "color": "measurements" + }, + "stat": "identity", + "sampling": "none", + "position": "jitter", + "geom": "point" + } + ] + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 17 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "While the colours might cause seizure, it goes to show how random it is. In some times saving session is slow. In some times checking session is slow. In some times decrypting is slow.\n", + "\n", + "One thing can definitely be seen from this, is that decrypting is the slowest part.\n", + "\n", + "Unfortunately, decrypting is something we can't solve on our end. It's embedded within CoreCrypto / CryptoBox, and we are unable to change it.\n", + "\n", + "Here's a simpler visualization:" + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:24.856529300Z", + "start_time": "2024-09-02T08:30:24.608176900Z" + } + }, + "cell_type": "code", + "source": [ + "val steps by columnOf(\"Checking Session\", \"Decrypting\", \"Saving Session\")\n", + "val values by columnOf(checkingSession.sum(), decrypting.sum(), savingSession.sum())\n", + "\n", + "val dataFrame = dataFrameOf(steps, values)\n", + "dataFrame.plot {\n", + " pie {\n", + " slice(values)\n", + " fillColor(steps)\n", + " }\n", + " layout {\n", + " title = \"Time share in different Proteus phases\"\n", + " style(Style.Void)\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Time share in different Proteus phases\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " steps\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Checking Session\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Decrypting\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Saving Session\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "ggtitle": { + "text": "Time share in different Proteus phases" + }, + "mapping": {}, + "data": { + "values": [ + 802.0, + 1631.0, + 1267.0 + ], + "steps": [ + "Checking Session", + "Decrypting", + "Saving Session" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "fill", + "discrete": true + } + ], + "layers": [ + { + "mapping": { + "slice": "values", + "fill": "steps" + }, + "stat": "identity", + "sampling": "none", + "position": "identity", + "geom": "pie" + } + ], + "theme": { + "name": "classic", + "axis": { + "blank": true + }, + "line": { + "blank": true + }, + "axis_ontop": false, + "axis_ontop_y": false, + "axis_ontop_x": false + } + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 18 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "Clearly decrypting is the biggest offender. And as mentioned before, we can't do much about it.\n", + "\n", + "However, we might have quick wins and reduce `Saving Session` and `Checking Session`, which account for almost 50% of the total Proteus time." + ] + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Idea #1\n", + "Remove `Saving Session` completely. It's unnecessary as CoreCrypto will perform session saving in every `decrypt`. We're doing that unnecessarily.\n", + "\n", + "## Idea #2\n", + "Cache existing sessions in-memory. Instead of using IO and relying solely on CoreCrypto to know if sessions exist or not, we can store known sessions in memory.\n", + "If sessions are deleted, we can remove from the memory cache. \n", + "\n", + "## Results\n", + "After doing the first idea, I collected data and stored it in `NotSavingSession.txt`. After that, I also did the second idea and collected data, bringing to the joined improvements." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:25.503196600Z", + "start_time": "2024-09-02T08:30:24.972036600Z" + } + }, + "cell_type": "code", + "source": [ + "val withoutSessionSavingData = getDataFromLogs(\"NotSavingSession.txt\")\n", + "val withSessionCacheData = getDataFromLogs(\"WithExistingSessionCache.txt\")\n", + "\n", + "val initialTime = initialData.map { it.total.inWholeMilliseconds to \"Initial\" }\n", + "val withoutSessionSaving =\n", + " withoutSessionSavingData.map { it.total.inWholeMilliseconds to \"NoSessionSaving\" }\n", + "val withCache = withSessionCacheData.map { it.total.inWholeMilliseconds to \"WithSessionCache\" }\n", + "val allData = (initialTime + withoutSessionSaving + withCache)\n", + "val timeTaken by column(allData.map { it.first })\n", + "val approach by column(allData.map { it.second })\n", + "\n", + "val averages = listOf(initialTime.map { it.first }.average(), withoutSessionSaving.map { it.first }.average(), withCache.map { it.first }.average())\n", + "val dataFrame = dataFrameOf(timeTaken, approach)\n", + "\n", + "dataFrame.plot {\n", + " layout.title = \"Average time taken to fully process events across different approaches\"\n", + " points {\n", + " y(timeTaken)\n", + " x(approach)\n", + " color(approach)\n", + " position = Position.jitter()\n", + " }\n", + " line {\n", + " y(averages) {\n", + " axis.name = \"Time Taken (ms)\"\n", + " }\n", + " x(approach.distinct()) {\n", + " axis.name = \"Approach\"\n", + " }\n", + " color = Color.PURPLE\n", + " this.tooltips(true)\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Initial\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " NoSessionSaving\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " WithSessionCache\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 100\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 200\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 300\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 400\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 500\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Average time taken to fully process events across different approaches\n", + " \n", + " \n", + " \n", + " \n", + " Time Taken (ms)\n", + " \n", + " \n", + " \n", + " \n", + " Approach\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " approach\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Initial\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " NoSessionSaving\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " WithSessionCache\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "ggtitle": { + "text": "Average time taken to fully process events across different approaches" + }, + "mapping": {}, + "data": { + "timeTaken": [ + 349.0, + 150.0, + 240.0, + 347.0, + 268.0, + 182.0, + 112.0, + 363.0, + 481.0, + 299.0, + 400.0, + 242.0, + 322.0, + 150.0, + 183.0, + 161.0, + 104.0, + 178.0, + 159.0, + 127.0, + 174.0, + 306.0, + 161.0, + 233.0, + 218.0, + 86.0, + 122.0, + 313.0, + 261.0, + 201.0, + 286.0, + 275.0, + 164.0, + 203.0, + 325.0, + 378.0, + 337.0, + 158.0, + 412.0, + 267.0, + 140.0, + 292.0, + 444.0, + 127.0, + 226.0, + 231.0, + 327.0, + 147.0, + 205.0, + 330.0, + 286.0, + 314.0, + 172.0, + 198.0, + 360.0, + 124.0, + 232.0, + 151.0, + 181.0, + 112.0, + 162.0, + 80.0, + 72.0, + 68.0, + 122.0, + 191.0, + 89.0, + 104.0, + 177.0, + 72.0, + 72.0, + 109.0, + 240.0, + 117.0, + 373.0, + 201.0, + 106.0, + 65.0, + 128.0, + 191.0, + 147.0, + 143.0, + 112.0, + 133.0, + 168.0, + 251.0, + 180.0, + 343.0, + 200.0, + 171.0, + 164.0, + 104.0, + 102.0, + 77.0, + 103.0, + 191.0, + 129.0, + 204.0, + 117.0, + 73.0, + 198.0, + 165.0, + 190.0, + 78.0, + 172.0, + 124.0, + 172.0, + 155.0, + 74.0, + 54.0, + 259.0, + 103.0, + 89.0, + 168.0, + 58.0, + 135.0, + 92.0, + 136.0, + 97.0, + 145.0, + 145.0, + 151.0, + 263.0, + 204.0, + 160.0, + 221.0, + 116.0, + 92.0, + 138.0 + ], + "approach": [ + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "Initial", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "NoSessionSaving", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache", + "WithSessionCache" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "y", + "limits": [ + null, + null + ] + }, + { + "aesthetic": "x", + "discrete": true + }, + { + "aesthetic": "color", + "discrete": true + }, + { + "aesthetic": "y", + "name": "Time Taken (ms)", + "limits": [ + null, + null + ] + }, + { + "aesthetic": "x", + "discrete": true, + "name": "Approach" + } + ], + "layers": [ + { + "mapping": { + "y": "timeTaken", + "x": "approach", + "color": "approach" + }, + "stat": "identity", + "sampling": "none", + "position": "jitter", + "geom": "point" + }, + { + "mapping": { + "y": "y", + "x": "approach" + }, + "stat": "identity", + "data": { + "y": [ + 237.31578947368422, + 195.71794871794873, + 147.1153846153846 + ], + "approach": [ + "Initial", + "NoSessionSaving", + "WithSessionCache" + ] + }, + "color": "#9a60b4", + "sampling": "none", + "position": "identity", + "geom": "line", + "tooltips": {} + } + ] + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 19 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## First win\n", + "Clearly we're up to something. Processing events is clearly faster with the changes in Proteus. The average time went from 247ms, to 196ms without saving session, and then down to 147ms without saving session + caching existing sessions.\n", + "\n", + "## But we're not over yet\n", + "Maybe we can also analyse the different parts of `ProcessingMessage` and identify what is taking more time? For that, we can join all the logs taken so far and ignore the Proteus part." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:25.811756500Z", + "start_time": "2024-09-02T08:30:25.533313800Z" + } + }, + "cell_type": "code", + "source": [ + "val allMeasurements = initialData + withoutSessionSavingData + withSessionCacheData\n", + "val allMessageProcessing = allMeasurements.map { \n", + " it.processingMessage\n", + "}\n", + "val checkingLegalhold by column(allMessageProcessing.map { it.checkingLegalhold.inWholeMilliseconds })\n", + "val handlingContent by column(allMessageProcessing.map { it.handlingMessageContent.inWholeMilliseconds })\n", + "val postHandling by column(allMessageProcessing.map { it.postHandling.inWholeMilliseconds })\n", + "val eventNumber by column(buildList { repeat(allMessageProcessing.size) { i -> add(i + 1)} })\n", + "val detailedFrame = dataFrameOf(eventNumber, checkingLegalhold, handlingContent, postHandling)\n", + " .gather(checkingLegalhold, handlingContent, postHandling)\n", + " .into(\"measurements\", \"timeSpent\")\n", + "\n", + "detailedFrame.plot { \n", + " points { \n", + " x(\"measurements\")\n", + " y(\"timeSpent\")\n", + " position = Position.jitter()\n", + " color(\"measurements\")\n", + " }\n", + "}\n" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " checkingLegalhold\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " handlingContent\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " postHandling\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 0\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 50\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 100\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 150\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 200\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 250\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 300\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " 350\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " timeSpent\n", + " \n", + " \n", + " \n", + " \n", + " measurements\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " measurements\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " checkingLegalhold\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " handlingContent\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " postHandling\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "mapping": {}, + "data": { + "timeSpent": [ + 5.0, + 130.0, + 0.0, + 23.0, + 52.0, + 0.0, + 12.0, + 100.0, + 0.0, + 86.0, + 141.0, + 0.0, + 9.0, + 106.0, + 0.0, + 3.0, + 99.0, + 0.0, + 4.0, + 41.0, + 0.0, + 11.0, + 142.0, + 0.0, + 131.0, + 124.0, + 0.0, + 12.0, + 144.0, + 8.0, + 6.0, + 179.0, + 0.0, + 17.0, + 106.0, + 0.0, + 19.0, + 112.0, + 0.0, + 8.0, + 50.0, + 0.0, + 21.0, + 52.0, + 0.0, + 4.0, + 62.0, + 0.0, + 5.0, + 40.0, + 0.0, + 5.0, + 79.0, + 0.0, + 3.0, + 78.0, + 0.0, + 4.0, + 49.0, + 0.0, + 4.0, + 119.0, + 0.0, + 18.0, + 139.0, + 0.0, + 12.0, + 84.0, + 0.0, + 26.0, + 55.0, + 0.0, + 7.0, + 104.0, + 0.0, + 5.0, + 30.0, + 0.0, + 3.0, + 87.0, + 0.0, + 4.0, + 106.0, + 0.0, + 28.0, + 144.0, + 0.0, + 4.0, + 105.0, + 0.0, + 39.0, + 101.0, + 0.0, + 6.0, + 72.0, + 0.0, + 14.0, + 86.0, + 0.0, + 24.0, + 92.0, + 0.0, + 3.0, + 191.0, + 0.0, + 13.0, + 161.0, + 0.0, + 33.0, + 122.0, + 0.0, + 16.0, + 79.0, + 0.0, + 0.0, + 97.0, + 0.0, + 10.0, + 181.0, + 0.0, + 9.0, + 75.0, + 0.0, + 5.0, + 138.0, + 0.0, + 4.0, + 346.0, + 0.0, + 14.0, + 43.0, + 0.0, + 5.0, + 104.0, + 0.0, + 33.0, + 58.0, + 0.0, + 4.0, + 89.0, + 0.0, + 9.0, + 87.0, + 0.0, + 4.0, + 82.0, + 0.0, + 10.0, + 139.0, + 0.0, + 6.0, + 125.0, + 0.0, + 28.0, + 121.0, + 0.0, + 9.0, + 101.0, + 0.0, + 4.0, + 111.0, + 0.0, + 5.0, + 210.0, + 0.0, + 7.0, + 26.0, + 0.0, + 43.0, + 84.0, + 0.0, + 5.0, + 65.0, + 0.0, + 6.0, + 57.0, + 0.0, + 5.0, + 37.0, + 0.0, + 4.0, + 61.0, + 0.0, + 4.0, + 20.0, + 0.0, + 9.0, + 16.0, + 0.0, + 6.0, + 14.0, + 0.0, + 10.0, + 25.0, + 0.0, + 7.0, + 45.0, + 0.0, + 4.0, + 29.0, + 0.0, + 4.0, + 16.0, + 0.0, + 40.0, + 24.0, + 0.0, + 6.0, + 21.0, + 0.0, + 19.0, + 11.0, + 0.0, + 4.0, + 58.0, + 0.0, + 4.0, + 120.0, + 0.0, + 4.0, + 52.0, + 0.0, + 79.0, + 47.0, + 0.0, + 10.0, + 106.0, + 0.0, + 25.0, + 16.0, + 0.0, + 0.0, + 30.0, + 0.0, + 8.0, + 87.0, + 0.0, + 43.0, + 84.0, + 0.0, + 22.0, + 82.0, + 0.0, + 18.0, + 39.0, + 0.0, + 12.0, + 59.0, + 0.0, + 4.0, + 43.0, + 0.0, + 4.0, + 74.0, + 0.0, + 17.0, + 160.0, + 0.0, + 29.0, + 32.0, + 0.0, + 8.0, + 110.0, + 0.0, + 26.0, + 100.0, + 0.0, + 32.0, + 77.0, + 0.0, + 13.0, + 59.0, + 0.0, + 22.0, + 54.0, + 0.0, + 7.0, + 56.0, + 0.0, + 4.0, + 30.0, + 0.0, + 13.0, + 23.0, + 0.0, + 5.0, + 63.0, + 0.0, + 7.0, + 45.0, + 0.0, + 4.0, + 82.0, + 0.0, + 5.0, + 41.0, + 0.0, + 5.0, + 23.0, + 0.0, + 5.0, + 84.0, + 0.0, + 6.0, + 44.0, + 0.0, + 23.0, + 73.0, + 0.0, + 15.0, + 19.0, + 0.0, + 7.0, + 95.0, + 0.0, + 30.0, + 31.0, + 0.0, + 19.0, + 70.0, + 0.0, + 9.0, + 19.0, + 0.0, + 4.0, + 18.0, + 0.0, + 4.0, + 30.0, + 0.0, + 13.0, + 114.0, + 0.0, + 28.0, + 16.0, + 0.0, + 4.0, + 14.0, + 0.0, + 36.0, + 41.0, + 0.0, + 5.0, + 32.0, + 0.0, + 4.0, + 38.0, + 0.0, + 4.0, + 47.0, + 0.0, + 4.0, + 26.0, + 0.0, + 14.0, + 46.0, + 3.0, + 5.0, + 76.0, + 0.0, + 19.0, + 76.0, + 0.0, + 5.0, + 56.0, + 0.0, + 11.0, + 99.0, + 0.0, + 13.0, + 78.0, + 0.0, + 31.0, + 24.0, + 0.0, + 50.0, + 69.0, + 0.0, + 5.0, + 11.0, + 0.0, + 9.0, + 45.0, + 0.0, + 6.0, + 83.0, + 0.0 + ], + "measurements": [ + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling", + "checkingLegalhold", + "handlingContent", + "postHandling" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "x", + "discrete": true + }, + { + "aesthetic": "y", + "limits": [ + null, + null + ] + }, + { + "aesthetic": "color", + "discrete": true + } + ], + "layers": [ + { + "mapping": { + "x": "measurements", + "y": "timeSpent", + "color": "measurements" + }, + "stat": "identity", + "sampling": "none", + "position": "jitter", + "geom": "point" + } + ] + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 20 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Bad news\n", + "\n", + "While checking legalhold is definitely a bit more random (probably due to IO operations), handling each different content is definitely the biggest offender.\n", + "Here's a comparison across all different steps using the most improved data." + ] + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-02T08:30:26.254403300Z", + "start_time": "2024-09-02T08:30:25.873610800Z" + } + }, + "cell_type": "code", + "source": [ + "import org.jetbrains.kotlinx.kandy.ir.feature.FeatureName\n", + "\n", + "val pairs =\n", + " withSessionCacheData.map { it.proteus.checkingSession.inWholeMicroseconds to \"Checking Session\" } +\n", + " withSessionCacheData.map { it.proteus.decrypting.inWholeMicroseconds to \"Decrypting\" } +\n", + " withSessionCacheData.map { it.proteus.savingSession.inWholeMicroseconds to \"Saving Session\" } +\n", + " withSessionCacheData.map { it.processingMessage.checkingLegalhold.inWholeMicroseconds to \"Checking Legalhold\" } +\n", + " withSessionCacheData.map { it.processingMessage.handlingMessageContent.inWholeMicroseconds to \"Handling Content\" } +\n", + " withSessionCacheData.map { it.processingMessage.postHandling.inWholeMicroseconds to \"Post Handling\" } +\n", + " withSessionCacheData.map { it.unknownTime.inWholeMicroseconds to \"Other\" }\n", + "\n", + "val value by column(pairs.map { it.first })\n", + "val stepName by column(pairs.map { it.second })\n", + "val dataSet = dataFrameOf(value, stepName)\n", + "dataSet.groupBy(stepName).aggregate {\n", + " sum(value) into \"total\"\n", + "}.plot {\n", + " pie {\n", + " slice(\"total\")\n", + " fillColor(\"stepName\")\n", + " }\n", + " layout {\n", + " title = \"Time spent unpacking and processing message events after improvements\"\n", + " style(Style.Void)\n", + " }\n", + "}" + ], + "outputs": [ + { + "data": { + "text/html": [ + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Time spent unpacking and processing message events after improvements\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " stepName\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Checking Session\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Decrypting\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Saving Session\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Checking Legalhold\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Handling Content\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Post Handling\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " Other\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "\n", + " " + ], + "application/plot+json": { + "output_type": "lets_plot_spec", + "output": { + "ggtitle": { + "text": "Time spent unpacking and processing message events after improvements" + }, + "mapping": {}, + "data": { + "total": [ + 6634.0, + 2872468.0, + 232.0, + 721930.0, + 2952363.0, + 6072.0, + 1090122.0 + ], + "stepName": [ + "Checking Session", + "Decrypting", + "Saving Session", + "Checking Legalhold", + "Handling Content", + "Post Handling", + "Other" + ] + }, + "kind": "plot", + "scales": [ + { + "aesthetic": "fill", + "discrete": true + } + ], + "layers": [ + { + "mapping": { + "slice": "total", + "fill": "stepName" + }, + "stat": "identity", + "sampling": "none", + "position": "identity", + "geom": "pie" + } + ], + "theme": { + "name": "classic", + "axis": { + "blank": true + }, + "line": { + "blank": true + }, + "axis_ontop": false, + "axis_ontop_y": false, + "axis_ontop_x": false + } + }, + "apply_color_scheme": true, + "swing_enabled": true + } + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "execution_count": 21 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## What's next?\n", + "\n", + "### CoreCrypto transactions\n", + "CoreCrypto has plans to create a transaction API so we can batch-decrypt messages, which should speed up the `decrypting` part.\n", + "\n", + "### Check \"Other\" slice\n", + "Perhaps there is still a bit of time to gain in the \"Other\" part of the code, which is related to Protobuf parsing, Base64 decoding, etc. ?\n", + "\n", + "### Dive deeper into each message type\n", + "Keep in mind that most of the events collected for the experiments above were Text events. Which seems to be one of the worst performing messages we process.\n", + "This is definitely the most demanding job, as it requires pretty much one investigation for Text, one for LastRead, one for Asset, one for Receipt, etc.\n", + "But might be one of the most fruitful ones, and strikes a good balance between a major refactor that would be with CoreCrypto transactions, and small / segmented improvements across the processing pipeline." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Kotlin", + "language": "kotlin", + "name": "kotlin" + }, + "language_info": { + "name": "kotlin", + "version": "1.9.23", + "mimetype": "text/x-kotlin", + "file_extension": ".kt", + "pygments_lexer": "kotlin", + "codemirror_mode": "text/x-kotlin", + "nbconvert_exporter": "" + }, + "ktnbPluginMetadata": { + "projectLibraries": [] + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/docs/notebooks/event-performance-investigation/logs/InitialData.txt b/docs/notebooks/event-performance-investigation/logs/InitialData.txt new file mode 100644 index 00000000000..bf35441bacd --- /dev/null +++ b/docs/notebooks/event-performance-investigation/logs/InitialData.txt @@ -0,0 +1,152 @@ + +Decrypting Proteus. CheckingSession: 8.873250ms; Decrypting: 63.703776ms; Saving Session: 30.135295ms +Processing Application Message. LegalHold: 5.755086ms; Actually Handling: 130.706096ms; Post Handling: 143.717us +349 Text + +Decrypting Proteus. CheckingSession: 10.304281ms; Decrypting: 36.299642ms; Saving Session: 21.362386ms +Processing Application Message. LegalHold: 23.325888ms; Actually Handling: 52.317098ms; Post Handling: 47.323us +150 Text + +Decrypting Proteus. CheckingSession: 19.705038ms; Decrypting: 25.850708ms; Saving Session: 49.176188ms +Processing Application Message. LegalHold: 12.177897ms; Actually Handling: 100.877320ms; Post Handling: 58.146us +240 Text + +Decrypting Proteus. CheckingSession: 10.535889ms; Decrypting: 30.623861ms; Saving Session: 50.635539ms +Processing Application Message. LegalHold: 86.545207ms; Actually Handling: 141.545858ms; Post Handling: 212.321us +347 Text + +Decrypting Proteus. CheckingSession: 40.843750ms; Decrypting: 77.642823ms; Saving Session: 21.599772ms +Processing Application Message. LegalHold: 9.775106ms; Actually Handling: 106.065226ms; Post Handling: 45.207us +268 Text + +Decrypting Proteus. CheckingSession: 5.257365ms; Decrypting: 20.301676ms; Saving Session: 46.290242ms +Processing Application Message. LegalHold: 3.956584ms; Actually Handling: 99.363404ms; Post Handling: 55.868us +182 Text + +Decrypting Proteus. CheckingSession: 9.790934ms; Decrypting: 20.166585ms; Saving Session: 20.536784ms +Processing Application Message. LegalHold: 4.910481ms; Actually Handling: 41.863892ms; Post Handling: 43.742us +112 Text + +Decrypting Proteus. CheckingSession: 37.533936ms; Decrypting: 117.194052ms; Saving Session: 34.765462ms +Processing Application Message. LegalHold: 11.211955ms; Actually Handling: 142.946085ms; Post Handling: 62.215us +363 Text + +Decrypting Proteus. CheckingSession: 129.018718ms; Decrypting: 61.166015ms; Saving Session: 12.040650ms +Processing Application Message. LegalHold: 131.752482ms; Actually Handling: 124.796590ms; Post Handling: 46.672us +481 Text + +Decrypting Proteus. CheckingSession: 9.000204ms; Decrypting: 69.963949ms; Saving Session: 19.456218ms +Processing Application Message. LegalHold: 12.183798ms; Actually Handling: 144.756022ms; Post Handling: 8.200154ms +299 Text + +Decrypting Proteus. CheckingSession: 34.256022ms; Decrypting: 106.964966ms; Saving Session: 57.150595ms +Processing Application Message. LegalHold: 6.519572ms; Actually Handling: 179.368612ms; Post Handling: 44.678us +400 Text + +Decrypting Proteus. CheckingSession: 16.447510ms; Decrypting: 10.732666ms; Saving Session: 76.608684ms +Processing Application Message. LegalHold: 17.636882ms; Actually Handling: 106.291829ms; Post Handling: 42.358us +242 Text + +Decrypting Proteus. CheckingSession: 7.767496ms; Decrypting: 85.712199ms; Saving Session: 56.541259ms +Processing Application Message. LegalHold: 19.073527ms; Actually Handling: 112.489461ms; Post Handling: 45.288us +322 Text + +Decrypting Proteus. CheckingSession: 11.468709ms; Decrypting: 60.700643ms; Saving Session: 13.350546ms +Processing Application Message. LegalHold: 8.505982ms; Actually Handling: 50.083130ms; Post Handling: 45.532us +150 Text + +Decrypting Proteus. CheckingSession: 6.188883ms; Decrypting: 13.053426ms; Saving Session: 85.124837ms +Processing Application Message. LegalHold: 21.323039ms; Actually Handling: 52.064534ms; Post Handling: 44.271us +183 Text + +Decrypting Proteus. CheckingSession: 13.070516ms; Decrypting: 28.716634ms; Saving Session: 30.298055ms +Processing Application Message. LegalHold: 4.070516ms; Actually Handling: 62.070149ms; Post Handling: 46.346us +161 Text + +Decrypting Proteus. CheckingSession: 6.138061ms; Decrypting: 12.006470ms; Saving Session: 14.416707ms +Processing Application Message. LegalHold: 5.838745ms; Actually Handling: 40.329020ms; Post Handling: 45.084us +104 Text + +Decrypting Proteus. CheckingSession: 18.136312ms; Decrypting: 56.142212ms; Saving Session: 10.411458ms +Processing Application Message. LegalHold: 5.751913ms; Actually Handling: 79.311198ms; Post Handling: 86.67us +178 Text + +Decrypting Proteus. CheckingSession: 5.159220ms; Decrypting: 22.406942ms; Saving Session: 43.825399ms +Processing Application Message. LegalHold: 3.482463ms; Actually Handling: 78.667847ms; Post Handling: 46.102us +159 Text + +Decrypting Proteus. CheckingSession: 22.073405ms; Decrypting: 15.910563ms; Saving Session: 18.681885ms +Processing Application Message. LegalHold: 4.313762ms; Actually Handling: 49.198934ms; Post Handling: 42.603us +127 Text + +Decrypting Proteus. CheckingSession: 7.837036ms; Decrypting: 11.566772ms; Saving Session: 25.379231ms +Processing Application Message. LegalHold: 4.045329ms; Actually Handling: 119.658447ms; Post Handling: 45.573us +174 Text + +Decrypting Proteus. CheckingSession: 37.247396ms; Decrypting: 50.347819ms; Saving Session: 31.310221ms +Processing Application Message. LegalHold: 18.579997ms; Actually Handling: 139.728719ms; Post Handling: 90.698us +306 Text + +Decrypting Proteus. CheckingSession: 6.503500ms; Decrypting: 10.227702ms; Saving Session: 17.202026ms +Processing Application Message. LegalHold: 12.241333ms; Actually Handling: 84.598836ms; Post Handling: 43.294us +161 Text + +Decrypting Proteus. CheckingSession: 7.555704ms; Decrypting: 77.212647ms; Saving Session: 60.606690ms +Processing Application Message. LegalHold: 26.573812ms; Actually Handling: 55.140462ms; Post Handling: 57.862us +233 Text + +Decrypting Proteus. CheckingSession: 6.635539ms; Decrypting: 76.914876ms; Saving Session: 12.664348ms +Processing Application Message. LegalHold: 7.748047ms; Actually Handling: 104.782104ms; Post Handling: 45.044us +218 Text + +Decrypting Proteus. CheckingSession: 9.785726ms; Decrypting: 17.814534ms; Saving Session: 10.872436ms +Processing Application Message. LegalHold: 5.510823ms; Actually Handling: 30.021362ms; Post Handling: 46.021us +86 Text + +Decrypting Proteus. CheckingSession: 7.540120ms; Decrypting: 10.186483ms; Saving Session: 8.016560ms +Processing Application Message. LegalHold: 3.805827ms; Actually Handling: 87.553345ms; Post Handling: 45.899us +122 Text + +Decrypting Proteus. CheckingSession: 14.270101ms; Decrypting: 28.638753ms; Saving Session: 92.611044ms +Processing Application Message. LegalHold: 4.849487ms; Actually Handling: 106.860189ms; Post Handling: 61.442us +313 Text + +Decrypting Proteus. CheckingSession: 18.783529ms; Decrypting: 14.807129ms; Saving Session: 35.545858ms +Processing Application Message. LegalHold: 28.228353ms; Actually Handling: 144.122518ms; Post Handling: 44.719us +261 Text + +Decrypting Proteus. CheckingSession: 9.662638ms; Decrypting: 10.893880ms; Saving Session: 32.210978ms +Processing Application Message. LegalHold: 4.977783ms; Actually Handling: 105.997029ms; Post Handling: 45.451us +201 Text + +Decrypting Proteus. CheckingSession: 58.855998ms; Decrypting: 15.815918ms; Saving Session: 46.989014ms +Processing Application Message. LegalHold: 39.791626ms; Actually Handling: 101.596924ms; Post Handling: 45.573us +286 Text + +Decrypting Proteus. CheckingSession: 48.868653ms; Decrypting: 82.024048ms; Saving Session: 15.148235ms +Processing Application Message. LegalHold: 6.876343ms; Actually Handling: 72.648153ms; Post Handling: 44.027us +275 Text + +Decrypting Proteus. CheckingSession: 4.992024ms; Decrypting: 34.686604ms; Saving Session: 17.456991ms +Processing Application Message. LegalHold: 14.882975ms; Actually Handling: 86.396240ms; Post Handling: 47.282us +164 Text + +Decrypting Proteus. CheckingSession: 27.943970ms; Decrypting: 28.186035ms; Saving Session: 18.758708ms +Processing Application Message. LegalHold: 24.808106ms; Actually Handling: 92.472941ms; Post Handling: 69.824us +203 Text + +Decrypting Proteus. CheckingSession: 27.895060ms; Decrypting: 66.050049ms; Saving Session: 30.500814ms +Processing Application Message. LegalHold: 3.519571ms; Actually Handling: 191.166545ms; Post Handling: 47.893us +325 Text + +Decrypting Proteus. CheckingSession: 48.456991ms; Decrypting: 90.494141ms; Saving Session: 57.134359ms +Processing Application Message. LegalHold: 13.465983ms; Actually Handling: 161.665446ms; Post Handling: 41.748us +378 Text + +Decrypting Proteus. CheckingSession: 44.470988ms; Decrypting: 53.007121ms; Saving Session: 49.168823ms +Processing Application Message. LegalHold: 33.682983ms; Actually Handling: 122.440349ms; Post Handling: 43.538us +337 Text + +Decrypting Proteus. CheckingSession: 12.033041ms; Decrypting: 35.181274ms; Saving Session: 9.400960ms +Processing Application Message. LegalHold: 16.091227ms; Actually Handling: 79.925822ms; Post Handling: 44.108us +158 Text diff --git a/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt b/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt new file mode 100644 index 00000000000..55f2a282b2d --- /dev/null +++ b/docs/notebooks/event-performance-investigation/logs/NotSavingSession.txt @@ -0,0 +1,156 @@ + +Decrypting Proteus. CheckingSession: 15.454061ms; Decrypting: 28.070231ms; Saving Session: 5.697us +Processing Application Message. LegalHold: 14.933us; Actually Handling: 97.785929ms; Post Handling: 182.657us +412 LastRead + +Decrypting Proteus. CheckingSession: 11.785157ms; Decrypting: 22.148885ms; Saving Session: 4.069us +Processing Application Message. LegalHold: 10.174601ms; Actually Handling: 181.047770ms; Post Handling: 108.154us +267 Text + +Decrypting Proteus. CheckingSession: 17.730428ms; Decrypting: 16.207683ms; Saving Session: 4.761us +Processing Application Message. LegalHold: 9.501302ms; Actually Handling: 75.200358ms; Post Handling: 108.805us +140 Text + +Decrypting Proteus. CheckingSession: 95.788696ms; Decrypting: 36.985514ms; Saving Session: 5.045us +Processing Application Message. LegalHold: 5.578369ms; Actually Handling: 138.195842ms; Post Handling: 89.518us +292 Text + +Decrypting Proteus. CheckingSession: 29.748698ms; Decrypting: 45.943482ms; Saving Session: 3.866us +Processing Application Message. LegalHold: 4.784953ms; Actually Handling: 346.692790ms; Post Handling: 89.518us +444 Text + +Decrypting Proteus. CheckingSession: 8.107707ms; Decrypting: 21.656494ms; Saving Session: 3.825us +Processing Application Message. LegalHold: 14.723348ms; Actually Handling: 43.679484ms; Post Handling: 90.698us +127 Text + +Decrypting Proteus. CheckingSession: 42.428589ms; Decrypting: 50.376099ms; Saving Session: 3.988us +Processing Application Message. LegalHold: 5.829631ms; Actually Handling: 104.662761ms; Post Handling: 92.082us +226 Text + +Decrypting Proteus. CheckingSession: 17.885702ms; Decrypting: 108.601074ms; Saving Session: 4.965us +Processing Application Message. LegalHold: 33.264404ms; Actually Handling: 58.645386ms; Post Handling: 91.39us +231 Text + +Decrypting Proteus. CheckingSession: 85.114421ms; Decrypting: 105.152222ms; Saving Session: 5.127us +Processing Application Message. LegalHold: 4.807495ms; Actually Handling: 89.738851ms; Post Handling: 86.548us +327 Text + +Decrypting Proteus. CheckingSession: 15.756429ms; Decrypting: 27.951905ms; Saving Session: 3.906us +Processing Application Message. LegalHold: 9.168823ms; Actually Handling: 87.466471ms; Post Handling: 93.14us +147 Text + +Decrypting Proteus. CheckingSession: 44.635172ms; Decrypting: 60.306600ms; Saving Session: 4.761us +Processing Application Message. LegalHold: 4.242594ms; Actually Handling: 82.865194ms; Post Handling: 95.621us +205 Text + +Decrypting Proteus. CheckingSession: 58.932658ms; Decrypting: 92.710083ms; Saving Session: 5.209us +Processing Application Message. LegalHold: 10.136637ms; Actually Handling: 139.580038ms; Post Handling: 89.274us +330 Text + +Decrypting Proteus. CheckingSession: 33.654093ms; Decrypting: 106.929199ms; Saving Session: 4.964us +Processing Application Message. LegalHold: 6.297811ms; Actually Handling: 125.664958ms; Post Handling: 106.893us +286 Text + +Decrypting Proteus. CheckingSession: 37.078735ms; Decrypting: 110.817546ms; Saving Session: 5.005us +Processing Application Message. LegalHold: 28.246907ms; Actually Handling: 121.051514ms; Post Handling: 87.687us +314 Text + +Decrypting Proteus. CheckingSession: 7.747396ms; Decrypting: 45.533691ms; Saving Session: 5.818us +Processing Application Message. LegalHold: 9.863688ms; Actually Handling: 101.962199ms; Post Handling: 90.902us +172 Text + +Decrypting Proteus. CheckingSession: 8.374227ms; Decrypting: 59.100220ms; Saving Session: 3.866us +Processing Application Message. LegalHold: 4.148681ms; Actually Handling: 111.556722ms; Post Handling: 89.559us +198 Text + +Decrypting Proteus. CheckingSession: 61.912313ms; Decrypting: 69.451416ms; Saving Session: 4.965us +Processing Application Message. LegalHold: 5.938232ms; Actually Handling: 210.333130ms; Post Handling: 138.346us +360 Text + +Decrypting Proteus. CheckingSession: 15.601196ms; Decrypting: 10.290812ms; Saving Session: 3.744us +Processing Application Message. LegalHold: 7.292155ms; Actually Handling: 26.934814ms; Post Handling: 43.091us +124 Text + +Decrypting Proteus. CheckingSession: 48.981486ms; Decrypting: 13.180949ms; Saving Session: 3.744us +Processing Application Message. LegalHold: 43.387492ms; Actually Handling: 84.452922ms; Post Handling: 41.789us +232 Text + +Decrypting Proteus. CheckingSession: 10.598593ms; Decrypting: 39.504395ms; Saving Session: 4.232us +Processing Application Message. LegalHold: 5.250448ms; Actually Handling: 65.109619ms; Post Handling: 53.996us +151 Text + +Decrypting Proteus. CheckingSession: 21.209838ms; Decrypting: 62.311402ms; Saving Session: 5.209us +Processing Application Message. LegalHold: 6.484538ms; Actually Handling: 57.000326ms; Post Handling: 39.632us +181 Text + +Decrypting Proteus. CheckingSession: 6.562581ms; Decrypting: 55.756226ms; Saving Session: 5.127us +Processing Application Message. LegalHold: 5.546671ms; Actually Handling: 37.751709ms; Post Handling: 40.405us +112 Text + +Decrypting Proteus. CheckingSession: 40.094686ms; Decrypting: 24.501017ms; Saving Session: 3.906us +Processing Application Message. LegalHold: 4.171590ms; Actually Handling: 61.336955ms; Post Handling: 44.149us +162 Text + +Decrypting Proteus. CheckingSession: 21.579956ms; Decrypting: 26.329590ms; Saving Session: 4.191us +Processing Application Message. LegalHold: 4.704508ms; Actually Handling: 20.097859ms; Post Handling: 41.381us +80 Text + +Decrypting Proteus. CheckingSession: 6.706909ms; Decrypting: 22.204875ms; Saving Session: 5.818us +Processing Application Message. LegalHold: 9.896729ms; Actually Handling: 16.063802ms; Post Handling: 41.423us +72 Text + +Decrypting Proteus. CheckingSession: 15.992961ms; Decrypting: 23.335937ms; Saving Session: 4.883us +Processing Application Message. LegalHold: 6.042847ms; Actually Handling: 14.047811ms; Post Handling: 40.608us +68 Text + +Decrypting Proteus. CheckingSession: 33.254761ms; Decrypting: 33.165324ms; Saving Session: 4.151us +Processing Application Message. LegalHold: 10.563924ms; Actually Handling: 25.756714ms; Post Handling: 46.956us +122 Text + +Decrypting Proteus. CheckingSession: 40.167928ms; Decrypting: 55.158122ms; Saving Session: 4.191us +Processing Application Message. LegalHold: 7.033366ms; Actually Handling: 45.535278ms; Post Handling: 40.975us +191 Text + +Decrypting Proteus. CheckingSession: 20.063924ms; Decrypting: 28.767172ms; Saving Session: 4.069us +Processing Application Message. LegalHold: 4.934366ms; Actually Handling: 29.515666ms; Post Handling: 40.731us +89 Text + +Decrypting Proteus. CheckingSession: 15.520548ms; Decrypting: 57.723185ms; Saving Session: 5.046us +Processing Application Message. LegalHold: 4.028361ms; Actually Handling: 16.930623ms; Post Handling: 42.196us +104 Text + +Decrypting Proteus. CheckingSession: 6.614787ms; Decrypting: 99.018148ms; Saving Session: 5.005us +Processing Application Message. LegalHold: 40.461263ms; Actually Handling: 24.233317ms; Post Handling: 41.341us +177 Text + +Decrypting Proteus. CheckingSession: 19.239502ms; Decrypting: 19.057088ms; Saving Session: 3.907us +Processing Application Message. LegalHold: 6.788941ms; Actually Handling: 21.477051ms; Post Handling: 44.759us +72 Text + +Decrypting Proteus. CheckingSession: 20.884278ms; Decrypting: 13.739380ms; Saving Session: 4.761us +Processing Application Message. LegalHold: 19.949260ms; Actually Handling: 11.556518ms; Post Handling: 37.191us +72 Text + +Decrypting Proteus. CheckingSession: 22.686971ms; Decrypting: 12.227987ms; Saving Session: 4.68us +Processing Application Message. LegalHold: 4.054525ms; Actually Handling: 58.352865ms; Post Handling: 40.446us +109 Text + +Decrypting Proteus. CheckingSession: 33.300741ms; Decrypting: 69.654378ms; Saving Session: 4.964us +Processing Application Message. LegalHold: 4.356689ms; Actually Handling: 120.923299ms; Post Handling: 47.363us +240 Text + +Decrypting Proteus. CheckingSession: 33.834310ms; Decrypting: 18.699748ms; Saving Session: 5.452us +Processing Application Message. LegalHold: 4.842203ms; Actually Handling: 52.177408ms; Post Handling: 54.932us +117 Text + +Decrypting Proteus. CheckingSession: 40.743897ms; Decrypting: 188.739014ms; Saving Session: 6.958us +Processing Application Message. LegalHold: 79.312296ms; Actually Handling: 47.106241ms; Post Handling: 42.48us +373 Text + +Decrypting Proteus. CheckingSession: 52.185547ms; Decrypting: 21.590617ms; Saving Session: 5.005us +Processing Application Message. LegalHold: 10.031861ms; Actually Handling: 106.708211ms; Post Handling: 47.729us +201 Text + +Decrypting Proteus. CheckingSession: 12.232300ms; Decrypting: 43.849935ms; Saving Session: 4.923us +Processing Application Message. LegalHold: 25.414754ms; Actually Handling: 16.168091ms; Post Handling: 41.748us +106 Text diff --git a/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt b/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt new file mode 100644 index 00000000000..f396d1b8a7b --- /dev/null +++ b/docs/notebooks/event-performance-investigation/logs/WithExistingSessionCache.txt @@ -0,0 +1,208 @@ + +Decrypting Proteus. CheckingSession: 171.63us; Decrypting: 23.412150ms; Saving Session: 4.72us +Processing Application Message. LegalHold: 17.415us; Actually Handling: 30.606893ms; Post Handling: 61.157us +65 LastRead + +Decrypting Proteus. CheckingSession: 130.615us; Decrypting: 23.862264ms; Saving Session: 4.761us +Processing Application Message. LegalHold: 8.499105ms; Actually Handling: 87.421549ms; Post Handling: 47.689us +128 Text + +Decrypting Proteus. CheckingSession: 132.447us; Decrypting: 40.465699ms; Saving Session: 4.883us +Processing Application Message. LegalHold: 43.508220ms; Actually Handling: 84.078043ms; Post Handling: 43.498us +191 Text + +Decrypting Proteus. CheckingSession: 97.819us; Decrypting: 35.966390ms; Saving Session: 3.947us +Processing Application Message. LegalHold: 22.293782ms; Actually Handling: 82.308838ms; Post Handling: 41.015us +147 Text + +Decrypting Proteus. CheckingSession: 109.823us; Decrypting: 67.539388ms; Saving Session: 4.76us +Processing Application Message. LegalHold: 18.875651ms; Actually Handling: 39.499064ms; Post Handling: 41.422us +143 Text + +Decrypting Proteus. CheckingSession: 88.786us; Decrypting: 34.672689ms; Saving Session: 4.435us +Processing Application Message. LegalHold: 12.254029ms; Actually Handling: 59.550049ms; Post Handling: 42.155us +112 Text + +Decrypting Proteus. CheckingSession: 100.057us; Decrypting: 56.516113ms; Saving Session: 4.476us +Processing Application Message. LegalHold: 4.922648ms; Actually Handling: 43.885620ms; Post Handling: 40.935us +133 Text + +Decrypting Proteus. CheckingSession: 99.203us; Decrypting: 76.217733ms; Saving Session: 5.168us +Processing Application Message. LegalHold: 4.521729ms; Actually Handling: 74.523437ms; Post Handling: 65.186us +168 Text + +Decrypting Proteus. CheckingSession: 146.891us; Decrypting: 41.369263ms; Saving Session: 5.94us +Processing Application Message. LegalHold: 17.425049ms; Actually Handling: 160.854289ms; Post Handling: 369.913us +251 Text + +Decrypting Proteus. CheckingSession: 156.779us; Decrypting: 69.674438ms; Saving Session: 5.331us +Processing Application Message. LegalHold: 29.853068ms; Actually Handling: 32.974365ms; Post Handling: 44.556us +180 Text + +Decrypting Proteus. CheckingSession: 97.412us; Decrypting: 194.863933ms; Saving Session: 4.721us +Processing Application Message. LegalHold: 8.449300ms; Actually Handling: 110.715779ms; Post Handling: 47.607us +343 Text + +Decrypting Proteus. CheckingSession: 143.88us; Decrypting: 63.876993ms; Saving Session: 4.72us +Processing Application Message. LegalHold: 26.663046ms; Actually Handling: 100.153483ms; Post Handling: 43.01us +200 Text + +Decrypting Proteus. CheckingSession: 102.051us; Decrypting: 33.846557ms; Saving Session: 3.988us +Processing Application Message. LegalHold: 32.440105ms; Actually Handling: 77.101400ms; Post Handling: 50.13us +171 Text + +Decrypting Proteus. CheckingSession: 90.82us; Decrypting: 82.648763ms; Saving Session: 8.22us +Processing Application Message. LegalHold: 13.928873ms; Actually Handling: 59.246338ms; Post Handling: 74.911us +164 Text + +Decrypting Proteus. CheckingSession: 81.95us; Decrypting: 20.931438ms; Saving Session: 3.377us +Processing Application Message. LegalHold: 22.170573ms; Actually Handling: 54.304688ms; Post Handling: 60.628us +104 Text + +Decrypting Proteus. CheckingSession: 112.955us; Decrypting: 30.412068ms; Saving Session: 3.621us +Processing Application Message. LegalHold: 7.205160ms; Actually Handling: 56.475545ms; Post Handling: 48.869us +102 Text + +Decrypting Proteus. CheckingSession: 85.368us; Decrypting: 36.424520ms; Saving Session: 5.005us +Processing Application Message. LegalHold: 4.438477ms; Actually Handling: 30.431315ms; Post Handling: 40.771us +77 Text + +Decrypting Proteus. CheckingSession: 80.892us; Decrypting: 55.061971ms; Saving Session: 4.517us +Processing Application Message. LegalHold: 13.422851ms; Actually Handling: 23.873372ms; Post Handling: 40.243us +103 Text + +Decrypting Proteus. CheckingSession: 90.047us; Decrypting: 115.897624ms; Saving Session: 5.045us +Processing Application Message. LegalHold: 5.418457ms; Actually Handling: 63.461101ms; Post Handling: 41.667us +191 Text + +Decrypting Proteus. CheckingSession: 80.933us; Decrypting: 67.734945ms; Saving Session: 4.394us +Processing Application Message. LegalHold: 7.931397ms; Actually Handling: 45.393474ms; Post Handling: 39.795us +129 Text + +Decrypting Proteus. CheckingSession: 96.436us; Decrypting: 55.728027ms; Saving Session: 6.144us +Processing Application Message. LegalHold: 4.656291ms; Actually Handling: 82.777262ms; Post Handling: 55.502us +204 Text + +Decrypting Proteus. CheckingSession: 134.074us; Decrypting: 31.648315ms; Saving Session: 3.662us +Processing Application Message. LegalHold: 5.795695ms; Actually Handling: 41.531982ms; Post Handling: 40.894us +117 Text + +Decrypting Proteus. CheckingSession: 125.529us; Decrypting: 32.789713ms; Saving Session: 4.882us +Processing Application Message. LegalHold: 5.650310ms; Actually Handling: 23.350098ms; Post Handling: 41.178us +73 Text + +Decrypting Proteus. CheckingSession: 83.821us; Decrypting: 101.161296ms; Saving Session: 4.639us +Processing Application Message. LegalHold: 5.676758ms; Actually Handling: 84.787313ms; Post Handling: 42.643us +198 Text + +Decrypting Proteus. CheckingSession: 101.929us; Decrypting: 100.994466ms; Saving Session: 4.476us +Processing Application Message. LegalHold: 6.584229ms; Actually Handling: 44.870361ms; Post Handling: 43.701us +165 Text + +Decrypting Proteus. CheckingSession: 101.44us; Decrypting: 40.139079ms; Saving Session: 5.656us +Processing Application Message. LegalHold: 23.923584ms; Actually Handling: 73.660075ms; Post Handling: 40.161us +190 Text + +Decrypting Proteus. CheckingSession: 97.575us; Decrypting: 13.363729ms; Saving Session: 3.459us +Processing Application Message. LegalHold: 15.317383ms; Actually Handling: 19.124390ms; Post Handling: 45.573us +78 Text + +Decrypting Proteus. CheckingSession: 114.461us; Decrypting: 18.798991ms; Saving Session: 4.435us +Processing Application Message. LegalHold: 7.636108ms; Actually Handling: 95.518391ms; Post Handling: 62.418us +172 Text + +Decrypting Proteus. CheckingSession: 89.396us; Decrypting: 45.923869ms; Saving Session: 5.046us +Processing Application Message. LegalHold: 30.199504ms; Actually Handling: 31.469930ms; Post Handling: 57.617us +124 Text + +Decrypting Proteus. CheckingSession: 117.187us; Decrypting: 68.765584ms; Saving Session: 6.348us +Processing Application Message. LegalHold: 19.563192ms; Actually Handling: 70.637858ms; Post Handling: 68.074us +172 Text + +Decrypting Proteus. CheckingSession: 206.543us; Decrypting: 110.533895ms; Saving Session: 9.684us +Processing Application Message. LegalHold: 9.586548ms; Actually Handling: 19.422160ms; Post Handling: 44.719us +155 Text + +Decrypting Proteus. CheckingSession: 84.798us; Decrypting: 28.744751ms; Saving Session: 4.558us +Processing Application Message. LegalHold: 4.979573ms; Actually Handling: 18.377157ms; Post Handling: 39.998us +74 Text + +Decrypting Proteus. CheckingSession: 178.101us; Decrypting: 14.005493ms; Saving Session: 3.337us +Processing Application Message. LegalHold: 4.134807ms; Actually Handling: 30.131144ms; Post Handling: 38.492us +54 Text + +Decrypting Proteus. CheckingSession: 92.407us; Decrypting: 70.199544ms; Saving Session: 4.679us +Processing Application Message. LegalHold: 13.248739ms; Actually Handling: 114.933187ms; Post Handling: 39.998us +259 Text + +Decrypting Proteus. CheckingSession: 159.424us; Decrypting: 41.670899ms; Saving Session: 4.313us +Processing Application Message. LegalHold: 28.232829ms; Actually Handling: 16.727539ms; Post Handling: 36.621us +103 Text + +Decrypting Proteus. CheckingSession: 92.529us; Decrypting: 64.359416ms; Saving Session: 4.028us +Processing Application Message. LegalHold: 4.952107ms; Actually Handling: 14.338094ms; Post Handling: 54.932us +89 Text + +Decrypting Proteus. CheckingSession: 101.888us; Decrypting: 49.372478ms; Saving Session: 5.249us +Processing Application Message. LegalHold: 36.024455ms; Actually Handling: 41.159505ms; Post Handling: 43.905us +168 Text + +Decrypting Proteus. CheckingSession: 85.205us; Decrypting: 14.965413ms; Saving Session: 4.028us +Processing Application Message. LegalHold: 5.274007ms; Actually Handling: 32.183391ms; Post Handling: 44.271us +58 Text + +Decrypting Proteus. CheckingSession: 92.082us; Decrypting: 86.730713ms; Saving Session: 5.086us +Processing Application Message. LegalHold: 4.609131ms; Actually Handling: 38.390340ms; Post Handling: 45.166us +135 Text + +Decrypting Proteus. CheckingSession: 101.725us; Decrypting: 15.713461ms; Saving Session: 3.662us +Processing Application Message. LegalHold: 4.611979ms; Actually Handling: 47.136109ms; Post Handling: 50.904us +92 Text + +Decrypting Proteus. CheckingSession: 86.1us; Decrypting: 74.593669ms; Saving Session: 6.755us +Processing Application Message. LegalHold: 4.965617ms; Actually Handling: 26.958374ms; Post Handling: 41.015us +136 Text + +Decrypting Proteus. CheckingSession: 94.523us; Decrypting: 26.139405ms; Saving Session: 3.58us +Processing Application Message. LegalHold: 14.431519ms; Actually Handling: 46.557496ms; Post Handling: 3.227986ms +97 Text + +Decrypting Proteus. CheckingSession: 149.211us; Decrypting: 38.674520ms; Saving Session: 3.58us +Processing Application Message. LegalHold: 5.177002ms; Actually Handling: 76.428630ms; Post Handling: 78.125us +145 Text + +Decrypting Proteus. CheckingSession: 115.845us; Decrypting: 18.225139ms; Saving Session: 4.476us +Processing Application Message. LegalHold: 19.205403ms; Actually Handling: 76.934448ms; Post Handling: 62.622us +145 Text + +Decrypting Proteus. CheckingSession: 103.231us; Decrypting: 53.127604ms; Saving Session: 5.005us +Processing Application Message. LegalHold: 5.670289ms; Actually Handling: 56.225545ms; Post Handling: 58.675us +151 Text + +Decrypting Proteus. CheckingSession: 82.885us; Decrypting: 115.505900ms; Saving Session: 8.789us +Processing Application Message. LegalHold: 11.379720ms; Actually Handling: 99.572673ms; Post Handling: 122.192us +263 Text + +Decrypting Proteus. CheckingSession: 183.756us; Decrypting: 104.482625ms; Saving Session: 6.714us +Processing Application Message. LegalHold: 13.235555ms; Actually Handling: 78.640258ms; Post Handling: 54.525us +204 Text + +Decrypting Proteus. CheckingSession: 111.206us; Decrypting: 36.096680ms; Saving Session: 6.307us +Processing Application Message. LegalHold: 31.570028ms; Actually Handling: 24.436483ms; Post Handling: 46.712us +160 Text + +Decrypting Proteus. CheckingSession: 79.671us; Decrypting: 92.863240ms; Saving Session: 4.965us +Processing Application Message. LegalHold: 50.057739ms; Actually Handling: 69.449626ms; Post Handling: 61.482us +221 Text + +Decrypting Proteus. CheckingSession: 1.046183ms; Decrypting: 72.940877ms; Saving Session: 5.208us +Processing Application Message. LegalHold: 5.366862ms; Actually Handling: 11.162923ms; Post Handling: 38.615us +116 Text + +Decrypting Proteus. CheckingSession: 71.37us; Decrypting: 22.764160ms; Saving Session: 4.191us +Processing Application Message. LegalHold: 9.260538ms; Actually Handling: 45.517619ms; Post Handling: 43.132us +92 Text + +Decrypting Proteus. CheckingSession: 80.119us; Decrypting: 40.078939ms; Saving Session: 4.476us +Processing Application Message. LegalHold: 6.736613ms; Actually Handling: 83.116537ms; Post Handling: 42.521us +138 Text diff --git a/gradle.properties b/gradle.properties index f49ce3e6c56..e5a644b6f8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,3 +27,4 @@ org.gradle.parallel=true # Support KMP Gradle Composite Builds - See https://youtrack.jetbrains.com/issue/KT-52172/ kotlin.mpp.import.enableKgpDependencyResolution=true +kotlin.native.ignoreDisabledTargets=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ecb66216eff..7403b9b0317 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,12 @@ [versions] kotlin = "1.9.23" +ksp = "1.9.23-1.0.20" activity-compose = "1.9.0" app-compat = "1.6.1" android-paging3 = "3.2.1" cli-kt = "3.5.0" -coroutines = "1.8.0" -compose-compiler = "1.5.11" +coroutines = "1.8.1" +compose-compiler = "1.5.13" compose-ui = "1.6.6" compose-material = "1.6.6" cryptobox4j = "1.4.0" @@ -16,12 +17,12 @@ okio = "3.9.0" ok-http = "4.12.0" mockative = "2.2.0" android-work = "2.9.0" -android-test-runner = "1.5.2" -android-test-core-ktx = "1.5.0" -android-test-rules = "1.5.0" -android-test-core = "1.5.0" +android-test-runner = "1.6.2" +android-test-core-ktx = "1.6.1" +android-test-rules = "1.6.1" +android-test-core = "1.6.1" androidx-arch = "2.2.0" -androidx-test-orchestrator = "1.4.2" +androidx-test-orchestrator = "1.5.1" androidx-sqlite = "2.4.0" benasher-uuid = "0.8.0" ktx-datetime = { strictly = "0.5.0" } @@ -36,20 +37,20 @@ sqldelight = "2.0.2" sqlcipher-android = "4.5.6" pbandk = "0.14.2" turbine = "1.1.0" -avs = "9.8.15" +avs = "10.0.1" jna = "5.14.0" -core-crypto = "1.0.0-rc.56-hotfix.2" +core-crypto = "1.0.2" core-crypto-multiplatform = "0.6.0-rc.3-multiplatform-pre1" completeKotlin = "1.1.0" -desugar-jdk = "2.0.4" +desugar-jdk = "2.1.3" kermit = "2.0.3" detekt = "1.23.6" -agp = "8.3.2" +agp = "8.5.2" dokka = "1.8.20" carthage = "0.0.1" libsodiumBindings = "0.8.7" protobufCodegen = "0.9.4" -annotation = "1.7.1" +annotation = "1.9.1" mordant = "2.0.0-beta13" apache-tika = "2.9.2" mockk = "1.13.10" @@ -66,6 +67,7 @@ benchmark = "0.4.10" jmh = "1.37" jmhReport = "0.9.6" xerialDriver = "3.45.3.0" +kotlinx-io = "0.5.3" [plugins] # Home-made convention plugins @@ -78,7 +80,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } -ksp = { id = "com.google.devtools.ksp", version = "1.9.23-1.0.20" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } carthage = { id = "com.wire.carthage-gradle-plugin", version.ref = "carthage" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } protobuf = { id = "com.google.protobuf", version.ref = "protobufCodegen" } @@ -102,6 +104,7 @@ ktxSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json" ktxDateTime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "ktx-datetime" } ktx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "ktx-atomicfu" } ktxReactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive", version.ref = "coroutines" } +ktxIO = { module = "org.jetbrains.kotlinx:kotlinx-io-core", version.ref = "kotlinx-io"} # android dependencies appCompat = { module = "androidx.appcompat:appcompat", version.ref = "app-compat" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae..e6441136f3d 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 74c71905115..2a551a737fa 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -18,6 +18,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionSha256Sum=f8b4f4772d302c8ff580bc40d0f56e715de69b163546944f787c87abf209c961 distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index ca8867d1cca..b740cf13397 100755 --- a/gradlew +++ b/gradlew @@ -1,67 +1,115 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MSYS* | MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -71,9 +119,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -82,88 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c4e..25da30dbdee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +41,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt index 8e77dfb90ac..f029a6bf0df 100644 --- a/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt +++ b/logger/src/commonMain/kotlin/com/wire/kalium/logger/KaliumLogger.kt @@ -259,7 +259,7 @@ class KaliumLogger( enum class ApplicationFlow { SYNC, EVENT_RECEIVER, CONVERSATIONS, CONNECTIONS, MESSAGES, SEARCH, SESSION, REGISTER, - CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS + CLIENTS, CALLING, ASSETS, LOCAL_STORAGE, ANALYTICS, CONVERSATIONS_FOLDERS } } } diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt index 7b87ab28bc6..2e89948aefd 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/CallManagerTest.kt @@ -21,22 +21,26 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.calling.Calling import com.wire.kalium.calling.types.Handle import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater +import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.network.NetworkStateObserver import io.mockative.Mock import io.mockative.any import io.mockative.eq @@ -44,14 +48,10 @@ import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant import kotlin.test.BeforeTest import kotlin.test.Ignore import kotlin.test.Test -import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater -import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider -import com.wire.kalium.network.NetworkStateObserver -import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString -import kotlinx.datetime.Instant class CallManagerTest { @@ -79,6 +79,9 @@ class CallManagerTest { @Mock private val selfConversationIdProvider = mock(SelfConversationIdProvider::class) + @Mock + private val userConfigRepository = mock(UserConfigRepository::class) + @Mock private val conversationRepository = mock(ConversationRepository::class) @@ -117,6 +120,7 @@ class CallManagerTest { currentClientIdProvider = currentClientIdProvider, selfConversationIdProvider = selfConversationIdProvider, conversationRepository = conversationRepository, + userConfigRepository = userConfigRepository, messageSender = messageSender, kaliumDispatchers = dispatcher, federatedIdMapper = federatedIdMapper, @@ -128,7 +132,7 @@ class CallManagerTest { networkStateObserver = networkStateObserver, kaliumConfigs = kaliumConfigs, mediaManagerService = mediaManagerService, - flowManagerService = flowManagerService, + flowManagerService = flowManagerService ) } diff --git a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt index c2c58431013..1bb4efa23d1 100644 --- a/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt +++ b/logic/src/androidInstrumentedTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCallTest.kt @@ -17,13 +17,13 @@ */ package com.wire.kalium.logic.feature.call.scenario -import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.calling.ConversationTypeCalling +import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.call.ConversationTypeForCall import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl -import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.framework.TestConversation @@ -33,11 +33,13 @@ import io.mockative.coVerify import io.mockative.eq import io.mockative.mock import io.mockative.once +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class OnIncomingCallTest { val testScope = TestScope() @@ -64,7 +66,7 @@ class OnIncomingCallTest { eq(TestConversation.CONVERSATION.id), eq(ConversationTypeForCall.Conference), eq(CallStatus.INCOMING), - eq(TestUser.USER_ID.toString()), + eq(TestUser.USER_ID), eq(true), eq(false), eq(false) @@ -94,7 +96,7 @@ class OnIncomingCallTest { eq(TestConversation.CONVERSATION.id), eq(ConversationTypeForCall.Conference), eq(CallStatus.STILL_ONGOING), - eq(TestUser.USER_ID.toString()), + eq(TestUser.USER_ID), eq(true), eq(false), eq(false) diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt new file mode 100644 index 00000000000..86834de9f89 --- /dev/null +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScopeExtensions.kt @@ -0,0 +1,21 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation + +val ConversationScope.getPaginatedFlowOfConversationDetailsWithEventsBySearchQuery + get() = GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt new file mode 100644 index 00000000000..61bf99999e3 --- /dev/null +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase.kt @@ -0,0 +1,44 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation + +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.ConversationQueryConfig +import com.wire.kalium.util.KaliumDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn + +/** + * This use case will observe and return a flow of paginated searched conversation details with last message and unread events counts. + * @see PagingData + * @see ConversationDetailsWithEvents + */ +class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase internal constructor( + private val dispatcher: KaliumDispatcher, + private val conversationRepository: ConversationRepository, +) { + suspend operator fun invoke( + queryConfig: ConversationQueryConfig, + pagingConfig: PagingConfig, + startingOffset: Long, + ): Flow> = conversationRepository.extensions + .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset).flowOn(dispatcher.io) +} diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt index 1fe832a2ef7..c7782670592 100644 --- a/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImpl.kt @@ -22,7 +22,10 @@ import android.content.Context import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities +import android.os.Build +import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.logStructuredJson import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import com.wire.kalium.util.KaliumDispatcher @@ -32,6 +35,7 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -55,7 +59,8 @@ internal actual class NetworkStateObserverImpl( init { val initialDefaultNetworkData = connectivityManager.activeNetwork?.let { - val defaultNetworkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + val defaultNetworkCapabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) DefaultNetworkData.Connected(it, defaultNetworkCapabilities) } ?: DefaultNetworkData.NotConnected defaultNetworkDataStateFlow = MutableStateFlow(initialDefaultNetworkData) @@ -70,18 +75,38 @@ internal actual class NetworkStateObserverImpl( else networkData.networkCapabilities.toState() } else NetworkState.NotConnected } + .buffer(capacity = 0) .stateIn(scope, SharingStarted.Eagerly, initialState) val callback = object : ConnectivityManager.NetworkCallback() { - override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { super.onCapabilitiesChanged(network, networkCapabilities) - kaliumLogger.i( - "${NetworkStateObserver.TAG} capabilities changed " + - "internet:${networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)} " + - "validated:${networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)}" - ) - defaultNetworkDataStateFlow.update { DefaultNetworkData.Connected(network, networkCapabilities) } + val loggerMessage = mutableMapOf().apply { + put("internet", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).toString()) + put("validated", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED).toString()) + put("not restricted", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED).toString()) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + put("foreground", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND).toString()) + put("not congested", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED).toString()) + put("not suspended", networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED).toString()) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + put("signalStrength", networkCapabilities.signalStrength.toString()) + } + } + kaliumLogger.logStructuredJson(KaliumLogLevel.INFO, "${NetworkStateObserver.TAG} capabilities changed", loggerMessage) + defaultNetworkDataStateFlow.update { + DefaultNetworkData.Connected( + network, + networkCapabilities + ) + } } override fun onLost(network: Network) { @@ -110,8 +135,18 @@ internal actual class NetworkStateObserverImpl( override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { kaliumLogger.i("${NetworkStateObserver.TAG} block connection changed to $blocked") defaultNetworkDataStateFlow.update { - if (it is DefaultNetworkData.Connected) it.copy(isBlocked = blocked) - else it + val updatedValue = when (it) { + is DefaultNetworkData.Connected -> { + it.copy(isBlocked = blocked) + } + + is DefaultNetworkData.NotConnected -> { + if (blocked) it + else DefaultNetworkData.Connected(network) + } + } + kaliumLogger.d("${NetworkStateObserver.TAG} default network state $it changed to $updatedValue") + updatedValue } super.onBlockedStatusChanged(network, blocked) } diff --git a/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt b/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt index 8d024920776..b2cfe894414 100644 --- a/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt +++ b/logic/src/androidMain/kotlin/com/wire/kalium/logic/sync/ForegroundNotificationDetailsProvider.kt @@ -24,7 +24,7 @@ import androidx.annotation.DrawableRes * Provide resources that will be displayed when Kalium * needs to display a Foreground notification due to some work being done. */ -interface ForegroundNotificationDetailsProvider { +fun interface ForegroundNotificationDetailsProvider { @DrawableRes fun getSmallIconResId(): Int } diff --git a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt new file mode 100644 index 00000000000..31ace8e2c65 --- /dev/null +++ b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/feature/conversation/GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest.kt @@ -0,0 +1,82 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation + +import androidx.paging.PagingData +import app.cash.paging.PagingConfig +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationQueryConfig +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.ConversationRepositoryExtensions +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCaseTest { + private val dispatcher = TestKaliumDispatcher + + @Test + fun givenSearchQuery_whenGettingPaginatedList_thenCallUseCaseWithProperParams() = runTest(dispatcher.default) { + // Given + val (arrangement, useCase) = Arrangement().withPaginatedConversationResult(emptyFlow()).arrange() + with(arrangement) { + // When + useCase(queryConfig = queryConfig, pagingConfig = pagingConfig, startingOffset = startingOffset) + // Then + coVerify { + conversationRepository.extensions + .getPaginatedConversationDetailsWithEventsBySearchQuery(queryConfig, pagingConfig, startingOffset) + }.wasInvoked(exactly = once) + } + } + + inner class Arrangement { + @Mock + val conversationRepository = mock(ConversationRepository::class) + + @Mock + val conversationRepositoryExtensions = mock(ConversationRepositoryExtensions::class) + + val queryConfig = ConversationQueryConfig("search") + val pagingConfig = PagingConfig(20) + val startingOffset = 0L + + init { + every { + conversationRepository.extensions + }.returns(conversationRepositoryExtensions) + } + + suspend fun withPaginatedConversationResult(result: Flow>) = apply { + coEvery { + conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery(any(), any(), any()) + }.returns(result) + } + + fun arrange() = this to GetPaginatedFlowOfConversationDetailsWithEventsBySearchQueryUseCase(dispatcher, conversationRepository) + } +} diff --git a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt index 3dbc414f131..ecda6d43e7c 100644 --- a/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt +++ b/logic/src/androidUnitTest/kotlin/com/wire/kalium/logic/network/NetworkStateObserverImplTest.kt @@ -229,6 +229,21 @@ class NetworkStateObserverImplTest { } } + @Test + fun givenNetworkNotConnectedAndButBlocked_whenItChangesToNotBlocked_thenStateChangesToConnectedWithInternet() = + runTest(dispatcher.default) { + // given + val (arrangement, networkStateObserverImpl) = Arrangement() + .arrange() + // when-then + networkStateObserverImpl.observeNetworkState().test { + assertEquals(NetworkState.NotConnected, awaitItem()) + arrangement.connectNetwork(networkType = NetworkType.MOBILE, setAsDefault = true, withInternetValidated = true) + arrangement.changeNetworkBlocked(networkType = NetworkType.MOBILE, false) + assertEquals(NetworkState.ConnectedWithInternet, awaitItem()) + } + } + @Test fun givenOneNetworkConnectedWithoutInternetValidated_whenItChangesToBlocked_thenStateDoesNotChange() = runTest(dispatcher.default) { // given diff --git a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index f2a4229f634..1382845bf18 100644 --- a/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/appleMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -21,16 +21,17 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.conversation.ConversationRepository -import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.call.usecase.ConversationClientsInCallUpdater import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.message.MessageSender @@ -46,6 +47,7 @@ actual class GlobalCallManager { currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt index d4ba1b1e45e..a41535a1dc3 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/CoreCryptoExceptionMapper.kt @@ -32,6 +32,7 @@ actual fun mapMLSException(exception: Exception): MLSFailure = is CryptoError.StaleCommit -> MLSFailure.StaleCommit is CryptoError.ConversationAlreadyExists -> MLSFailure.ConversationAlreadyExists is CryptoError.MessageEpochTooOld -> MLSFailure.MessageEpochTooOld + is CryptoError.MlsException -> MLSFailure.InternalErrors else -> MLSFailure.Generic(exception) } } else { diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt index 9af3912b90a..90244f7eb76 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/CallManagerImpl.kt @@ -32,8 +32,10 @@ import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.callingLogger +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallClient import com.wire.kalium.logic.data.call.CallClientList +import com.wire.kalium.logic.data.call.CallHelperImpl import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.call.CallType @@ -76,6 +78,8 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.util.ServerTimeHandler +import com.wire.kalium.logic.util.ServerTimeHandlerImpl import com.wire.kalium.logic.util.toInt import com.wire.kalium.network.NetworkStateObserver import com.wire.kalium.util.KaliumDispatcher @@ -111,11 +115,13 @@ class CallManagerImpl internal constructor( private val conversationClientsInCallUpdater: ConversationClientsInCallUpdater, private val networkStateObserver: NetworkStateObserver, private val getCallConversationType: GetCallConversationTypeProvider, + private val userConfigRepository: UserConfigRepository, private val kaliumConfigs: KaliumConfigs, private val mediaManagerService: MediaManagerService, private val flowManagerService: FlowManagerService, private val json: Json = Json { ignoreUnknownKeys = true }, private val shouldRemoteMuteChecker: ShouldRemoteMuteChecker = ShouldRemoteMuteCheckerImpl(), + private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(), kaliumDispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : CallManager { @@ -209,8 +215,12 @@ class CallManagerImpl internal constructor( .keepingStrongReference(), establishedCallHandler = OnEstablishedCall(callRepository, scope, qualifiedIdMapper) .keepingStrongReference(), - closeCallHandler = OnCloseCall(callRepository, scope, qualifiedIdMapper, networkStateObserver) - .keepingStrongReference(), + closeCallHandler = OnCloseCall( + callRepository = callRepository, + networkStateObserver = networkStateObserver, + scope = scope, + qualifiedIdMapper = qualifiedIdMapper + ).keepingStrongReference(), metricsHandler = metricsHandler, callConfigRequestHandler = OnConfigRequest(calling, callRepository, scope) .keepingStrongReference(), @@ -248,8 +258,6 @@ class CallManagerImpl internal constructor( ) if (callingValue.type != REMOTE_MUTE_TYPE || shouldRemoteMute) { - val currTime = System.currentTimeMillis() - val targetConversationId = if (message.isSelfMessage) { content.conversationId ?: message.conversationId } else { @@ -263,7 +271,7 @@ class CallManagerImpl internal constructor( inst = deferredHandle.await(), msg = msg, len = msg.size, - curr_time = Uint32_t(value = currTime / 1000), + curr_time = Uint32_t(value = serverTimeHandler.toServerTimestamp()), msg_time = Uint32_t(value = message.date.epochSeconds), convId = federatedIdMapper.parseToFederatedId(targetConversationId), userId = federatedIdMapper.parseToFederatedId(message.senderUserId), @@ -295,7 +303,7 @@ class CallManagerImpl internal constructor( isMuted = false, isCameraOn = isCameraOn, isCbrEnabled = isAudioCbr, - callerId = userId.await().toString() + callerId = userId.await() ) withCalling { @@ -455,14 +463,19 @@ class CallManagerImpl internal constructor( callClients: CallClientList ) { withCalling { - // Needed to support calls between federated and non federated environments + // Mapping Needed to support calls between federated and non federated environments (domain separation) val clients = callClients.clients.map { callClient -> CallClient( - federatedIdMapper.parseToFederatedId(callClient.userId), - callClient.clientId + userId = federatedIdMapper.parseToFederatedId(callClient.userId), + clientId = callClient.clientId, + isMemberOfSubconversation = callClient.isMemberOfSubconversation, + quality = callClient.quality ) } val clientsJson = CallClientList(clients).toJsonString() + callingLogger.d( + "$TAG - wcall_request_video_streams() called -> Requesting video streams for conversation = ${conversationId.toLogString()}" + ) val conversationIdString = federatedIdMapper.parseToFederatedId(conversationId) calling.wcall_request_video_streams( inst = it, @@ -532,6 +545,9 @@ class CallManagerImpl internal constructor( callRepository = callRepository, qualifiedIdMapper = qualifiedIdMapper, participantMapper = ParticipantMapperImpl(videoStateChecker, callMapper, qualifiedIdMapper), + userConfigRepository = userConfigRepository, + callHelper = CallHelperImpl(), + endCall = { endCall(it) }, callingScope = scope ).keepingStrongReference() diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index f88f5494be8..277eadc1662 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -26,6 +26,7 @@ import com.wire.kalium.calling.ENVIRONMENT_DEFAULT import com.wire.kalium.calling.callbacks.LogHandler import com.wire.kalium.logic.cache.SelfConversationIdProvider import com.wire.kalium.logic.callingLogger +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker import com.wire.kalium.logic.data.call.mapper.CallMapper @@ -41,6 +42,7 @@ import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvide import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.util.CurrentPlatform +import com.wire.kalium.logic.util.DummyCallManager import com.wire.kalium.logic.util.PlatformContext import com.wire.kalium.logic.util.PlatformType import com.wire.kalium.network.NetworkStateObserver @@ -83,6 +85,7 @@ actual class GlobalCallManager( currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, @@ -93,26 +96,31 @@ actual class GlobalCallManager( networkStateObserver: NetworkStateObserver, kaliumConfigs: KaliumConfigs ): CallManager { - return callManagerHolder.computeIfAbsent(userId) { - CallManagerImpl( - calling = calling, - callRepository = callRepository, - userRepository = userRepository, - currentClientIdProvider = currentClientIdProvider, - selfConversationIdProvider = selfConversationIdProvider, - callMapper = callMapper, - messageSender = messageSender, - conversationRepository = conversationRepository, - federatedIdMapper = federatedIdMapper, - qualifiedIdMapper = qualifiedIdMapper, - videoStateChecker = videoStateChecker, - conversationClientsInCallUpdater = conversationClientsInCallUpdater, - getCallConversationType = getCallConversationType, - networkStateObserver = networkStateObserver, - mediaManagerService = mediaManager, - flowManagerService = flowManager, - kaliumConfigs = kaliumConfigs - ) + if (kaliumConfigs.enableCalling) { + return callManagerHolder.computeIfAbsent(userId) { + CallManagerImpl( + calling = calling, + callRepository = callRepository, + userRepository = userRepository, + currentClientIdProvider = currentClientIdProvider, + selfConversationIdProvider = selfConversationIdProvider, + callMapper = callMapper, + messageSender = messageSender, + conversationRepository = conversationRepository, + federatedIdMapper = federatedIdMapper, + qualifiedIdMapper = qualifiedIdMapper, + videoStateChecker = videoStateChecker, + conversationClientsInCallUpdater = conversationClientsInCallUpdater, + getCallConversationType = getCallConversationType, + networkStateObserver = networkStateObserver, + mediaManagerService = mediaManager, + flowManagerService = flowManager, + userConfigRepository = userConfigRepository, + kaliumConfigs = kaliumConfigs + ) + } + } else { + return DummyCallManager() } } diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt index cee73c451e4..26d8c0b8194 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCall.kt @@ -27,10 +27,10 @@ import com.wire.kalium.calling.types.Uint32_t import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper -import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver import kotlinx.coroutines.CoroutineScope @@ -63,8 +63,13 @@ class OnCloseCall( scope.launch { - val isConnectedToInternet = networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet - if (shouldPersistMissedCall(conversationIdWithDomain, callStatus) && isConnectedToInternet) { + val isConnectedToInternet = + networkStateObserver.observeNetworkState().value == NetworkState.ConnectedWithInternet + if (shouldPersistMissedCall( + conversationIdWithDomain, + callStatus + ) && isConnectedToInternet + ) { callRepository.persistMissedCall(conversationIdWithDomain) } @@ -76,12 +81,14 @@ class OnCloseCall( if (callRepository.getCallMetadataProfile()[conversationIdWithDomain]?.protocol is Conversation.ProtocolInfo.MLS) { callRepository.leaveMlsConference(conversationIdWithDomain) } - callingLogger.i("[OnCloseCall] -> ConversationId: ${conversationId.obfuscateId()} | callStatus: $callStatus") } } - private fun shouldPersistMissedCall(conversationId: ConversationId, callStatus: CallStatus): Boolean { + private fun shouldPersistMissedCall( + conversationId: ConversationId, + callStatus: CallStatus + ): Boolean { if (callStatus == CallStatus.MISSED) return true return callRepository.getCallMetadataProfile().data[conversationId]?.let { diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt index 4c23e8fe868..25b68968352 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnIncomingCall.kt @@ -62,7 +62,7 @@ class OnIncomingCall( callRepository.createCall( conversationId = qualifiedConversationId, status = status, - callerId = qualifiedIdMapper.fromStringToQualifiedID(userId).toString(), + callerId = qualifiedIdMapper.fromStringToQualifiedID(userId), isMuted = isMuted, isCameraOn = false, type = mappedConversationType, diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt index 1ffa9c6c950..a9836ef78dc 100644 --- a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChanged.kt @@ -23,11 +23,17 @@ import com.wire.kalium.calling.callbacks.ParticipantChangedHandler import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.callingLogger import com.wire.kalium.logic.data.call.CallParticipants +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallHelper import com.wire.kalium.logic.data.call.ParticipantMinimized import com.wire.kalium.logic.data.call.mapper.ParticipantMapper +import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedIdMapper +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.kaliumLogger import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.json.Json @@ -36,21 +42,45 @@ class OnParticipantListChanged internal constructor( private val callRepository: CallRepository, private val qualifiedIdMapper: QualifiedIdMapper, private val participantMapper: ParticipantMapper, - private val callingScope: CoroutineScope + private val userConfigRepository: UserConfigRepository, + private val callHelper: CallHelper, + private val endCall: suspend (conversationId: ConversationId) -> Unit, + private val callingScope: CoroutineScope, + private val jsonDecoder: Json = Json ) : ParticipantChangedHandler { override fun onParticipantChanged(remoteConversationId: String, data: String, arg: Pointer?) { - val participantsChange = Json.decodeFromString(data) + val participantsChange = jsonDecoder.decodeFromString(data) callingScope.launch { val participants = mutableListOf() - val conversationIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId) + val conversationIdWithDomain = + qualifiedIdMapper.fromStringToQualifiedID(remoteConversationId) participantsChange.members.map { member -> participants.add(participantMapper.fromCallMemberToParticipantMinimized(member)) } + if (userConfigRepository.shouldUseSFTForOneOnOneCalls().getOrElse(false)) { + val callProtocol = callRepository.currentCallProtocol(conversationIdWithDomain) + + val currentCall = callRepository.establishedCallsFlow().first().firstOrNull() + currentCall?.let { + val shouldEndSFTOneOnOneCall = callHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationIdWithDomain, + callProtocol = callProtocol, + conversationType = it.conversationType, + newCallParticipants = participants, + previousCallParticipants = it.participants + ) + if (shouldEndSFTOneOnOneCall) { + kaliumLogger.i("[onParticipantChanged] - Ending SFT one on one call due to participant leaving") + endCall(conversationIdWithDomain) + } + } + } + callRepository.updateCallParticipants( conversationId = conversationIdWithDomain, participants = participants diff --git a/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt new file mode 100644 index 00000000000..017ceeb5a4d --- /dev/null +++ b/logic/src/commonJvmAndroid/kotlin/com/wire/kalium/logic/util/DummyCallManager.kt @@ -0,0 +1,69 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.util + +import com.wire.kalium.calling.ConversationTypeCalling +import com.wire.kalium.logic.data.call.CallClientList +import com.wire.kalium.logic.data.call.CallType +import com.wire.kalium.logic.data.call.EpochInfo +import com.wire.kalium.logic.data.call.Participant +import com.wire.kalium.logic.data.call.TestVideoType +import com.wire.kalium.logic.data.call.VideoState +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.feature.call.CallManager + +@Suppress("EmptyFunctionBlock", "TooManyFunctions") +class DummyCallManager : CallManager { + override suspend fun onCallingMessageReceived(message: Message.Signaling, content: MessageContent.Calling) {} + + override suspend fun startCall( + conversationId: ConversationId, + callType: CallType, + conversationTypeCalling: ConversationTypeCalling, + isAudioCbr: Boolean + ) { + } + + override suspend fun answerCall(conversationId: ConversationId, isAudioCbr: Boolean) {} + + override suspend fun endCall(conversationId: ConversationId) {} + + override suspend fun rejectCall(conversationId: ConversationId) {} + + override suspend fun muteCall(shouldMute: Boolean) {} + + override suspend fun setVideoSendState(conversationId: ConversationId, videoState: VideoState) {} + + override suspend fun requestVideoStreams(conversationId: ConversationId, callClients: CallClientList) {} + + override suspend fun updateEpochInfo(conversationId: ConversationId, epochInfo: EpochInfo) {} + + override suspend fun updateConversationClients(conversationId: ConversationId, clients: String) {} + + override suspend fun reportProcessNotifications(isStarted: Boolean) {} + + override suspend fun setTestVideoType(testVideoType: TestVideoType) {} + + override suspend fun setTestPreviewActive(shouldEnable: Boolean) {} + + override suspend fun setTestRemoteVideoStates(conversationId: ConversationId, participants: List) {} + + override suspend fun cancelJobs() {} +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt index 6ce16825226..24e28261290 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/CoreFailure.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic import com.wire.kalium.cryptography.exceptions.ProteusException import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.network.exceptions.APINotSupported import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.exceptions.isFederationDenied @@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach sealed interface CoreFailure { @@ -208,6 +210,7 @@ interface MLSFailure : CoreFailure { data object ConversationDoesNotSupportMLS : MLSFailure data object StaleProposal : MLSFailure data object StaleCommit : MLSFailure + data object InternalErrors : MLSFailure data class Generic(internal val exception: Exception) : MLSFailure { val rootCause: Throwable get() = exception @@ -336,7 +339,7 @@ internal inline fun wrapProteusRequest(proteusRequest: () -> T): Eithe |"cause": ${e.cause} }" """.trimMargin() ) kaliumLogger.e(e.stackTraceToString()) - Either.Left(ProteusFailure(ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, e.cause))) + Either.Left(ProteusFailure(ProteusException(e.message, ProteusException.Code.UNKNOWN_ERROR, null, e.cause))) } } @@ -367,12 +370,13 @@ internal inline fun wrapE2EIRequest(e2eiRequest: () -> T): Either wrapStorageRequest(storageRequest: () -> T?): Either { - return try { + val result = try { storageRequest()?.let { data -> Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound) } catch (e: Exception) { - kaliumLogger.e(e.stackTraceToString()) Either.Left(StorageFailure.Generic(e)) } + result.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) } + return result } /** @@ -403,8 +407,9 @@ internal fun Flow.wrapStorageRequest(): Flow Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound) }.catch { e -> - kaliumLogger.e(e.stackTraceToString()) emit(Either.Left(StorageFailure.Generic(e))) + }.onEach { + it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) } } internal inline fun wrapFlowStorageRequest(storageRequest: () -> Flow): Flow> { @@ -412,12 +417,12 @@ internal inline fun wrapFlowStorageRequest(storageRequest: () -> Flow< storageRequest().map { it?.let { data -> Either.Right(data) } ?: Either.Left(StorageFailure.DataNotFound) }.catch { e -> - kaliumLogger.e(e.stackTraceToString()) emit(Either.Left(StorageFailure.Generic(e))) } } catch (e: Exception) { - kaliumLogger.e(e.stackTraceToString()) flowOf(Either.Left(StorageFailure.Generic(e))) + }.onEach { + it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) } } } @@ -426,12 +431,12 @@ internal inline fun wrapNullableFlowStorageRequest(storageRequest: () storageRequest().map { Either.Right(it) as Either }.catch { e -> - kaliumLogger.e(e.stackTraceToString()) - emit(Either.Left(StorageFailure.Generic(e))) + emit(Either.Left(StorageFailure.Generic(e))) } } catch (e: Exception) { - kaliumLogger.e(e.stackTraceToString()) - flowOf(Either.Left(StorageFailure.Generic(e))) + flowOf(Either.Left(StorageFailure.Generic(e))) + }.onEach { + it.onFailure { storageFailure -> kaliumLogger.e(storageFailure.toString()) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt index 51b0d969358..61f6fa6d038 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/UserConfigRepository.kt @@ -93,6 +93,7 @@ interface UserConfigRepository { suspend fun getSupportedProtocols(): Either> fun setConferenceCallingEnabled(enabled: Boolean): Either fun isConferenceCallingEnabled(): Either + fun observeConferenceCallingEnabled(): Flow> fun setUseSFTForOneOnOneCalls(shouldUse: Boolean): Either fun shouldUseSFTForOneOnOneCalls(): Either fun setSecondFactorPasswordChallengeStatus(isRequired: Boolean): Either @@ -136,14 +137,16 @@ interface UserConfigRepository { suspend fun setShouldNotifyForRevokedCertificate(shouldNotify: Boolean) suspend fun observeShouldNotifyForRevokedCertificate(): Flow> suspend fun clearE2EISettings() - fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) - fun getShouldFetchE2EITrustAnchor(): Boolean suspend fun setCurrentTrackingIdentifier(newIdentifier: String) suspend fun getCurrentTrackingIdentifier(): String? suspend fun observeCurrentTrackingIdentifier(): Flow> suspend fun setPreviousTrackingIdentifier(identifier: String) suspend fun getPreviousTrackingIdentifier(): String? suspend fun deletePreviousTrackingIdentifier() + suspend fun updateNextTimeForCallFeedback(valueMs: Long) + suspend fun getNextTimeForCallFeedback(): Either + suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) + suspend fun getShouldFetchE2EITrustAnchor(): Boolean } @Suppress("TooManyFunctions") @@ -300,6 +303,9 @@ internal class UserConfigDataSource internal constructor( userConfigStorage.isConferenceCallingEnabled() } + override fun observeConferenceCallingEnabled(): Flow> = + userConfigStorage.isConferenceCallingEnabledFlow().wrapStorageRequest() + override fun setUseSFTForOneOnOneCalls(shouldUse: Boolean): Either = wrapStorageRequest { userConfigStorage.persistUseSftForOneOnOneCalls(shouldUse) } @@ -506,12 +512,10 @@ internal class UserConfigDataSource internal constructor( override suspend fun observeShouldNotifyForRevokedCertificate(): Flow> = userConfigDAO.observeShouldNotifyForRevokedCertificate().wrapStorageRequest() - override fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) { - userConfigStorage.setShouldFetchE2EITrustAnchors(shouldFetch = shouldFetch) + override suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) { + userConfigDAO.setShouldFetchE2EITrustAnchors(shouldFetch = shouldFetch) } - override fun getShouldFetchE2EITrustAnchor(): Boolean = userConfigStorage.getShouldFetchE2EITrustAnchorHasRun() - override suspend fun setCurrentTrackingIdentifier(newIdentifier: String) { wrapStorageRequest { userConfigDAO.setTrackingIdentifier(identifier = newIdentifier) @@ -538,4 +542,12 @@ internal class UserConfigDataSource internal constructor( userConfigDAO.deletePreviousTrackingIdentifier() } } + + override suspend fun updateNextTimeForCallFeedback(valueMs: Long) { + userConfigDAO.setNextTimeForCallFeedback(valueMs) + } + + override suspend fun getNextTimeForCallFeedback() = wrapStorageRequest { userConfigDAO.getNextTimeForCallFeedback() } + + override suspend fun getShouldFetchE2EITrustAnchor(): Boolean = userConfigDAO.getShouldFetchE2EITrustAnchorHasRun() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt index 149493abf25..3bb8a0273a8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfig.kt @@ -134,6 +134,19 @@ data class ServerConfig( isOnPremises = false, apiProxy = null ) + + val DUMMY = Links( + api = """https://dummy-nginz-https.zinfra.io""", + accounts = """https://wire-account-dummy.zinfra.io""", + webSocket = """https://dummy-nginz-ssl.zinfra.io""", + teams = """https://wire-teams-dummy.zinfra.io""", + blackList = """https://clientblacklist.wire.com/dummy""", + website = """https://dummy.wire.com""", + title = "dummy", + isOnPremises = false, + apiProxy = null + ) + val DEFAULT = PRODUCTION private const val FORGOT_PASSWORD_PATH = "forgot" diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt index 54f10b8df38..faef6688735 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/configuration/server/ServerConfigRepository.kt @@ -31,15 +31,18 @@ import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapStorageRequest -import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi.Companion.MIN_API_VERSION import com.wire.kalium.network.api.base.unbound.versioning.VersionApi +import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO import com.wire.kalium.persistence.daokaliumdb.ServerConfigurationDAO import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import io.ktor.http.Url import kotlinx.coroutines.withContext -internal interface ServerConfigRepository { +interface ServerConfigRepository { + val minimumApiVersionForPersonalToTeamAccountMigration: Int + suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either suspend fun storeConfig(links: ServerConfig.Links, metadata: ServerConfig.MetaData): Either @@ -62,6 +65,7 @@ internal interface ServerConfigRepository { * Return the server links and metadata for the given userId */ suspend fun configForUser(userId: UserId): Either + suspend fun commonApiVersion(domain: String): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -72,6 +76,8 @@ internal class ServerConfigDataSource( private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : ServerConfigRepository { + override val minimumApiVersionForPersonalToTeamAccountMigration = MIN_API_VERSION + override suspend fun getOrFetchMetadata(serverLinks: ServerConfig.Links): Either = wrapStorageRequest { dao.configByLinks(serverConfigMapper.toEntity(serverLinks)) }.fold({ fetchApiVersionAndStore(serverLinks) @@ -127,13 +133,17 @@ internal class ServerConfigDataSource( } override suspend fun updateConfigApiVersion(serverConfig: ServerConfig): Either = - fetchMetadata(serverConfig.links) - .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } } + fetchMetadata(serverConfig.links) + .flatMap { wrapStorageRequest { dao.updateApiVersion(serverConfig.id, it.commonApiVersion.version) } } override suspend fun configForUser(userId: UserId): Either = wrapStorageRequest { dao.configForUser(userId.toDao()) } .map { serverConfigMapper.fromEntity(it) } + override suspend fun commonApiVersion(domain: String): Either = wrapStorageRequest { + dao.getCommonApiVersion(domain) + } + private suspend fun fetchMetadata(serverLinks: ServerConfig.Links): Either = wrapApiRequest { versionApi.fetchApiVersion(Url(serverLinks.api)) } .flatMap { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt index db04024050d..a8eb1976618 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/asset/AssetRepository.kt @@ -156,29 +156,33 @@ internal class AssetDataSource( otrKey: AES256Key, extension: String? ): Either> { - - val tempEncryptedDataPath = kaliumFileSystem.tempFilePath("${assetDataPath.name}.aes") - val assetDataSource = kaliumFileSystem.source(assetDataPath) - val assetDataSink = kaliumFileSystem.sink(tempEncryptedDataPath) - - // Encrypt the data on the provided temp path - val encryptedDataSize = encryptFileWithAES256(assetDataSource, otrKey, assetDataSink) - val encryptedDataSource = kaliumFileSystem.source(tempEncryptedDataPath) - - // Calculate the SHA of the encrypted data - val sha256 = calcFileSHA256(encryptedDataSource) - assetDataSink.close() - encryptedDataSource.close() - assetDataSource.close() - - val encryptionSucceeded = (encryptedDataSize > 0L && sha256 != null) - - return if (encryptionSucceeded) { - val uploadAssetData = UploadAssetData(tempEncryptedDataPath, encryptedDataSize, mimeType, false, RetentionType.PERSISTENT) - uploadAndPersistAsset(uploadAssetData, assetDataPath, extension).map { it to SHA256Key(sha256!!) } - } else { - kaliumLogger.e("Something went wrong when encrypting the Asset Message") - Either.Left(EncryptionFailure.GenericEncryptionError) + try { + val tempEncryptedDataPath = kaliumFileSystem.tempFilePath("${assetDataPath.name}.aes") + val assetDataSource = kaliumFileSystem.source(assetDataPath) + val assetDataSink = kaliumFileSystem.sink(tempEncryptedDataPath) + + // Encrypt the data on the provided temp path + val encryptedDataSize = encryptFileWithAES256(assetDataSource, otrKey, assetDataSink) + val encryptedDataSource = kaliumFileSystem.source(tempEncryptedDataPath) + + // Calculate the SHA of the encrypted data + val sha256 = calcFileSHA256(encryptedDataSource) + assetDataSink.close() + encryptedDataSource.close() + assetDataSource.close() + + val encryptionSucceeded = (encryptedDataSize > 0L && sha256 != null) + + return if (encryptionSucceeded) { + val uploadAssetData = UploadAssetData(tempEncryptedDataPath, encryptedDataSize, mimeType, false, RetentionType.PERSISTENT) + uploadAndPersistAsset(uploadAssetData, assetDataPath, extension).map { it to SHA256Key(sha256!!) } + } else { + kaliumLogger.e("Something went wrong when encrypting the Asset Message") + Either.Left(EncryptionFailure.GenericEncryptionError) + } + } catch (e: IOException) { + kaliumLogger.e("Something went wrong when uploading the Asset Message. $e") + return Either.Left(CoreFailure.Unknown(e)) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt new file mode 100644 index 00000000000..710633ab3e5 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallHelper.kt @@ -0,0 +1,74 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.call + +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId + +/** + * Helper class to handle call related operations. + */ +interface CallHelper { + + /** + * Check if the OneOnOne call that uses SFT should be ended. + * For Proteus, the call should be ended if the call has one participant after having 2 in the call. + * For MLS, the call should be ended if the call has two participants and the second participant has lost audio. + * + * @param conversationId the conversation id. + * @param callProtocol the call protocol. + * @param conversationType the conversation type. + * @param newCallParticipants the new call participants. + * @param previousCallParticipants the previous call participants. + * @return true if the call should be ended, false otherwise. + */ + fun shouldEndSFTOneOnOneCall( + conversationId: ConversationId, + callProtocol: Conversation.ProtocolInfo?, + conversationType: Conversation.Type, + newCallParticipants: List, + previousCallParticipants: List + ): Boolean +} + +class CallHelperImpl : CallHelper { + + override fun shouldEndSFTOneOnOneCall( + conversationId: ConversationId, + callProtocol: Conversation.ProtocolInfo?, + conversationType: Conversation.Type, + newCallParticipants: List, + previousCallParticipants: List + ): Boolean { + return if (callProtocol is Conversation.ProtocolInfo.Proteus) { + conversationType == Conversation.Type.ONE_ON_ONE && + newCallParticipants.size == ONE_PARTICIPANTS && + previousCallParticipants.size == TWO_PARTICIPANTS + } else { + conversationType == Conversation.Type.ONE_ON_ONE && + newCallParticipants.size == TWO_PARTICIPANTS && + previousCallParticipants.size == TWO_PARTICIPANTS && + previousCallParticipants[1].hasEstablishedAudio && !newCallParticipants[1].hasEstablishedAudio + } + } + + companion object { + const val TWO_PARTICIPANTS = 2 + const val ONE_PARTICIPANTS = 1 + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt index 722a6b9f07b..f45ce704881 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/CallRepository.kt @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.FederatedIdMapper import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.QualifiedClientID +import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SubconversationId import com.wire.kalium.logic.data.id.toCrypto @@ -81,6 +82,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.datetime.Clock @@ -108,7 +110,7 @@ interface CallRepository { conversationId: ConversationId, type: ConversationTypeForCall, status: CallStatus, - callerId: String, + callerId: UserId, isMuted: Boolean, isCameraOn: Boolean, isCbrEnabled: Boolean @@ -122,6 +124,7 @@ interface CallRepository { fun updateParticipantsActiveSpeaker(conversationId: ConversationId, activeSpeakers: Map>) suspend fun getLastClosedCallCreatedByConversationId(conversationId: ConversationId): Flow suspend fun updateOpenCallsToClosedStatus() + suspend fun leavePreviouslyJoinedMlsConferences() suspend fun persistMissedCall(conversationId: ConversationId) suspend fun joinMlsConference( conversationId: ConversationId, @@ -131,6 +134,8 @@ interface CallRepository { suspend fun leaveMlsConference(conversationId: ConversationId) suspend fun observeEpochInfo(conversationId: ConversationId): Either> suspend fun advanceEpoch(conversationId: ConversationId) + fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? + suspend fun observeCurrentCall(conversationId: ConversationId): Flow } @Suppress("LongParameterList", "TooManyFunctions") @@ -160,6 +165,26 @@ internal class CallDataSource( private val callJobs = ConcurrentMutableMap() private val staleParticipantJobs = ConcurrentMutableMap() + override suspend fun observeCurrentCall(conversationId: ConversationId): Flow = _callMetadataProfile.map { + it[conversationId]?.let { currentCall -> + Call( + conversationId = conversationId, + status = currentCall.callStatus, + isMuted = currentCall.isMuted, + isCameraOn = currentCall.isCameraOn, + isCbrEnabled = currentCall.isCbrEnabled, + callerId = currentCall.callerId, + conversationName = currentCall.conversationName, + conversationType = currentCall.conversationType, + callerName = currentCall.callerName, + callerTeamName = currentCall.callerTeamName, + establishedTime = currentCall.establishedTime, + participants = currentCall.getFullParticipants(), + maxParticipants = currentCall.maxParticipants + ) + } + } + override suspend fun getCallConfigResponse(limit: Int?): Either = wrapApiRequest { callApi.getCallConfig(limit = limit) } @@ -189,7 +214,7 @@ internal class CallDataSource( conversationId: ConversationId, type: ConversationTypeForCall, status: CallStatus, - callerId: String, + callerId: UserId, isMuted: Boolean, isCameraOn: Boolean, isCbrEnabled: Boolean @@ -197,10 +222,7 @@ internal class CallDataSource( val conversation: ConversationDetails = conversationRepository.observeConversationDetailsById(conversationId).onlyRight().first() - // in OnIncomingCall we get callerId without a domain, - // to cover that case and have a valid UserId we have that workaround - val callerIdWithDomain = qualifiedIdMapper.fromStringToQualifiedID(callerId) - val caller = userRepository.getKnownUser(callerIdWithDomain).first() + val caller = userRepository.getKnownUser(callerId).first() val team = caller?.teamId?.let { teamId -> teamRepository.getTeam(teamId).first() } val callEntity = callMapper.toCallEntity( @@ -209,10 +231,11 @@ internal class CallDataSource( type = type, status = status, conversationType = conversation.conversation.type, - callerId = callerIdWithDomain + callerId = callerId ) val metadata = CallMetadata( + callerId = callerId, conversationName = conversation.conversation.name, conversationType = conversation.conversation.type, callerName = caller?.name, @@ -419,6 +442,8 @@ internal class CallDataSource( val currentParticipantIds = call.participants.map { it.userId }.toSet() val newParticipantIds = participants.map { it.userId }.toSet() + val sharingScreenParticipantIds = participants.filter { it.isSharingScreen } + .map { participant -> participant.id } val updatedUsers = call.users.toMutableList() @@ -434,7 +459,11 @@ internal class CallDataSource( this[conversationId] = call.copy( participants = participants, maxParticipants = max(call.maxParticipants, participants.size + 1), - users = updatedUsers + users = updatedUsers, + screenShareMetadata = updateScreenSharingMetadata( + metadata = call.screenShareMetadata, + usersCurrentlySharingScreen = sharingScreenParticipantIds + ) ) } @@ -444,7 +473,9 @@ internal class CallDataSource( } } - if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS) { + if (_callMetadataProfile.value[conversationId]?.protocol is Conversation.ProtocolInfo.MLS && + _callMetadataProfile.value[conversationId]?.conversationType == Conversation.Type.GROUP + ) { participants.forEach { participant -> if (participant.hasEstablishedAudio) { clearStaleParticipantTimeout(participant) @@ -455,6 +486,43 @@ internal class CallDataSource( } } + /** + * Manages call sharing metadata for analytical purposes by tracking the following: + * - **Active Screen Shares**: Maintains a record of currently active screen shares with their start times (local to the device). + * - **Completed Screen Share Duration**: Accumulates the total duration of screen shares that have already ended. + * - **Unique Sharing Users**: Keeps a unique list of all users who have shared their screen during the call. + * + * To update the metadata, the following steps are performed: + * 1. **Calculate Ended Screen Share Time**: Determine the total time for users who stopped sharing since the last update. + * 2. **Update Active Shares**: Filter out inactive shares and add any new ones, associating them with the current start time. + * 3. **Track Unique Users**: Append ids to current set in order to keep track of unique users. + */ + private fun updateScreenSharingMetadata( + metadata: CallScreenSharingMetadata, + usersCurrentlySharingScreen: List + ): CallScreenSharingMetadata { + val now = DateTimeUtil.currentInstant() + + val alreadyEndedScreenSharesTimeInMillis = metadata.activeScreenShares + .filterKeys { id -> id !in usersCurrentlySharingScreen } + .values + .sumOf { startTime -> DateTimeUtil.calculateMillisDifference(startTime, now) } + + val updatedShares = metadata.activeScreenShares + .filterKeys { id -> id in usersCurrentlySharingScreen } + .plus( + usersCurrentlySharingScreen + .filterNot { id -> metadata.activeScreenShares.containsKey(id) } + .associateWith { now } + ) + + return metadata.copy( + activeScreenShares = updatedShares, + completedScreenShareDurationInMillis = metadata.completedScreenShareDurationInMillis + alreadyEndedScreenSharesTimeInMillis, + uniqueSharingUsers = metadata.uniqueSharingUsers.plus(usersCurrentlySharingScreen.map { id -> id.toString() }) + ) + } + private fun clearStaleParticipantTimeout(participant: ParticipantMinimized) { callingLogger.i("Clear stale participant timer") val qualifiedClient = QualifiedClientID(ClientId(participant.clientId), participant.id) @@ -552,7 +620,7 @@ internal class CallDataSource( } } - private suspend fun leavePreviouslyJoinedMlsConferences() { + override suspend fun leavePreviouslyJoinedMlsConferences() { callingLogger.i("Leaving previously joined MLS conferences") callDAO.observeEstablishedCalls() @@ -678,6 +746,9 @@ internal class CallDataSource( } ?: callingLogger.w("[CallRepository] -> Requested new epoch but there's no conference subconversation") } + override fun currentCallProtocol(conversationId: ConversationId): Conversation.ProtocolInfo? = + _callMetadataProfile.value.data[conversationId]?.protocol + companion object { val STALE_PARTICIPANT_TIMEOUT = 190.toDuration(kotlin.time.DurationUnit.SECONDS) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt index 19f18747811..caba61b1818 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/call/mapper/CallMapper.kt @@ -180,7 +180,7 @@ class CallMapperImpl( isMuted = metadata?.isMuted ?: true, isCameraOn = metadata?.isCameraOn ?: false, isCbrEnabled = metadata?.isCbrEnabled ?: false, - callerId = callEntity.callerId, + callerId = metadata?.callerId ?: qualifiedIdMapper.fromStringToQualifiedID(callEntity.callerId), conversationName = metadata?.conversationName, conversationType = toConversationType(conversationType = callEntity.conversationType), callerName = metadata?.callerName, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt index 8cca1a5da58..eb84c333743 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ClientRepository.kt @@ -76,15 +76,29 @@ interface ClientRepository { suspend fun isClientRegistrationBlockedByE2EI(): Either suspend fun deleteClient(param: DeleteClientParam): Either suspend fun selfListOfClients(): Either> - suspend fun observeClientsByUserIdAndClientId(userId: UserId, clientId: ClientId): Flow> + suspend fun observeClientsByUserIdAndClientId( + userId: UserId, + clientId: ClientId + ): Flow> + suspend fun storeUserClientListAndRemoveRedundantClients(clients: List): Either - suspend fun storeUserClientIdList(userId: UserId, clients: List): Either + suspend fun storeUserClientIdList( + userId: UserId, + clients: List + ): Either + suspend fun storeMapOfUserToClientId(userToClientMap: Map>): Either suspend fun removeClientsAndReturnUsersWithNoClients( redundantClientsOfUsers: Map> ): Either> - suspend fun registerToken(body: PushTokenBody): Either + suspend fun registerToken( + senderId: String, + client: String, + token: String, + transport: String + ): Either + suspend fun deregisterToken(token: String): Either suspend fun getClientsByUserId(userId: UserId): Either> suspend fun observeClientsByUserId(userId: UserId): Flow>> @@ -205,7 +219,10 @@ class ClientDataSource( } } - override suspend fun observeClientsByUserIdAndClientId(userId: UserId, clientId: ClientId): Flow> = + override suspend fun observeClientsByUserIdAndClientId( + userId: UserId, + clientId: ClientId + ): Flow> = clientDAO.observeClient(userId.toDao(), clientId.value) .map { it?.let { clientMapper.fromClientEntity(it) } } .wrapStorageRequest() @@ -250,7 +267,11 @@ class ClientDataSource( redundantClientsOfUsers.mapKeys { it.key.toDao() } .mapValues { it.value.map { clientId -> clientId.value } } .let { redundantClientsOfUsersDao -> - wrapStorageRequest { clientDAO.removeClientsAndReturnUsersWithNoClients(redundantClientsOfUsersDao) } + wrapStorageRequest { + clientDAO.removeClientsAndReturnUsersWithNoClients( + redundantClientsOfUsersDao + ) + } .map { it.map { userId -> userId.toModel() } } @@ -258,10 +279,20 @@ class ClientDataSource( override suspend fun storeUserClientListAndRemoveRedundantClients( clients: List - ): Either = wrapStorageRequest { clientDAO.insertClientsAndRemoveRedundant(clients) } - - override suspend fun registerToken(body: PushTokenBody): Either = clientRemoteRepository.registerToken(body) - override suspend fun deregisterToken(token: String): Either = clientRemoteRepository.deregisterToken(token) + ): Either = + wrapStorageRequest { clientDAO.insertClientsAndRemoveRedundant(clients) } + + override suspend fun registerToken( + senderId: String, + client: String, + token: String, + transport: String + ): Either = clientRemoteRepository.registerToken( + PushTokenBody(senderId, client, token, transport) + ) + + override suspend fun deregisterToken(token: String): Either = + clientRemoteRepository.deregisterToken(token) override suspend fun getClientsByUserId(userId: UserId): Either> = wrapStorageRequest { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt index 18369dcda3d..97d2dc0afe8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/MLSClientProvider.kt @@ -141,9 +141,12 @@ class MLSClientProviderImpl( override suspend fun clearLocalFiles() { mlsClientMutex.withLock { - mlsClient?.close() - mlsClient = null - FileUtil.deleteDirectory(rootKeyStorePath) + coreCryptoCentralMutex.withLock { + mlsClient?.close() + mlsClient = null + coreCryptoCentral = null + FileUtil.deleteDirectory(rootKeyStorePath) + } } } @@ -182,7 +185,10 @@ class MLSClientProviderImpl( } } - private suspend fun mlsClient(userId: CryptoUserID, clientId: ClientId): Either { + private suspend fun mlsClient( + userId: CryptoUserID, + clientId: ClientId + ): Either { return getCoreCrypto(clientId).flatMap { cc -> getOrFetchMLSConfig().map { (supportedCipherSuite, defaultCipherSuite) -> cc.mlsClient( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt index 013deac601f..26f2f6ece6d 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusClientProvider.kt @@ -18,9 +18,11 @@ package com.wire.kalium.logic.data.client +import com.wire.kalium.cryptography.CoreCryptoCentral import com.wire.kalium.cryptography.ProteusClient import com.wire.kalium.cryptography.coreCryptoCentral import com.wire.kalium.cryptography.cryptoboxProteusClient +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure @@ -58,20 +60,15 @@ class ProteusClientProviderImpl( private val userId: UserId, private val passphraseStorage: PassphraseStorage, private val kaliumConfigs: KaliumConfigs, - private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl + private val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler ) : ProteusClientProvider { private var _proteusClient: ProteusClient? = null private val mutex = Mutex() override suspend fun clearLocalFiles() { - mutex.withLock { - withContext(dispatcher.io) { - _proteusClient?.close() - _proteusClient = null - FileUtil.deleteDirectory(rootProteusPath) - } - } + mutex.withLock { removeLocalFiles() } } override suspend fun getOrCreate(): ProteusClient { @@ -109,7 +106,6 @@ class ProteusClientProviderImpl( databaseKey = SecurityHelperImpl(passphraseStorage).proteusDBSecret(userId).value ) } catch (e: Exception) { - val logMap = mapOf( "userId" to userId.value.obfuscateId(), "exception" to e, @@ -119,7 +115,7 @@ class ProteusClientProviderImpl( kaliumLogger.logStructuredJson(KaliumLogLevel.ERROR, TAG, logMap) throw e } - central.proteusClient() + getCentralProteusClientOrError(central) } else { cryptoboxProteusClient( rootDir = rootProteusPath, @@ -129,6 +125,34 @@ class ProteusClientProviderImpl( } } + private suspend fun getCentralProteusClientOrError(central: CoreCryptoCentral): ProteusClient { + return try { + central.proteusClient() + } catch (exception: ProteusStorageMigrationException) { + proteusMigrationRecoveryHandler.clearClientData { removeLocalFiles() } + val logMap = mapOf( + "userId" to userId.value.obfuscateId(), + "exception" to exception, + "message" to exception.message, + "stackTrace" to exception.stackTraceToString() + ) + kaliumLogger.withTextTag(TAG).logStructuredJson(KaliumLogLevel.ERROR, "Proteus storage migration failed", logMap) + throw exception + } + } + + /** + * Actually deletes the proteus local files. + * Important! It is the caller responsibility to use the mutex, DON'T add a mutex here or it will be dead lock it. + */ + private suspend fun removeLocalFiles() { + withContext(dispatcher.io) { + _proteusClient?.close() + _proteusClient = null + FileUtil.deleteDirectory(rootProteusPath) + } + } + private companion object { const val TAG = "ProteusClientProvider" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt new file mode 100644 index 00000000000..9fe6880ecc4 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/client/ProteusMigrationRecoveryHandler.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.client + +import com.wire.kalium.logic.data.logout.LogoutReason + +/** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ +interface ProteusMigrationRecoveryHandler { + suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt index d8185faf11c..da27400302b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepository.kt @@ -154,6 +154,7 @@ internal class ConversationGroupRepositoryImpl( val conversationEntity = conversationMapper.fromApiModelToDaoModel( conversationResponse, mlsGroupState = ConversationEntity.GroupState.PENDING_CREATION, selfTeamId ) + val mlsPublicKeys = conversationMapper.fromApiModel(conversationResponse.publicKeys) val protocol = protocolInfoMapper.fromEntity(conversationEntity.protocolInfo) return wrapStorageRequest { @@ -166,7 +167,8 @@ internal class ConversationGroupRepositoryImpl( is Conversation.ProtocolInfo.MLSCapable -> mlsConversationRepository.establishMLSGroup( groupID = protocol.groupId, members = usersList + selfUserId, - allowSkippingUsersWithoutKeyPackages = true + publicKeys = mlsPublicKeys, + allowSkippingUsersWithoutKeyPackages = true, ).map { it.notAddedUsers } } }.flatMap { protocolSpecificAdditionFailures -> diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt index 59a63218fce..cc2d9995a75 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapper.kt @@ -27,7 +27,9 @@ import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.message.MessagePreview +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.data.message.UnreadEventType +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.data.user.AvailabilityStatusMapper import com.wire.kalium.logic.data.user.BotService import com.wire.kalium.logic.data.user.Connection @@ -41,14 +43,18 @@ import com.wire.kalium.network.api.authenticated.conversation.ConvTeamInfo import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse import com.wire.kalium.network.api.authenticated.conversation.CreateConversationRequest import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode +import com.wire.kalium.network.api.authenticated.serverpublickey.MLSPublicKeysDTO import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity.GroupState import com.wire.kalium.persistence.dao.conversation.ConversationEntity.Protocol import com.wire.kalium.persistence.dao.conversation.ConversationEntity.ProtocolInfo +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.conversation.ProposalTimerEntity +import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.util.requireField import com.wire.kalium.util.DateTimeUtil import com.wire.kalium.util.time.UNIX_FIRST_DATE @@ -59,13 +65,11 @@ import kotlin.time.toDuration interface ConversationMapper { fun fromApiModelToDaoModel(apiModel: ConversationResponse, mlsGroupState: GroupState?, selfUserTeamId: TeamId?): ConversationEntity + fun fromApiModel(mlsPublicKeysDTO: MLSPublicKeysDTO?): MLSPublicKeys? + fun fromDaoModel(daoModel: ConversationViewEntity): Conversation fun fromDaoModel(daoModel: ConversationEntity): Conversation - fun fromDaoModelToDetails( - daoModel: ConversationViewEntity, - lastMessage: MessagePreview?, - unreadEventCount: UnreadEventCount? - ): ConversationDetails - + fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails + fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer fun toDAOAccess(accessList: Set): List fun toDAOAccessRole(accessRoleList: Set): List @@ -84,6 +88,11 @@ interface ConversationMapper { fun legalHoldStatusFromEntity(legalHoldStatus: ConversationEntity.LegalHoldStatus): Conversation.LegalHoldStatus fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type + + fun fromModelToDAOAccess(accessList: Set): List + fun fromModelToDAOAccessRole(accessRoleList: Set): List + fun fromApiModelToAccessModel(accessList: Set): Set + fun fromApiModelToAccessRoleModel(accessRoleList: Set): Set } @Suppress("TooManyFunctions", "LongParameterList") @@ -96,7 +105,8 @@ internal class ConversationMapperImpl( private val domainUserTypeMapper: DomainUserTypeMapper, private val connectionStatusMapper: ConnectionStatusMapper, private val conversationRoleMapper: ConversationRoleMapper, - private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper() + private val messageMapper: MessageMapper, + private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper(), ) : ConversationMapper { override fun fromApiModelToDaoModel( @@ -117,7 +127,8 @@ internal class ConversationMapperImpl( lastNotificationDate = null, lastModifiedDate = apiModel.lastEventTime.toInstant(), access = apiModel.access.map { it.toDAO() }, - accessRole = apiModel.accessRole.map { it.toDAO() }, + accessRole = (apiModel.accessRole ?: ConversationAccessRoleDTO.DEFAULT_VALUE_WHEN_NULL) + .map { it.toDAO() }, receiptMode = receiptModeMapper.fromApiToDaoModel(apiModel.receiptMode), messageTimer = apiModel.messageTimer, userMessageTimer = null, // user picked self deletion timer is only persisted locally @@ -158,6 +169,41 @@ internal class ConversationMapperImpl( ) } + override fun fromApiModel(mlsPublicKeysDTO: MLSPublicKeysDTO?) = mlsPublicKeysDTO?.let { + MLSPublicKeys( + removal = mlsPublicKeysDTO.removal + ) + } + + override fun fromDaoModel(daoModel: ConversationViewEntity): Conversation = with(daoModel) { + val lastReadDateEntity = if (type == ConversationEntity.Type.CONNECTION_PENDING) Instant.UNIX_FIRST_DATE + else lastReadDate + + Conversation( + id = id.toModel(), + name = name, + type = type.fromDaoModelToType(), + teamId = teamId?.let { TeamId(it) }, + protocol = protocolInfoMapper.fromEntity(protocolInfo), + mutedStatus = conversationStatusMapper.fromMutedStatusDaoModel(mutedStatus), + removedBy = removedBy?.let { conversationStatusMapper.fromRemovedByToLogicModel(it) }, + lastNotificationDate = lastNotificationDate, + lastModifiedDate = lastModifiedDate, + lastReadDate = lastReadDateEntity, + access = accessList.map { it.toDAO() }, + accessRole = accessRoleList.map { it.toDAO() }, + creatorId = creatorId, + receiptMode = receiptModeMapper.fromEntityToModel(receiptMode), + messageTimer = messageTimer?.toDuration(DurationUnit.MILLISECONDS), + userMessageTimer = userMessageTimer?.toDuration(DurationUnit.MILLISECONDS), + archived = archived, + archivedDateTime = archivedDateTime, + mlsVerificationStatus = verificationStatusFromEntity(mlsVerificationStatus), + proteusVerificationStatus = verificationStatusFromEntity(proteusVerificationStatus), + legalHoldStatus = legalHoldStatusFromEntity(legalHoldStatus) + ) + } + override fun fromDaoModel(daoModel: ConversationEntity): Conversation = with(daoModel) { val lastReadDateEntity = if (type == ConversationEntity.Type.CONNECTION_PENDING) Instant.UNIX_FIRST_DATE else lastReadDate @@ -187,11 +233,7 @@ internal class ConversationMapperImpl( } @Suppress("ComplexMethod", "LongMethod") - override fun fromDaoModelToDetails( - daoModel: ConversationViewEntity, - lastMessage: MessagePreview?, - unreadEventCount: UnreadEventCount? - ): ConversationDetails = + override fun fromDaoModelToDetails(daoModel: ConversationViewEntity): ConversationDetails = with(daoModel) { when (type) { ConversationEntity.Type.SELF -> { @@ -204,7 +246,7 @@ internal class ConversationMapperImpl( otherUser = OtherUser( id = otherUserId.requireField("otherUserID in OneOnOne").toModel(), name = name, - accentId = 0, + accentId = accentId ?: 0, userType = domainUserTypeMapper.fromUserTypeEntity(userType), availabilityStatus = userAvailabilityStatusMapper.fromDaoAvailabilityStatusToModel(userAvailabilityStatus), deleted = userDeleted ?: false, @@ -221,8 +263,7 @@ internal class ConversationMapperImpl( activeOneOnOneConversationId = userActiveOneOnOneConversationId?.toModel() ), userType = domainUserTypeMapper.fromUserTypeEntity(userType), - unreadEventCount = unreadEventCount ?: mapOf(), - lastMessage = lastMessage + isFavorite = isFavorite ) } @@ -230,11 +271,9 @@ internal class ConversationMapperImpl( ConversationDetails.Group( conversation = fromConversationViewToEntity(daoModel), hasOngoingCall = callStatus != null, // todo: we can do better! - unreadEventCount = unreadEventCount ?: mapOf(), - lastMessage = lastMessage, isSelfUserMember = isMember, - isSelfUserCreator = isCreator == 1L, - selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) } + selfRole = selfRole?.let { conversationRoleMapper.fromDAO(it) }, + isFavorite = isFavorite ) } @@ -280,6 +319,27 @@ internal class ConversationMapperImpl( } } + override fun fromDaoModelToDetailsWithEvents(daoModel: ConversationDetailsWithEventsEntity): ConversationDetailsWithEvents = + ConversationDetailsWithEvents( + conversationDetails = fromDaoModelToDetails(daoModel.conversationViewEntity), + unreadEventCount = daoModel.unreadEvents.unreadEvents.mapKeys { + when (it.key) { + UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK + UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL + UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION + UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY + UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE + } + }, + lastMessage = when { + daoModel.conversationViewEntity.archived -> null // no last message in archived conversations + daoModel.messageDraft != null -> messageMapper.fromDraftToMessagePreview(daoModel.messageDraft!!) + daoModel.lastMessage != null -> messageMapper.fromEntityToMessagePreview(daoModel.lastMessage!!) + else -> null + }, + hasNewActivitiesToShow = daoModel.hasNewActivitiesToShow + ) + override fun fromDaoModel(daoModel: ProposalTimerEntity): ProposalTimer = ProposalTimer(idMapper.fromGroupIDEntity(daoModel.groupID), daoModel.firingDate) @@ -459,6 +519,18 @@ internal class ConversationMapperImpl( override fun fromConversationEntityType(type: ConversationEntity.Type): Conversation.Type { return type.fromDaoModelToType() } + + override fun fromModelToDAOAccess(accessList: Set): List = + accessList.map { it.toDAO() } + + override fun fromModelToDAOAccessRole(accessRoleList: Set): List = + accessRoleList.map { it.toDAO() } + + override fun fromApiModelToAccessModel(accessList: Set): Set = + accessList.map { it.toModel() }.toSet() + + override fun fromApiModelToAccessRoleModel(accessRoleList: Set): Set = + accessRoleList.map { it.toModel() }.toSet() } internal fun ConversationResponse.toConversationType(selfUserTeamId: TeamId?): ConversationEntity.Type { @@ -551,6 +623,22 @@ private fun Conversation.Access.toDAO(): ConversationEntity.Access = when (this) Conversation.Access.CODE -> ConversationEntity.Access.CODE } +private fun ConversationAccessDTO.toModel(): Conversation.Access = when (this) { + ConversationAccessDTO.PRIVATE -> Conversation.Access.PRIVATE + ConversationAccessDTO.CODE -> Conversation.Access.CODE + ConversationAccessDTO.INVITE -> Conversation.Access.INVITE + ConversationAccessDTO.SELF_INVITE -> Conversation.Access.SELF_INVITE + ConversationAccessDTO.LINK -> Conversation.Access.LINK +} + +private fun ConversationAccessRoleDTO.toModel(): Conversation.AccessRole = when (this) { + ConversationAccessRoleDTO.TEAM_MEMBER -> Conversation.AccessRole.TEAM_MEMBER + ConversationAccessRoleDTO.NON_TEAM_MEMBER -> Conversation.AccessRole.NON_TEAM_MEMBER + ConversationAccessRoleDTO.GUEST -> Conversation.AccessRole.GUEST + ConversationAccessRoleDTO.SERVICE -> Conversation.AccessRole.SERVICE + ConversationAccessRoleDTO.EXTERNAL -> Conversation.AccessRole.EXTERNAL +} + internal fun Conversation.Protocol.toApi(): ConvProtocol = when (this) { Conversation.Protocol.PROTEUS -> ConvProtocol.PROTEUS Conversation.Protocol.MIXED -> ConvProtocol.MIXED @@ -586,3 +674,17 @@ internal fun ConversationEntity.VerificationStatus.toModel(): Conversation.Verif ConversationEntity.VerificationStatus.NOT_VERIFIED -> Conversation.VerificationStatus.NOT_VERIFIED ConversationEntity.VerificationStatus.DEGRADED -> Conversation.VerificationStatus.DEGRADED } + +internal fun ConversationFilter.toDao(): ConversationFilterEntity = when (this) { + ConversationFilter.ALL -> ConversationFilterEntity.ALL + ConversationFilter.FAVORITES -> ConversationFilterEntity.FAVORITES + ConversationFilter.GROUPS -> ConversationFilterEntity.GROUPS + ConversationFilter.ONE_ON_ONE -> ConversationFilterEntity.ONE_ON_ONE +} + +internal fun ConversationFilterEntity.toModel(): ConversationFilter = when (this) { + ConversationFilterEntity.ALL -> ConversationFilter.ALL + ConversationFilterEntity.FAVORITES -> ConversationFilter.FAVORITES + ConversationFilterEntity.GROUPS -> ConversationFilter.GROUPS + ConversationFilterEntity.ONE_ON_ONE -> ConversationFilter.ONE_ON_ONE +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt index 9bbf027020b..f199fe862f7 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepository.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.conversation.Conversation.ProtocolInfo.MLSCapable.GroupState import com.wire.kalium.logic.data.conversation.mls.EpochChangesData +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.IdMapper @@ -36,9 +37,7 @@ import com.wire.kalium.logic.data.id.toApi import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.id.toDao import com.wire.kalium.logic.data.id.toModel -import com.wire.kalium.logic.data.message.MessageMapper import com.wire.kalium.logic.data.message.SelfDeletionTimer -import com.wire.kalium.logic.data.message.UnreadEventType import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either @@ -57,8 +56,6 @@ import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapMLSRequest import com.wire.kalium.logic.wrapStorageRequest -import com.wire.kalium.network.api.base.authenticated.client.ClientApi -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO import com.wire.kalium.network.api.authenticated.conversation.ConversationRenameResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse @@ -67,28 +64,32 @@ import com.wire.kalium.network.api.authenticated.conversation.UpdateConversation import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationReceiptModeResponse import com.wire.kalium.network.api.authenticated.conversation.model.ConversationMemberRoleDTO import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO +import com.wire.kalium.network.api.base.authenticated.client.ClientApi +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.member.MemberDAO import com.wire.kalium.persistence.dao.message.MessageDAO import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO -import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity import com.wire.kalium.util.DelicateKaliumApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.datetime.Instant @Suppress("TooManyFunctions") interface ConversationRepository { + val extensions: ConversationRepositoryExtensions + // region Get/Observe by id suspend fun observeConversationById(conversationId: ConversationId): Flow> @@ -129,7 +130,16 @@ interface ConversationRepository { suspend fun getConversationList(): Either>> suspend fun observeConversationList(): Flow> - suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> + suspend fun observeConversationListDetails( + fromArchive: Boolean, + conversationFilter: ConversationFilter = ConversationFilter.ALL + ): Flow> + + suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean = false, + conversationFilter: ConversationFilter = ConversationFilter.ALL + ): Flow> + suspend fun getConversationIds( type: Conversation.Type, protocol: Conversation.Protocol, @@ -303,6 +313,7 @@ interface ConversationRepository { suspend fun observeLegalHoldStatusChangeNotified(conversationId: ConversationId): Flow> suspend fun getGroupStatusMembersNamesAndHandles(groupID: GroupID): Either + suspend fun selectMembersNameAndHandle(conversationId: ConversationId): Either> } @Suppress("LongParameterList", "TooManyFunctions", "LargeClass") @@ -314,19 +325,20 @@ internal class ConversationDataSource internal constructor( private val memberDAO: MemberDAO, private val conversationApi: ConversationApi, private val messageDAO: MessageDAO, + private val messageDraftDAO: MessageDraftDAO, private val clientDAO: ClientDAO, private val clientApi: ClientApi, private val conversationMetaDataDAO: ConversationMetaDataDAO, - private val messageDraftDAO: MessageDraftDAO, private val idMapper: IdMapper = MapperProvider.idMapper(), private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId), private val memberMapper: MemberMapper = MapperProvider.memberMapper(), private val conversationStatusMapper: ConversationStatusMapper = MapperProvider.conversationStatusMapper(), private val conversationRoleMapper: ConversationRoleMapper = MapperProvider.conversationRoleMapper(), private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(), - private val messageMapper: MessageMapper = MapperProvider.messageMapper(selfUserId), private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper() ) : ConversationRepository { + override val extensions: ConversationRepositoryExtensions = + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper) // region Get/Observe by id @@ -347,14 +359,14 @@ internal class ConversationDataSource internal constructor( conversationMapper.fromConversationEntityType(it) } } + override suspend fun observeConversationDetailsById(conversationID: ConversationId): Flow> = conversationDAO.observeConversationDetailsById(conversationID.toDao()) .wrapStorageRequest() - // TODO we don't need last message and unread count here, we should discuss to divide model for list and for details .map { eitherConversationView -> eitherConversationView.flatMap { try { - Either.Right(conversationMapper.fromDaoModelToDetails(it, null, mapOf())) + Either.Right(conversationMapper.fromDaoModelToDetails(it)) } catch (error: IllegalArgumentException) { kaliumLogger.e("require field in conversation Details", error) Either.Left(StorageFailure.DataNotFound) @@ -512,30 +524,36 @@ internal class ConversationDataSource internal constructor( return conversationDAO.getAllConversations().map { it.map(conversationMapper::fromDaoModel) } } - override suspend fun observeConversationListDetails(fromArchive: Boolean): Flow> = + override suspend fun observeConversationListDetails( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> = + conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()).map { conversationViewEntityList -> + conversationViewEntityList.map { conversationViewEntity -> conversationMapper.fromDaoModelToDetails(conversationViewEntity) } + } + + override suspend fun observeConversationListDetailsWithEvents( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> = combine( - conversationDAO.getAllConversationDetails(fromArchive), + conversationDAO.getAllConversationDetails(fromArchive, conversationFilter.toDao()), if (fromArchive) flowOf(listOf()) else messageDAO.observeLastMessages(), messageDAO.observeConversationsUnreadEvents(), messageDraftDAO.observeMessageDrafts() ) { conversationList, lastMessageList, unreadEvents, drafts -> val lastMessageMap = lastMessageList.associateBy { it.conversationId } val messageDraftMap = drafts.filter { it.text.isNotBlank() }.associateBy { it.conversationId } + val unreadEventsMap = unreadEvents.associateBy { it.conversationId } conversationList.map { conversation -> - conversationMapper.fromDaoModelToDetails( - conversation, - lastMessage = messageDraftMap[conversation.id]?.let { messageMapper.fromDraftToMessagePreview(it) } - ?: lastMessageMap[conversation.id]?.let { messageMapper.fromEntityToMessagePreview(it) }, - unreadEventCount = unreadEvents.firstOrNull { it.conversationId == conversation.id }?.unreadEvents?.mapKeys { - when (it.key) { - UnreadEventTypeEntity.KNOCK -> UnreadEventType.KNOCK - UnreadEventTypeEntity.MISSED_CALL -> UnreadEventType.MISSED_CALL - UnreadEventTypeEntity.MENTION -> UnreadEventType.MENTION - UnreadEventTypeEntity.REPLY -> UnreadEventType.REPLY - UnreadEventTypeEntity.MESSAGE -> UnreadEventType.MESSAGE - } - } + conversationMapper.fromDaoModelToDetailsWithEvents( + ConversationDetailsWithEventsEntity( + conversationViewEntity = conversation, + lastMessage = lastMessageMap[conversation.id], + messageDraft = messageDraftMap[conversation.id], + unreadEvents = unreadEventsMap[conversation.id] ?: ConversationUnreadEventEntity(conversation.id, mapOf()), + ) ) } } @@ -544,6 +562,7 @@ internal class ConversationDataSource internal constructor( wrapApiRequest { conversationApi.fetchMlsOneToOneConversation(userId.toApi()) }.map { conversationResponse -> + // question: do we need to do this? since it's one on one! addOtherMemberIfMissing(conversationResponse, userId) }.flatMap { conversationResponse -> val selfUserTeamId = selfTeamIdProvider().getOrNull() @@ -552,7 +571,9 @@ internal class ConversationDataSource internal constructor( selfUserTeamId = selfUserTeamId ).map { conversationResponse } }.flatMap { response -> - this.getConversationById(response.id.toModel()) + this.getConversationById(response.id.toModel()).map { + it.copy(mlsPublicKeys = conversationMapper.fromApiModel(response.publicKeys)) + } } private fun addOtherMemberIfMissing( @@ -988,8 +1009,8 @@ internal class ConversationDataSource internal constructor( } override suspend fun getConversationDetailsByMLSGroupId(mlsGroupId: GroupID): Either = - wrapStorageRequest { conversationDAO.getConversationByGroupID(mlsGroupId.value) } - .map { conversationMapper.fromDaoModelToDetails(it, null, mapOf()) } + wrapStorageRequest { conversationDAO.getConversationDetailsByGroupID(mlsGroupId.value) } + .map { conversationMapper.fromDaoModelToDetails(it) } override suspend fun observeUnreadArchivedConversationsCount(): Flow = conversationDAO.observeUnreadArchivedConversationsCount() @@ -1102,6 +1123,13 @@ internal class ConversationDataSource internal constructor( conversationDAO.selectGroupStatusMembersNamesAndHandles(groupID.value) }.map { EpochChangesData.fromEntity(it) } + override suspend fun selectMembersNameAndHandle(conversationId: ConversationId): Either> = + wrapStorageRequest { + memberDAO.selectMembersNameAndHandle(conversationId.toDao()) + .mapValues { NameAndHandle.fromEntity(it.value) } + .mapKeys { it.key.toModel() } + } + companion object { const val DEFAULT_MEMBER_ROLE = "wire_member" } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt new file mode 100644 index 00000000000..aec18932a97 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensions.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation + +import app.cash.paging.PagingConfig +import app.cash.paging.PagingData +import app.cash.paging.map +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig +import com.wire.kalium.persistence.dao.message.KaliumPager +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +interface ConversationRepositoryExtensions { + suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( + queryConfig: ConversationQueryConfig, + pagingConfig: PagingConfig, + startingOffset: Long, + ): Flow> +} + +class ConversationRepositoryExtensionsImpl internal constructor( + private val conversationDAO: ConversationDAO, + private val conversationMapper: ConversationMapper +) : ConversationRepositoryExtensions { + override suspend fun getPaginatedConversationDetailsWithEventsBySearchQuery( + queryConfig: ConversationQueryConfig, + pagingConfig: PagingConfig, + startingOffset: Long + ): Flow> { + val pager: KaliumPager = with(queryConfig) { + conversationDAO.platformExtensions.getPagerForConversationDetailsWithEventsSearch( + queryConfig = QueryConfig( + searchQuery = searchQuery, + fromArchive = fromArchive, + onlyInteractionEnabled = onlyInteractionEnabled, + newActivitiesOnTop = newActivitiesOnTop, + conversationFilter = conversationFilter.toDao() + ), + pagingConfig = pagingConfig + ) + } + + return pager.pagingDataFlow.map { pagingData -> + pagingData + .map { conversationDetailsWithEventsEntity -> + conversationMapper.fromDaoModelToDetailsWithEvents( + conversationDetailsWithEventsEntity + ) + } + } + } +} + +data class ConversationQueryConfig( + val searchQuery: String = "", + val fromArchive: Boolean = false, + val onlyInteractionEnabled: Boolean = false, + val newActivitiesOnTop: Boolean = false, + val conversationFilter: ConversationFilter = ConversationFilter.ALL, +) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt index e94a5864fa1..942c55a22b2 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinExistingMLSConversationUseCase.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.client.ClientRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.featureFlags.FeatureSupport import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap @@ -51,7 +52,7 @@ import kotlinx.coroutines.withContext * but has not yet joined the corresponding MLS group. */ internal interface JoinExistingMLSConversationUseCase { - suspend operator fun invoke(conversationId: ConversationId): Either + suspend operator fun invoke(conversationId: ConversationId, mlsPublicKeys: MLSPublicKeys? = null): Either } @Suppress("LongParameterList") @@ -65,7 +66,7 @@ internal class JoinExistingMLSConversationUseCaseImpl( ) : JoinExistingMLSConversationUseCase { private val dispatcher = kaliumDispatcher.io - override suspend operator fun invoke(conversationId: ConversationId): Either = + override suspend operator fun invoke(conversationId: ConversationId, mlsPublicKeys: MLSPublicKeys?): Either = if (!featureSupport.isMLSSupported || !clientRepository.hasRegisteredMLSClient().getOrElse(false) ) { @@ -76,15 +77,16 @@ internal class JoinExistingMLSConversationUseCaseImpl( Either.Left(StorageFailure.DataNotFound) }, { conversation -> withContext(dispatcher) { - joinOrEstablishMLSGroupAndRetry(conversation) + joinOrEstablishMLSGroupAndRetry(conversation, mlsPublicKeys) } }) } private suspend fun joinOrEstablishMLSGroupAndRetry( - conversation: Conversation + conversation: Conversation, + mlsPublicKeys: MLSPublicKeys? ): Either = - joinOrEstablishMLSGroup(conversation) + joinOrEstablishMLSGroup(conversation, mlsPublicKeys) .flatMapLeft { failure -> if (failure is NetworkFailure.ServerMiscommunication && failure.kaliumException is KaliumException.InvalidRequestError) { if (failure.kaliumException.isMlsStaleMessage()) { @@ -101,13 +103,15 @@ internal class JoinExistingMLSConversationUseCaseImpl( // Re-fetch current epoch and try again if (conversation.type == Conversation.Type.ONE_ON_ONE) { conversationRepository.getConversationMembers(conversation.id).flatMap { - conversationRepository.fetchMlsOneToOneConversation(it.first()) + conversationRepository.fetchMlsOneToOneConversation(it.first()).map { + it.mlsPublicKeys + } } } else { conversationRepository.fetchConversation(conversation.id) }.flatMap { conversationRepository.getConversationById(conversation.id).flatMap { conversation -> - joinOrEstablishMLSGroup(conversation) + joinOrEstablishMLSGroup(conversation, null) } } } else if (failure.kaliumException.isMlsMissingGroupInfo()) { @@ -122,7 +126,7 @@ internal class JoinExistingMLSConversationUseCaseImpl( } @Suppress("LongMethod") - private suspend fun joinOrEstablishMLSGroup(conversation: Conversation): Either { + private suspend fun joinOrEstablishMLSGroup(conversation: Conversation, publicKeys: MLSPublicKeys?): Either { val protocol = conversation.protocol val type = conversation.type return when { @@ -202,7 +206,8 @@ internal class JoinExistingMLSConversationUseCaseImpl( conversationRepository.getConversationMembers(conversation.id).flatMap { members -> mlsConversationRepository.establishMLSGroup( protocol.groupId, - members + members, + publicKeys ) }.onSuccess { kaliumLogger.logStructuredJson( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt index 6c90376d6ae..ee7f3428c55 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/JoinSubconversationUseCase.kt @@ -50,6 +50,7 @@ internal interface JoinSubconversationUseCase { suspend operator fun invoke(conversationId: ConversationId, subconversationId: SubconversationId): Either } +// TODO(refactor): usecase should not access API class directly, use SubconversationRepository instead internal class JoinSubconversationUseCaseImpl( private val conversationApi: ConversationApi, private val mlsConversationRepository: MLSConversationRepository, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt index 058a4736ce7..c6e8c608c27 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepository.kt @@ -46,9 +46,11 @@ import com.wire.kalium.logic.data.keypackage.KeyPackageLimitsProvider import com.wire.kalium.logic.data.keypackage.KeyPackageRepository import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.data.mlspublickeys.getRemovalKey import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.data.e2ei.RevocationListChecker +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.flatMapLeft @@ -123,6 +125,7 @@ interface MLSConversationRepository { suspend fun establishMLSGroup( groupID: GroupID, members: List, + publicKeys: MLSPublicKeys? = null, allowSkippingUsersWithoutKeyPackages: Boolean = false ): Either @@ -554,16 +557,18 @@ internal class MLSConversationDataSource( override suspend fun establishMLSGroup( groupID: GroupID, members: List, - allowSkippingUsersWithoutKeyPackages: Boolean, + publicKeys: MLSPublicKeys?, + allowSkippingUsersWithoutKeyPackages: Boolean ): Either = withContext(serialDispatcher) { - mlsClientProvider.getMLSClient().flatMap { - mlsPublicKeysRepository.getKeyForCipherSuite( - CipherSuite.fromTag(it.getDefaultCipherSuite()) - ).flatMap { key -> + mlsClientProvider.getMLSClient().flatMap { mlsClient -> + val cipherSuite = CipherSuite.fromTag(mlsClient.getDefaultCipherSuite()) + val keys = publicKeys?.getRemovalKey(cipherSuite) ?: mlsPublicKeysRepository.getKeyForCipherSuite(cipherSuite) + + keys.flatMap { externalSenders -> establishMLSGroup( groupID = groupID, members = members, - externalSenders = key, + externalSenders = externalSenders, allowPartialMemberList = allowSkippingUsersWithoutKeyPackages ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt index f2a1a6e8a9b..d6281310b22 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/SubconversationRepository.kt @@ -17,34 +17,81 @@ */ package com.wire.kalium.logic.data.conversation +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.SubconversationId +import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.network.api.authenticated.conversation.SubconversationDeleteRequest +import com.wire.kalium.network.api.authenticated.conversation.SubconversationResponse +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import io.ktor.util.collections.ConcurrentMap import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock interface SubconversationRepository { - suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID) - suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID? - suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId) + suspend fun insertSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId, + groupId: GroupID + ) + + suspend fun getSubconversationInfo( + conversationId: ConversationId, + subconversationId: SubconversationId + ): GroupID? + + suspend fun deleteSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId + ) + suspend fun containsSubconversation(groupId: GroupID): Boolean + suspend fun deleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + subConversationDeleteRequest: SubconversationDeleteRequest + ): Either + suspend fun fetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either + + suspend fun fetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either } -class SubconversationRepositoryImpl : SubconversationRepository { +class SubconversationRepositoryImpl( + private val conversationApi: ConversationApi +) : SubconversationRepository { private val mutex = Mutex() private val subconversations = ConcurrentMap, GroupID>() - override suspend fun insertSubconversation(conversationId: ConversationId, subconversationId: SubconversationId, groupId: GroupID) { + override suspend fun insertSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId, + groupId: GroupID + ) { mutex.withLock { subconversations[Pair(conversationId, subconversationId)] = groupId } } - override suspend fun getSubconversationInfo(conversationId: ConversationId, subconversationId: SubconversationId): GroupID? { + override suspend fun getSubconversationInfo( + conversationId: ConversationId, + subconversationId: SubconversationId + ): GroupID? { mutex.withLock { return subconversations[Pair(conversationId, subconversationId)] } @@ -56,10 +103,50 @@ class SubconversationRepositoryImpl : SubconversationRepository { } } - override suspend fun deleteSubconversation(conversationId: ConversationId, subconversationId: SubconversationId) { + override suspend fun deleteSubconversation( + conversationId: ConversationId, + subconversationId: SubconversationId + ) { mutex.withLock { subconversations.remove(Pair(conversationId, subconversationId)) } } + override suspend fun deleteRemoteSubConversation( + conversationId: ConversationId, + subConversationId: SubconversationId, + subConversationDeleteRequest: SubconversationDeleteRequest + ): Either = + wrapApiRequest { + conversationApi.deleteSubconversation( + conversationId.toApi(), + subConversationId.toApi(), + subConversationDeleteRequest + ) + }.onSuccess { + kaliumLogger.i("Subconversation deleted successfully") + }.onFailure { + kaliumLogger.i("Failed to delete subconversation") + } + + override suspend fun fetchRemoteSubConversationGroupInfo( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either = wrapApiRequest { + conversationApi.fetchSubconversationGroupInfo( + conversationId.toApi(), + subConversationId.toApi() + ) + } + + // TODO: Replace SubconversationResponse with a domain model + override suspend fun fetchRemoteSubConversationDetails( + conversationId: ConversationId, + subConversationId: SubconversationId + ): Either = wrapApiRequest { + conversationApi.fetchSubconversationDetails( + conversationId.toApi(), + subConversationId.toApi() + ) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt new file mode 100644 index 00000000000..fc20894926b --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderMappers.kt @@ -0,0 +1,86 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation.folders + +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.id.toModel +import com.wire.kalium.network.api.authenticated.properties.LabelDTO +import com.wire.kalium.network.api.authenticated.properties.LabelTypeDTO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity +import com.wire.kalium.persistence.dao.conversation.folder.FolderWithConversationsEntity + +fun LabelDTO.toFolder(selfDomain: String) = FolderWithConversations( + conversationIdList = qualifiedConversations?.map { it.toModel() } ?: conversations.map { QualifiedID(it, selfDomain) }, + id = id, + name = name, + type = type.toFolderType() +) + +fun FolderWithConversations.toLabel() = LabelDTO( + id = id, + name = name, + qualifiedConversations = conversationIdList.map { it.toApi() }, + conversations = conversationIdList.map { it.value }, + type = type.toLabel() +) + +fun LabelTypeDTO.toFolderType() = when (this) { + LabelTypeDTO.USER -> FolderType.USER + LabelTypeDTO.FAVORITE -> FolderType.FAVORITE +} + +fun FolderType.toLabel() = when (this) { + FolderType.USER -> LabelTypeDTO.USER + FolderType.FAVORITE -> LabelTypeDTO.FAVORITE +} + +fun ConversationFolderEntity.toModel() = ConversationFolder( + id = id, + name = name, + type = type.toModel() +) + +fun FolderWithConversationsEntity.toModel() = FolderWithConversations( + id = id, + name = name, + type = type.toModel(), + conversationIdList = conversationIdList.map { it.toModel() } +) + +fun FolderWithConversations.toDao() = FolderWithConversationsEntity( + id = id, + name = name, + type = type.toDao(), + conversationIdList = conversationIdList.map { it.toDao() } +) + +fun FolderType.toDao() = when (this) { + FolderType.USER -> ConversationFolderTypeEntity.USER + FolderType.FAVORITE -> ConversationFolderTypeEntity.FAVORITE +} + +fun ConversationFolderTypeEntity.toModel(): FolderType = when (this) { + ConversationFolderTypeEntity.USER -> FolderType.USER + ConversationFolderTypeEntity.FAVORITE -> FolderType.FAVORITE +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt new file mode 100644 index 00000000000..70ad373bdfa --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepository.kt @@ -0,0 +1,155 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation.folders + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.CONVERSATIONS_FOLDERS +import com.wire.kalium.logger.obfuscateId +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.ConversationMapper +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.di.MapperProvider +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.flatMapLeft +import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.wrapApiRequest +import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO +import com.wire.kalium.network.api.authenticated.properties.PropertyKey +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.exceptions.KaliumException +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal interface ConversationFolderRepository { + + suspend fun getFavoriteConversationFolder(): Either + suspend fun observeConversationsFromFolder(folderId: String): Flow> + suspend fun updateConversationFolders(folderWithConversations: List): Either + suspend fun fetchConversationFolders(): Either + suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either + suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either + suspend fun syncConversationFoldersFromLocal(): Either +} + +internal class ConversationFolderDataSource internal constructor( + private val conversationFolderDAO: ConversationFolderDAO, + private val userPropertiesApi: PropertiesApi, + private val selfUserId: QualifiedID, + private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId) +) : ConversationFolderRepository { + + override suspend fun updateConversationFolders(folderWithConversations: List): Either = + wrapStorageRequest { + conversationFolderDAO.updateConversationFolders(folderWithConversations.map { it.toDao() }) + } + + override suspend fun getFavoriteConversationFolder(): Either = wrapStorageRequest { + conversationFolderDAO.getFavoriteConversationFolder().toModel() + } + + override suspend fun observeConversationsFromFolder(folderId: String): Flow> = + conversationFolderDAO.observeConversationListFromFolder(folderId).map { conversationDetailsWithEventsEntityList -> + conversationDetailsWithEventsEntityList.map { + conversationMapper.fromDaoModelToDetailsWithEvents(it) + } + } + + override suspend fun fetchConversationFolders(): Either = wrapApiRequest { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Fetching conversation folders") + userPropertiesApi.getLabels() + } + .flatMapLeft { + if (it is NetworkFailure.ServerMiscommunication + && it.kaliumException is KaliumException.InvalidRequestError + && it.kaliumException.errorResponse.code == HttpStatusCode.NotFound.value + ) { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("User has no labels, creating an empty list") + // If the user has no labels, we create an empty list and on next stage we will create a favorite label + Either.Right(LabelListResponseDTO(emptyList())) + } else { + Either.Left(it) + } + } + .onSuccess { labelsResponse -> + val folders = labelsResponse.labels.map { it.toFolder(selfUserId.domain) }.toMutableList() + val favoriteLabel = folders.firstOrNull { it.type == FolderType.FAVORITE } + + if (favoriteLabel == null) { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Favorite label not found, creating a new one") + folders.add( + FolderWithConversations( + id = uuid4().toString(), + name = "", // name will be handled by localization + type = FolderType.FAVORITE, + conversationIdList = emptyList() + ) + ) + userPropertiesApi.setProperty( + PropertyKey.WIRE_LABELS, + labelsResponse.copy(labels = folders.map { it.toLabel() }) + ) + } + conversationFolderDAO.updateConversationFolders(folders.map { it.toDao() }) + } + .onFailure { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).e("Error fetching conversation folders $it") + Either.Left(it) + } + .map { } + + override suspend fun addConversationToFolder(conversationId: QualifiedID, folderId: String): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS) + .v("Adding conversation ${conversationId.toLogString()} to folder ${folderId.obfuscateId()}") + return wrapStorageRequest { + conversationFolderDAO.addConversationToFolder(conversationId.toDao(), folderId) + } + } + + override suspend fun removeConversationFromFolder(conversationId: QualifiedID, folderId: String): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS) + .v("Removing conversation ${conversationId.toLogString()} from folder ${folderId.obfuscateId()}") + return wrapStorageRequest { + conversationFolderDAO.removeConversationFromFolder(conversationId.toDao(), folderId) + } + } + + override suspend fun syncConversationFoldersFromLocal(): Either { + kaliumLogger.withFeatureId(CONVERSATIONS_FOLDERS).v("Syncing conversation folders from local") + return wrapStorageRequest { conversationFolderDAO.getFoldersWithConversations().map { it.toModel() } } + .flatMap { + wrapApiRequest { + userPropertiesApi.updateLabels( + LabelListResponseDTO(it.map { it.toLabel() }) + ) + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt index c70c52611d8..a56b28876d6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/RevocationListChecker.kt @@ -32,7 +32,7 @@ import com.wire.kalium.logic.kaliumLogger /** * Use case to check if the CRL is expired and if so, register CRL and update conversation statuses if there is a change. */ -interface RevocationListChecker { +internal interface RevocationListChecker { suspend fun check(url: String): Either } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt index ea9fdebba3a..a7f3f2ca919 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/Event.kt @@ -23,10 +23,13 @@ import com.wire.kalium.logger.obfuscateDomain import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.data.client.Client import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.conversation.Conversation.Access +import com.wire.kalium.logic.data.conversation.Conversation.AccessRole import com.wire.kalium.logic.data.conversation.Conversation.Member import com.wire.kalium.logic.data.conversation.Conversation.Protocol import com.wire.kalium.logic.data.conversation.Conversation.ReceiptMode import com.wire.kalium.logic.data.conversation.Conversation.TypingIndicatorMode +import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.featureConfig.AppLockModel import com.wire.kalium.logic.data.featureConfig.ClassifiedDomainsModel @@ -55,7 +58,7 @@ import kotlinx.serialization.json.JsonNull */ data class EventEnvelope( val event: Event, - val deliveryInfo: EventDeliveryInfo, + val deliveryInfo: EventDeliveryInfo ) { override fun toString(): String { return super.toString() @@ -124,7 +127,8 @@ sealed class Event(open val id: String) { data class AccessUpdate( override val id: String, override val conversationId: ConversationId, - val data: ConversationResponse, + val access: Set, + val accessRole: Set, val qualifiedFrom: UserId, ) : Conversation(id, conversationId) { @@ -387,14 +391,16 @@ sealed class Event(open val id: String) { val uri: String?, val isPasswordProtected: Boolean, ) : Conversation(id, conversationId) { - override fun toLogMap(): Map = mapOf(typeKey to "Conversation.CodeUpdated") + override fun toLogMap(): Map = + mapOf(typeKey to "Conversation.CodeUpdated") } data class CodeDeleted( override val id: String, override val conversationId: ConversationId, ) : Conversation(id, conversationId) { - override fun toLogMap(): Map = mapOf(typeKey to "Conversation.CodeDeleted") + override fun toLogMap(): Map = + mapOf(typeKey to "Conversation.CodeDeleted") } data class TypingIndicator( @@ -705,6 +711,17 @@ sealed class Event(open val id: String) { "value" to "$value" ) } + + data class FoldersUpdate( + override val id: String, + val folders: List, + ) : UserProperty(id) { + override fun toLogMap(): Map = mapOf( + typeKey to "User.UserProperty.FoldersUpdate", + idKey to id.obfuscateId(), + "folders" to folders.map { it.id.obfuscateId() } + ) + } } data class Unknown( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt new file mode 100644 index 00000000000..bb894034c2b --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.kt @@ -0,0 +1,127 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.event + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.cryptography.CryptoClientId +import com.wire.kalium.cryptography.CryptoSessionId +import com.wire.kalium.cryptography.ProteusClient +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedClientID +import com.wire.kalium.logic.data.id.toApi +import com.wire.kalium.logic.data.id.toCrypto +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.message.PlainMessageBlob +import com.wire.kalium.logic.data.message.ProtoContent +import com.wire.kalium.logic.data.message.ProtoContentMapper +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.di.MapperProvider +import com.wire.kalium.network.api.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.authenticated.notification.EventResponse +import com.wire.kalium.network.api.authenticated.notification.conversation.MessageEventData +import io.ktor.util.encodeBase64 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.datetime.Clock + +class EventGenerator(private val selfClient: QualifiedClientID, targetClient: QualifiedClientID, val proteusClient: ProteusClient) { + + private val protoContentMapper: ProtoContentMapper = MapperProvider.protoContentMapper(selfClient.userId) + private val targetSessionId = CryptoSessionId( + targetClient.userId.toCrypto(), + CryptoClientId(targetClient.clientId.value) + ) + private val selfSessionId = CryptoSessionId( + selfClient.userId.toCrypto(), + CryptoClientId(selfClient.clientId.value) + ) + + fun generateEvents( + limit: Int, + conversationId: ConversationId, + ): Flow { + return flow { + repeat(limit) { count -> + val protobuf = generateProtoContent(generateTextContent(count)) + val message = encryptMessage(protobuf, proteusClient, selfSessionId, targetSessionId) + val event = generateNewMessageDTO(selfClient.userId, conversationId, message) + emit(generateEventResponse(event)) + } + } + } + + private fun generateTextContent( + index: Int + ): MessageContent.Text { + return MessageContent.Text("Message $index") + } + + private fun generateProtoContent( + messageContent: MessageContent.FromProto + ): PlainMessageBlob { + return protoContentMapper.encodeToProtobuf( + ProtoContent.Readable( + messageUid = uuid4().toString(), + messageContent = messageContent, + expectsReadConfirmation = true, + legalHoldStatus = Conversation.LegalHoldStatus.DISABLED, + expiresAfterMillis = null + ) + ) + } + + private suspend fun encryptMessage( + message: PlainMessageBlob, + proteusClient: ProteusClient, + sender: CryptoSessionId, + recipient: CryptoSessionId + ): MessageEventData { + return MessageEventData( + text = proteusClient.encrypt(message.data, recipient).encodeBase64(), + sender = sender.cryptoClientId.value, + recipient = recipient.cryptoClientId.value, + encryptedExternalData = null + ) + } + + private fun generateNewMessageDTO( + from: UserId, + conversationId: ConversationId, + data: MessageEventData + ): EventContentDTO.Conversation.NewMessageDTO { + return EventContentDTO.Conversation.NewMessageDTO( + qualifiedConversation = conversationId.toApi(), + qualifiedFrom = from.toApi(), + conversation = conversationId.toPlainID().value, + from = from.toPlainID().value, + time = Clock.System.now(), + data = data + ) + + } + + private fun generateEventResponse(event: EventContentDTO): EventResponse { + return EventResponse( + id = uuid4().toString(), // TODO jacob (should actually be UUIDv1) + payload = listOf(event), + transient = true // All events are transient to avoid persisting an incorrect last event id + ) + } + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt index d907751c978..9bbffa82e2c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventMapper.kt @@ -23,10 +23,12 @@ import com.wire.kalium.logic.data.client.ClientMapper import com.wire.kalium.logic.data.connection.ConnectionMapper import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.ConversationMapper import com.wire.kalium.logic.data.conversation.ConversationRoleMapper import com.wire.kalium.logic.data.conversation.MemberMapper import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.conversation.ReceiptModeMapper +import com.wire.kalium.logic.data.conversation.folders.toFolder import com.wire.kalium.logic.data.conversation.toModel import com.wire.kalium.logic.data.event.Event.UserProperty.ReadReceiptModeSet import com.wire.kalium.logic.data.event.Event.UserProperty.TypingIndicatorModeSet @@ -64,7 +66,8 @@ class EventMapper( private val selfUserId: UserId, private val receiptModeMapper: ReceiptModeMapper = MapperProvider.receiptModeMapper(), private val clientMapper: ClientMapper = MapperProvider.clientMapper(), - private val qualifiedIdMapper: QualifiedIdMapper = MapperProvider.qualifiedIdMapper(selfUserId) + private val qualifiedIdMapper: QualifiedIdMapper = MapperProvider.qualifiedIdMapper(selfUserId), + private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId) ) { fun fromDTO(eventResponse: EventResponse, isLive: Boolean): List { // TODO(edge-case): Multiple payloads in the same event have the same ID, is this an issue when marking lastProcessedEventId? @@ -94,7 +97,7 @@ class EventMapper( is EventContentDTO.User.LegalHoldDisabledDTO -> legalHoldDisabled(id, eventContentDTO) is EventContentDTO.FeatureConfig.FeatureConfigUpdatedDTO -> featureConfig(id, eventContentDTO) is EventContentDTO.Unknown -> unknown(id, eventContentDTO) - is EventContentDTO.Conversation.AccessUpdate -> unknown(id, eventContentDTO) + is EventContentDTO.Conversation.AccessUpdate -> conversationAccessUpdate(id, eventContentDTO) is EventContentDTO.Conversation.DeletedConversationDTO -> conversationDeleted(id, eventContentDTO) is EventContentDTO.Conversation.ConversationRenameDTO -> conversationRenamed(id, eventContentDTO) is EventContentDTO.Team.MemberLeave -> teamMemberLeft(id, eventContentDTO) @@ -194,6 +197,17 @@ class EventMapper( dateTime = eventContentDTO.time ) + private fun conversationAccessUpdate( + id: String, + eventContentDTO: EventContentDTO.Conversation.AccessUpdate + ): Event = Event.Conversation.AccessUpdate( + id = id, + conversationId = eventContentDTO.qualifiedConversation.toModel(), + access = conversationMapper.fromApiModelToAccessModel(eventContentDTO.data.access), + accessRole = conversationMapper.fromApiModelToAccessRoleModel(eventContentDTO.data.accessRole), + qualifiedFrom = eventContentDTO.qualifiedFrom.toModel() + ) + private fun conversationReceiptModeUpdate( id: String, eventContentDTO: EventContentDTO.Conversation.ReceiptModeUpdate, @@ -210,8 +224,8 @@ class EventMapper( ): Event { val fieldKeyValue = eventContentDTO.value val key = eventContentDTO.key - return when { - fieldKeyValue is EventContentDTO.FieldKeyNumberValue -> { + return when (fieldKeyValue) { + is EventContentDTO.FieldKeyNumberValue -> { when (key) { WIRE_RECEIPT_MODE.key -> ReadReceiptModeSet( id, @@ -231,7 +245,12 @@ class EventMapper( } } - else -> unknown( + is EventContentDTO.FieldLabelListValue -> Event.UserProperty.FoldersUpdate( + id = id, + folders = fieldKeyValue.value.labels.map { it.toFolder(selfUserId.domain) } + ) + + is EventContentDTO.FieldUnknownValue -> unknown( id = id, eventContentDTO = eventContentDTO, cause = "Unknown value type for key: ${eventContentDTO.key} " @@ -437,7 +456,7 @@ class EventMapper( } @Suppress("MagicNumber") - private fun mapConversationMutedStatus(status: Int?) = when (status) { + private fun mapConversationMutedStatus(status: Int?): MutedConversationStatus = when (status) { 0 -> MutedConversationStatus.AllAllowed 1 -> MutedConversationStatus.OnlyMentionsAndRepliesAllowed 3 -> MutedConversationStatus.AllMuted diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt index 8f4fca7ce28..a91b00d2225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventRepository.kt @@ -22,35 +22,41 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.logic.wrapStorageRequest -import com.wire.kalium.network.api.base.authenticated.notification.NotificationApi import com.wire.kalium.network.api.authenticated.notification.NotificationResponse +import com.wire.kalium.network.api.base.authenticated.notification.NotificationApi import com.wire.kalium.network.api.base.authenticated.notification.WebSocketEvent import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.isSuccessful import com.wire.kalium.persistence.dao.MetadataDAO import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.isActive +import kotlinx.serialization.json.Json import kotlin.coroutines.coroutineContext interface EventRepository { + suspend fun pendingEvents(): Flow> suspend fun liveEvents(): Either>> suspend fun updateLastProcessedEventId(eventId: String): Either + /** + * Parse events from an external JSON payload + * + * @return List of [EventEnvelope] + */ + fun parseExternalEvents(data: String): List + /** * Retrieves the last processed event ID from the storage. * @@ -76,6 +82,7 @@ interface EventRepository { * @return Either containing a [CoreFailure] or the oldest available event ID as a String. */ suspend fun fetchOldestAvailableEventId(): Either + suspend fun fetchServerTime(): String? } class EventDataSource( @@ -94,26 +101,31 @@ class EventDataSource( currentClientId().flatMap { clientId -> liveEventsFlow(clientId) } private suspend fun liveEventsFlow(clientId: ClientId): Either>> = - wrapApiRequest { notificationApi.listenToLiveEvents(clientId.value) }.map { - it.map { webSocketEvent -> - when (webSocketEvent) { - is WebSocketEvent.Open -> { - flowOf(WebSocketEvent.Open()) - } - - is WebSocketEvent.NonBinaryPayloadReceived -> { - flowOf(WebSocketEvent.NonBinaryPayloadReceived(webSocketEvent.payload)) - } - - is WebSocketEvent.Close -> { - flowOf(WebSocketEvent.Close(webSocketEvent.cause)) - } - - is WebSocketEvent.BinaryPayloadReceived -> { - eventMapper.fromDTO(webSocketEvent.payload, true).asFlow().map { WebSocketEvent.BinaryPayloadReceived(it) } + wrapApiRequest { notificationApi.listenToLiveEvents(clientId.value) }.map { webSocketEventFlow -> + flow { + webSocketEventFlow.collect { webSocketEvent -> + when (webSocketEvent) { + is WebSocketEvent.Open -> { + emit(WebSocketEvent.Open()) + } + + is WebSocketEvent.NonBinaryPayloadReceived -> { + emit(WebSocketEvent.NonBinaryPayloadReceived(webSocketEvent.payload)) + } + + is WebSocketEvent.Close -> { + emit(WebSocketEvent.Close(webSocketEvent.cause)) + } + + is WebSocketEvent.BinaryPayloadReceived -> { + val events = eventMapper.fromDTO(webSocketEvent.payload, true) + events.forEach { eventEnvelope -> + emit(WebSocketEvent.BinaryPayloadReceived(eventEnvelope)) + } + } } } - }.flattenConcat() + } } private suspend fun pendingEventsFlow( @@ -145,6 +157,13 @@ class EventDataSource( } } + override fun parseExternalEvents(data: String): List { + val notificationResponse = Json.decodeFromString(data) + return notificationResponse.notifications.flatMap { + eventMapper.fromDTO(it, isLive = false) + } + } + override suspend fun lastProcessedEventId(): Either = wrapStorageRequest { metadataDAO.valueByKey(LAST_PROCESSED_EVENT_ID_KEY) } @@ -177,6 +196,15 @@ class EventDataSource( } }.map { it.id } + override suspend fun fetchServerTime(): String? { + val result = notificationApi.getServerTime(NOTIFICATIONS_QUERY_SIZE) + return if (result.isSuccessful()) { + result.value + } else { + null + } + } + private companion object { const val NOTIFICATIONS_QUERY_SIZE = 100 const val LAST_PROCESSED_EVENT_ID_KEY = "last_processed_event_id" diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt index 32044174038..c28f15eea3a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageMapper.kt @@ -375,6 +375,7 @@ class MessageMapperImpl( // We store the encoded data in case we decide to try to decrypt them again in the future is MessageContent.FailedDecryption -> MessageEntityContent.FailedDecryption( regularMessage.encodedData, + regularMessage.errorCode, regularMessage.isDecryptionResolved, regularMessage.senderUserId.toDao(), regularMessage.clientId?.value @@ -611,6 +612,7 @@ fun MessageEntityContent.Regular.toMessageContent(hidden: Boolean, selfUserId: U is MessageEntityContent.Unknown -> MessageContent.Unknown(this.typeName, this.encodedData, hidden) is MessageEntityContent.FailedDecryption -> MessageContent.FailedDecryption( this.encodedData, + this.code, this.isDecryptionResolved, this.senderUserId.toModel(), ClientId(this.senderClientId.orEmpty()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt index 66ce73703f3..dd0f47b5996 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageRepository.kt @@ -80,7 +80,6 @@ internal interface MessageRepository { ) suspend fun persistMessage( message: Message.Standalone, - updateConversationReadDate: Boolean = false, updateConversationModifiedDate: Boolean = false, ): Either @@ -321,12 +320,10 @@ internal class MessageDataSource internal constructor( ) override suspend fun persistMessage( message: Message.Standalone, - updateConversationReadDate: Boolean, updateConversationModifiedDate: Boolean, ): Either = wrapStorageRequest { messageDAO.insertOrIgnoreMessage( messageMapper.fromMessageToEntity(message), - updateConversationReadDate, updateConversationModifiedDate ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt index eebc9246e5d..0d0288de34f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCase.kt @@ -43,11 +43,10 @@ internal class PersistMessageUseCaseImpl( ) : PersistMessageUseCase { override suspend operator fun invoke(message: Message.Standalone): Either { val modifiedMessage = getExpectsReadConfirmationFromMessage(message) - val isSelfSender = message.isSelfTheSender(selfUserId) + return messageRepository.persistMessage( message = modifiedMessage, - updateConversationReadDate = isSelfSender, updateConversationModifiedDate = message.content.shouldUpdateConversationOrder() ).onSuccess { val isConversationMuted = it == InsertMessageResult.INSERTED_INTO_MUTED_CONVERSATION diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt index 6de692f0b05..036fa81bf0f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapper.kt @@ -57,6 +57,7 @@ import com.wire.kalium.protobuf.messages.Quote import com.wire.kalium.protobuf.messages.Reaction import com.wire.kalium.protobuf.messages.Text import com.wire.kalium.protobuf.messages.TrackingIdentifier +import com.wire.kalium.protobuf.messages.UnknownStrategy import kotlinx.datetime.Instant import pbandk.ByteArr @@ -82,7 +83,10 @@ class ProtoContentMapperImpl( is ProtoContent.Readable -> mapReadableContentToProtobuf(protoContent) } - val message = GenericMessage(protoContent.messageUid, messageContent) + val message = GenericMessage( + messageId = protoContent.messageUid, + content = messageContent + ) return PlainMessageBlob(message.encodeToByteArray()) } @@ -374,8 +378,17 @@ class ProtoContentMapperImpl( } null -> { - kaliumLogger.w("Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}") - MessageContent.Ignored + kaliumLogger.w( + "Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}" + + " Message Unknown Strategy = ${genericMessage.unknownStrategy}" + ) + when (genericMessage.unknownStrategy) { + UnknownStrategy.DISCARD_AND_WARN -> MessageContent.Unknown() + UnknownStrategy.WARN_USER_ALLOW_RETRY -> MessageContent.Unknown(encodedData = encodedContent.data) + UnknownStrategy.IGNORE, + is UnknownStrategy.UNRECOGNIZED, + null -> MessageContent.Ignored + } } } return readableContent diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt index abc2ede9218..892b4c26511 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/reaction/ReactionsMapper.kt @@ -53,7 +53,8 @@ internal class ReactionsMapperImpl( userTypeEntity = userType, deleted = deleted, connectionStatus = connectionStatus, - availabilityStatus = userAvailabilityStatus + availabilityStatus = userAvailabilityStatus, + accentId = accentId ) } @@ -71,7 +72,8 @@ internal class ReactionsMapperImpl( userType = domainUserTypeMapper.fromUserTypeEntity(userTypeEntity), isUserDeleted = deleted, connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus), - availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus) + availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus), + accentId = accentId ) ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt index be3fd5a56ef..6aacce40d91 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapper.kt @@ -65,7 +65,8 @@ internal class ReceiptsMapperImpl( userType = domainUserTypeMapper.fromUserTypeEntity(userType), isUserDeleted = isUserDeleted, connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus), - availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus) + availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus), + accentId = accentId ) ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt index 48709255f9b..a54389074d1 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/mlspublickeys/MLSPublicKeysRepository.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.data.mlspublickeys import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.MLSFailure import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap @@ -30,9 +31,14 @@ import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.network.api.base.authenticated.serverpublickey.MLSPublicKeyApi import io.ktor.util.decodeBase64Bytes -data class MLSPublicKeys( - val removal: Map? -) +fun MLSPublicKeys.getRemovalKey(cipherSuite: CipherSuite): Either { + val mlsPublicKeysMapper: MLSPublicKeysMapper = MapperProvider.mlsPublicKeyMapper() + val keySignature = mlsPublicKeysMapper.fromCipherSuite(cipherSuite) + val key = this.removal?.let { removalKeys -> + removalKeys[keySignature.value] + } ?: return Either.Left(MLSFailure.Generic(IllegalStateException("No key found for cipher suite $cipherSuite"))) + return key.decodeBase64Bytes().right() +} interface MLSPublicKeysRepository { suspend fun fetchKeys(): Either @@ -42,7 +48,6 @@ interface MLSPublicKeysRepository { class MLSPublicKeysRepositoryImpl( private val mlsPublicKeyApi: MLSPublicKeyApi, - private val mlsPublicKeysMapper: MLSPublicKeysMapper = MapperProvider.mlsPublicKeyMapper() ) : MLSPublicKeysRepository { // TODO: make it thread safe @@ -60,14 +65,8 @@ class MLSPublicKeysRepositoryImpl( } override suspend fun getKeyForCipherSuite(cipherSuite: CipherSuite): Either { - return getKeys().flatMap { serverPublicKeys -> - val keySignature = mlsPublicKeysMapper.fromCipherSuite(cipherSuite) - val key = serverPublicKeys.removal?.let { removalKeys -> - removalKeys[keySignature.value] - } ?: return Either.Left(MLSFailure.Generic(IllegalStateException("No key found for cipher suite $cipherSuite"))) - key.decodeBase64Bytes().right() + serverPublicKeys.getRemovalKey(cipherSuite) } } - } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt index 0f7700af816..ce30758bd3c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepository.kt @@ -20,9 +20,13 @@ package com.wire.kalium.logic.data.properties import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.conversation.folders.toFolder +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.map import com.wire.kalium.logic.wrapApiRequest import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi import com.wire.kalium.network.api.authenticated.properties.PropertyKey @@ -38,11 +42,13 @@ interface UserPropertyRepository { suspend fun observeTypingIndicatorStatus(): Flow> suspend fun setTypingIndicatorEnabled(): Either suspend fun removeTypingIndicatorProperty(): Either + suspend fun getConversationFolders(): Either> } internal class UserPropertyDataSource( private val propertiesApi: PropertiesApi, private val userConfigRepository: UserConfigRepository, + private val selfUserId: UserId ) : UserPropertyRepository { override suspend fun getReadReceiptsStatus(): Boolean = userConfigRepository.isReadReceiptsEnabled() @@ -82,4 +88,9 @@ internal class UserPropertyDataSource( }.flatMap { userConfigRepository.setTypingIndicatorStatus(false) } + + override suspend fun getConversationFolders(): Either> = wrapApiRequest { + propertiesApi.getLabels() + } + .map { it.labels.map { label -> label.toFolder(selfUserId.domain) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt index 8092e6f3e4b..ef9c8eee0de 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/session/SessionMapper.kt @@ -107,6 +107,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReason.REMOVED_CLIENT -> LogoutReasonEntity.REMOVED_CLIENT LogoutReason.DELETED_ACCOUNT -> LogoutReasonEntity.DELETED_ACCOUNT LogoutReason.SESSION_EXPIRED -> LogoutReasonEntity.SESSION_EXPIRED + LogoutReason.MIGRATION_TO_CC_FAILED -> LogoutReasonEntity.MIGRATION_TO_CC_FAILED } override fun toSsoIdEntity(ssoId: SsoId?): SsoIdEntity? = @@ -140,6 +141,7 @@ internal class SessionMapperImpl : SessionMapper { LogoutReasonEntity.REMOVED_CLIENT -> LogoutReason.REMOVED_CLIENT LogoutReasonEntity.DELETED_ACCOUNT -> LogoutReason.DELETED_ACCOUNT LogoutReasonEntity.SESSION_EXPIRED -> LogoutReason.SESSION_EXPIRED + LogoutReasonEntity.MIGRATION_TO_CC_FAILED -> LogoutReason.MIGRATION_TO_CC_FAILED } override fun fromEntityToProxyCredentialsDTO(proxyCredentialsEntity: ProxyCredentialsEntity): ProxyCredentialsDTO = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt index 2820a318f7b..a095eb3b5d3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncStatus.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.data.sync import com.wire.kalium.logic.CoreFailure +import kotlin.time.Duration sealed interface IncrementalSyncStatus { @@ -34,8 +35,8 @@ sealed interface IncrementalSyncStatus { override fun toString() = "LIVE" } - data class Failed(val failure: CoreFailure) : IncrementalSyncStatus { - override fun toString() = "FAILED, cause: '$failure'" + data class Failed(val failure: CoreFailure, val retryDelay: Duration) : IncrementalSyncStatus { + override fun toString() = "FAILED, cause: '$failure' retry in: $retryDelay" } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt index 77affb1fbc8..5bb50ed3cf4 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SlowSyncStatus.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.data.sync import com.wire.kalium.logic.CoreFailure +import kotlin.time.Duration sealed interface SlowSyncStatus { @@ -28,7 +29,7 @@ sealed interface SlowSyncStatus { data class Ongoing(val currentStep: SlowSyncStep) : SlowSyncStatus - data class Failed(val failure: CoreFailure) : SlowSyncStatus + data class Failed(val failure: CoreFailure, val retryDelay: Duration) : SlowSyncStatus } enum class SlowSyncStep { @@ -43,4 +44,5 @@ enum class SlowSyncStep { JOINING_MLS_CONVERSATIONS, RESOLVE_ONE_ON_ONE_PROTOCOLS, LEGAL_HOLD, + CONVERSATION_FOLDERS, } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt index 925abda6a5a..a3a5fd0d67e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/sync/SyncState.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.data.sync import com.wire.kalium.logic.CoreFailure +import kotlin.time.Duration sealed class SyncState { @@ -53,6 +54,7 @@ sealed class SyncState { /** * Sync was not completed due to a failure. + * [retryDelay] specifies the duration in which next try will happen */ - data class Failed(val cause: CoreFailure) : SyncState() + data class Failed(val cause: CoreFailure, val retryDelay: Duration) : SyncState() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt index 69a8fad273d..cd8398f0f5c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserMapper.kt @@ -198,6 +198,7 @@ internal class UserMapperImpl( name = userEntity.name, completePicture = userEntity.completeAssetId?.toModel(), userType = domainUserTypeMapper.fromUserTypeEntity(userEntity.userType), + accentId = userEntity.accentId ) override fun fromEntityToUserSummary(userEntity: UserEntity) = with(userEntity) { @@ -209,7 +210,8 @@ internal class UserMapperImpl( userType = domainUserTypeMapper.fromUserTypeEntity(userType), isUserDeleted = deleted, availabilityStatus = availabilityStatusMapper.fromDaoAvailabilityStatusToModel(availabilityStatus), - connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus) + connectionStatus = connectionStateMapper.fromDaoConnectionStateToUser(connectionStatus), + accentId = accentId ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt index 03944f0a9b9..e8760f83b79 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/user/UserRepository.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.MemberMapper import com.wire.kalium.logic.data.conversation.Recipient +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.IdMapper @@ -63,6 +64,7 @@ import com.wire.kalium.network.api.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.authenticated.userDetails.ListUsersDTO import com.wire.kalium.network.api.authenticated.userDetails.qualifiedIds import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi import com.wire.kalium.network.api.base.authenticated.self.SelfApi import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.model.LegalHoldStatusDTO @@ -162,6 +164,9 @@ interface UserRepository { suspend fun getOneOnOnConversationId(userId: QualifiedID): Either suspend fun getUsersMinimizedByQualifiedIDs(userIds: List): Either> + suspend fun getNameAndHandle(userId: UserId): Either + suspend fun migrateUserToTeam(teamName: String): Either + suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either } @Suppress("LongParameterList", "TooManyFunctions") @@ -171,6 +176,7 @@ internal class UserDataSource internal constructor( private val clientDAO: ClientDAO, private val selfApi: SelfApi, private val userDetailsApi: UserDetailsApi, + private val upgradePersonalToTeamApi: UpgradePersonalToTeamApi, private val teamsApi: TeamsApi, private val sessionRepository: SessionRepository, private val selfUserId: UserId, @@ -641,6 +647,27 @@ internal class UserDataSource internal constructor( userDAO.getOneOnOnConversationId(userId.toDao())?.toModel() } + override suspend fun getNameAndHandle(userId: UserId): Either = wrapStorageRequest { + userDAO.getNameAndHandle(userId.toDao()) + }.map { NameAndHandle.fromEntity(it) } + + override suspend fun migrateUserToTeam(teamName: String): Either { + return wrapApiRequest { upgradePersonalToTeamApi.migrateToTeam(teamName) }.map { dto -> + CreateUserTeam(dto.teamId, dto.teamName) + } + .onSuccess { + kaliumLogger.d("Migrated user to team") + fetchSelfUser() + } + .onFailure { failure -> + kaliumLogger.e("Failed to migrate user to team: $failure") + } + } + + override suspend fun updateTeamId(userId: UserId, teamId: TeamId): Either = wrapStorageRequest { + userDAO.updateTeamId(userId.toDao(), teamId.value) + } + companion object { internal const val SELF_USER_ID_KEY = "selfUserID" diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt index bd1b7939132..52e90b92103 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/di/MapperProvider.kt @@ -128,7 +128,8 @@ internal object MapperProvider { AvailabilityStatusMapperImpl(), DomainUserTypeMapperImpl(), ConnectionStatusMapperImpl(), - ConversationRoleMapperImpl() + ConversationRoleMapperImpl(), + MessageMapperImpl(selfUserId), ) fun conversationRoleMapper(): ConversationRoleMapper = ConversationRoleMapperImpl() diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt index 68a353dc754..e7e1a1637a8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/UserSessionScope.kt @@ -51,6 +51,7 @@ import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.client.MLSClientProviderImpl import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.client.ProteusClientProviderImpl +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler import com.wire.kalium.logic.data.client.remote.ClientRemoteDataSource import com.wire.kalium.logic.data.client.remote.ClientRemoteRepository import com.wire.kalium.logic.data.connection.ConnectionDataSource @@ -78,6 +79,8 @@ import com.wire.kalium.logic.data.conversation.ProposalTimer import com.wire.kalium.logic.data.conversation.SubconversationRepositoryImpl import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProviderImpl +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderDataSource +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepositoryDataSource import com.wire.kalium.logic.data.e2ei.E2EIRepository @@ -153,13 +156,14 @@ import com.wire.kalium.logic.di.PlatformUserStorageProperties import com.wire.kalium.logic.di.RootPathsProvider import com.wire.kalium.logic.di.UserStorageProvider import com.wire.kalium.logic.feature.analytics.AnalyticsIdentifierManager +import com.wire.kalium.logic.feature.analytics.GetCurrentAnalyticsTrackingIdentifierUseCase import com.wire.kalium.logic.feature.analytics.ObserveAnalyticsTrackingIdentifierStatusUseCase import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserver import com.wire.kalium.logic.feature.applock.AppLockTeamFeatureConfigObserverImpl import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCase import com.wire.kalium.logic.feature.applock.MarkTeamAppLockStatusAsNotifiedUseCaseImpl -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCase -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCaseImpl +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCaseImpl import com.wire.kalium.logic.feature.auth.AuthenticationScope import com.wire.kalium.logic.feature.auth.AuthenticationScopeProvider import com.wire.kalium.logic.feature.auth.ClearUserDataUseCase @@ -186,6 +190,7 @@ import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.IsAllowedToRegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.client.MLSClientManager import com.wire.kalium.logic.feature.client.MLSClientManagerImpl +import com.wire.kalium.logic.feature.client.ProteusMigrationRecoveryHandlerImpl import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCase import com.wire.kalium.logic.feature.client.RegisterMLSClientUseCaseImpl import com.wire.kalium.logic.feature.connection.ConnectionScope @@ -205,6 +210,8 @@ import com.wire.kalium.logic.feature.conversation.RecoverMLSConversationsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCaseImpl import com.wire.kalium.logic.feature.conversation.TypingIndicatorSyncManager +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCaseImpl import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManager import com.wire.kalium.logic.feature.conversation.keyingmaterials.KeyingMaterialsManagerImpl import com.wire.kalium.logic.feature.conversation.mls.MLSOneOnOneConversationResolver @@ -331,6 +338,8 @@ import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlag import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCaseImpl import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCase import com.wire.kalium.logic.feature.user.guestroomlink.ObserveGuestRoomLinkFeatureFlagUseCaseImpl +import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.user.migration.MigrateFromPersonalToTeamUseCaseImpl import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCase import com.wire.kalium.logic.feature.user.screenshotCensoring.ObserveScreenshotCensoringConfigUseCaseImpl import com.wire.kalium.logic.feature.user.screenshotCensoring.PersistScreenshotCensoringConfigUseCase @@ -380,6 +389,7 @@ import com.wire.kalium.logic.sync.receiver.UserPropertiesEventReceiver import com.wire.kalium.logic.sync.receiver.UserPropertiesEventReceiverImpl import com.wire.kalium.logic.sync.receiver.asset.AssetMessageHandler import com.wire.kalium.logic.sync.receiver.asset.AssetMessageHandlerImpl +import com.wire.kalium.logic.sync.receiver.conversation.AccessUpdateEventHandler import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandlerImpl import com.wire.kalium.logic.sync.receiver.conversation.DeletedConversationEventHandler @@ -444,6 +454,7 @@ import com.wire.kalium.network.NetworkStateObserver import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.SessionManager import com.wire.kalium.network.utils.MockUnboundNetworkClient +import com.wire.kalium.network.utils.MockWebSocketSession import com.wire.kalium.persistence.client.ClientRegistrationStorage import com.wire.kalium.persistence.client.ClientRegistrationStorageImpl import com.wire.kalium.persistence.db.GlobalDatabaseBuilder @@ -551,6 +562,10 @@ class UserSessionScope internal constructor( } } + private val invalidateTeamId = { + _teamId = Either.Left(CoreFailure.Unknown(Throwable("NotInitialized"))) + } + private val selfTeamId = SelfTeamIdProvider { teamId() } private val accessTokenRepository: AccessTokenRepository @@ -584,10 +599,10 @@ class UserSessionScope internal constructor( userAgent = userAgent, certificatePinning = kaliumConfigs.certPinningConfig, mockEngine = kaliumConfigs.mockedRequests?.let { MockUnboundNetworkClient.createMockEngine(it) }, + mockWebSocketSession = if (kaliumConfigs.mockedWebSocket) MockWebSocketSession() else null, kaliumLogger = userScopedLogger ) private val featureSupport: FeatureSupport = FeatureSupportImpl( - kaliumConfigs, sessionManager.serverConfig().metaData.commonApiVersion.version ) @@ -610,7 +625,8 @@ class UserSessionScope internal constructor( private val userPropertyRepository: UserPropertyRepository get() = UserPropertyDataSource( authenticatedNetworkContainer.propertiesApi, - userConfigRepository + userConfigRepository, + userId ) private val keyPackageLimitsProvider: KeyPackageLimitsProvider @@ -619,12 +635,17 @@ class UserSessionScope internal constructor( private val updateKeyingMaterialThresholdProvider: UpdateKeyingMaterialThresholdProvider get() = UpdateKeyingMaterialThresholdProviderImpl(kaliumConfigs) + private val proteusMigrationRecoveryHandler: ProteusMigrationRecoveryHandler by lazy { + ProteusMigrationRecoveryHandlerImpl(lazy { logout }) + } + val proteusClientProvider: ProteusClientProvider by lazy { ProteusClientProviderImpl( rootProteusPath = rootPathsProvider.rootProteusPath(userId), userId = userId, passphraseStorage = globalPreferences.passphraseStorage, - kaliumConfigs = kaliumConfigs + kaliumConfigs = kaliumConfigs, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler ) } @@ -692,7 +713,8 @@ class UserSessionScope internal constructor( private val notificationTokenRepository get() = NotificationTokenDataSource(globalPreferences.tokenStorage) - private val subconversationRepository = SubconversationRepositoryImpl() + private val subconversationRepository = + SubconversationRepositoryImpl(conversationApi = authenticatedNetworkContainer.conversationApi) private val conversationRepository: ConversationRepository get() = ConversationDataSource( @@ -703,10 +725,17 @@ class UserSessionScope internal constructor( userStorage.database.memberDAO, authenticatedNetworkContainer.conversationApi, userStorage.database.messageDAO, + userStorage.database.messageDraftDAO, userStorage.database.clientDAO, authenticatedNetworkContainer.clientApi, userStorage.database.conversationMetaDataDAO, - userStorage.database.messageDraftDAO + ) + + private val conversationFolderRepository: ConversationFolderRepository + get() = ConversationFolderDataSource( + userStorage.database.conversationFolderDAO, + authenticatedNetworkContainer.propertiesApi, + userId ) private val conversationGroupRepository: ConversationGroupRepository @@ -778,16 +807,17 @@ class UserSessionScope internal constructor( ) private val userRepository: UserRepository = UserDataSource( - userStorage.database.userDAO, - userStorage.database.metadataDAO, - userStorage.database.clientDAO, - authenticatedNetworkContainer.selfApi, - authenticatedNetworkContainer.userDetailsApi, - authenticatedNetworkContainer.teamsApi, - globalScope.sessionRepository, - userId, - selfTeamId, - legalHoldHandler + userDAO = userStorage.database.userDAO, + metadataDAO = userStorage.database.metadataDAO, + clientDAO = userStorage.database.clientDAO, + selfApi = authenticatedNetworkContainer.selfApi, + userDetailsApi = authenticatedNetworkContainer.userDetailsApi, + upgradePersonalToTeamApi = authenticatedNetworkContainer.upgradePersonalToTeamApi, + teamsApi = authenticatedNetworkContainer.teamsApi, + sessionRepository = globalScope.sessionRepository, + selfUserId = userId, + selfTeamIdProvider = selfTeamId, + legalHoldHandler = legalHoldHandler, ) private val accountRepository: AccountRepository @@ -913,22 +943,24 @@ class UserSessionScope internal constructor( kaliumFileSystem = kaliumFileSystem ) - private val eventGatherer: EventGatherer get() = EventGathererImpl( - eventRepository, - incrementalSyncRepository, - userScopedLogger, - ) + private val eventGatherer: EventGatherer + get() = EventGathererImpl( + eventRepository = eventRepository, + incrementalSyncRepository = incrementalSyncRepository, + logger = userScopedLogger + ) private val eventProcessor: EventProcessor by lazy { EventProcessorImpl( - eventRepository, - conversationEventReceiver, - userEventReceiver, - teamEventReceiver, - featureConfigEventReceiver, - userPropertiesEventReceiver, - federationEventReceiver, - userScopedLogger, + eventRepository = eventRepository, + conversationEventReceiver = conversationEventReceiver, + userEventReceiver = userEventReceiver, + teamEventReceiver = teamEventReceiver, + featureConfigEventReceiver = featureConfigEventReceiver, + userPropertiesEventReceiver = userPropertiesEventReceiver, + federationEventReceiver = federationEventReceiver, + processingScope = this@UserSessionScope, + logger = userScopedLogger, ) } @@ -949,6 +981,9 @@ class UserSessionScope internal constructor( systemMessageInserter ) + private val syncConversationFolders: SyncConversationFoldersUseCase + get() = SyncConversationFoldersUseCaseImpl(conversationFolderRepository) + private val syncConnections: SyncConnectionsUseCase get() = SyncConnectionsUseCaseImpl( connectionRepository = connectionRepository @@ -1028,7 +1063,8 @@ class UserSessionScope internal constructor( conversationGroupRepository, conversationRepository, messageRepository, - userRepository + userRepository, + systemMessageInserter ) private val oneOnOneResolver: OneOnOneResolver get() = OneOnOneResolverImpl( @@ -1066,7 +1102,8 @@ class UserSessionScope internal constructor( syncContacts, joinExistingMLSConversations, fetchLegalHoldForSelfUserFromRemoteUseCase, - oneOnOneResolver + oneOnOneResolver, + syncConversationFolders ) } @@ -1219,7 +1256,6 @@ class UserSessionScope internal constructor( private val pendingProposalScheduler: PendingProposalScheduler = PendingProposalSchedulerImpl( - kaliumConfigs, incrementalSyncRepository, lazy { mlsConversationRepository }, lazy { subconversationRepository } @@ -1232,6 +1268,7 @@ class UserSessionScope internal constructor( userRepository = userRepository, currentClientIdProvider = clientIdProvider, conversationRepository = conversationRepository, + userConfigRepository = userConfigRepository, selfConversationIdProvider = selfConversationIdProvider, messageSender = messages.messageSender, federatedIdMapper = federatedIdMapper, @@ -1461,6 +1498,12 @@ class UserSessionScope internal constructor( callRepository = callRepository ) + private val conversationAccessUpdateEventHandler: AccessUpdateEventHandler + get() = AccessUpdateEventHandler( + conversationDAO = userStorage.database.conversationDAO, + selfUserId = userId + ) + private val conversationEventReceiver: ConversationEventReceiver by lazy { ConversationEventReceiverImpl( newMessageHandler, @@ -1476,7 +1519,8 @@ class UserSessionScope internal constructor( conversationCodeUpdateHandler, conversationCodeDeletedHandler, typingIndicatorHandler, - protocolUpdateEventHandler + protocolUpdateEventHandler, + conversationAccessUpdateEventHandler ) } override val coroutineContext: CoroutineContext = SupervisorJob() @@ -1492,6 +1536,9 @@ class UserSessionScope internal constructor( val observeAnalyticsTrackingIdentifierStatus: ObserveAnalyticsTrackingIdentifierStatusUseCase get() = ObserveAnalyticsTrackingIdentifierStatusUseCase(userConfigRepository, userScopedLogger) + val getCurrentAnalyticsTrackingIdentifier: GetCurrentAnalyticsTrackingIdentifierUseCase + get() = GetCurrentAnalyticsTrackingIdentifierUseCase(userConfigRepository) + val analyticsIdentifierManager: AnalyticsIdentifierManager get() = AnalyticsIdentifierManager( messages.messageSender, @@ -1570,7 +1617,7 @@ class UserSessionScope internal constructor( ) private val userPropertiesEventReceiver: UserPropertiesEventReceiver - get() = UserPropertiesEventReceiverImpl(userConfigRepository) + get() = UserPropertiesEventReceiverImpl(userConfigRepository, conversationFolderRepository) private val federationEventReceiver: FederationEventReceiver get() = FederationEventReceiverImpl( @@ -1738,6 +1785,7 @@ class UserSessionScope internal constructor( conversationGroupRepository, connectionRepository, userRepository, + conversationFolderRepository, syncManager, mlsConversationRepository, clientIdProvider, @@ -1778,6 +1826,7 @@ class UserSessionScope internal constructor( userRepository, userId, assetRepository, + eventRepository, syncManager, slowSyncRepository, messageSendingScheduler, @@ -1785,6 +1834,7 @@ class UserSessionScope internal constructor( staleEpochVerifier, eventProcessor, legalHoldHandler, + notificationTokenRepository, this, userScopedLogger, ) @@ -1842,6 +1892,7 @@ class UserSessionScope internal constructor( clientIdProvider, e2eiRepository, mlsConversationRepository, + conversationRepository, team.isSelfATeamMember, updateSupportedProtocols, clientRepository, @@ -1850,6 +1901,8 @@ class UserSessionScope internal constructor( isE2EIEnabled, certificateRevocationListRepository, incrementalSyncRepository, + sessionManager, + selfTeamId, checkRevocationList, syncFeatureConfigsUseCase, userScopedLogger @@ -1858,6 +1911,9 @@ class UserSessionScope internal constructor( val search: SearchScope by lazy { SearchScope( + mlsPublicKeysRepository = mlsPublicKeysRepository, + getDefaultProtocol = getDefaultProtocol, + getConversationProtocolInfo = conversations.getConversationProtocolInfo, searchUserRepository = searchUserRepository, selfUserId = userId, sessionRepository = globalScope.sessionRepository, @@ -1867,7 +1923,7 @@ class UserSessionScope internal constructor( private val clearUserData: ClearUserDataUseCase get() = ClearUserDataUseCaseImpl(userStorage) - private val validateAssetMimeType: ValidateAssetMimeTypeUseCase get() = ValidateAssetMimeTypeUseCaseImpl() + private val validateAssetMimeType: ValidateAssetFileTypeUseCase get() = ValidateAssetFileTypeUseCaseImpl() val logout: LogoutUseCase get() = LogoutUseCaseImpl( @@ -1959,7 +2015,11 @@ class UserSessionScope internal constructor( @OptIn(DelicateKaliumApi::class) private val isAllowedToRegisterMLSClient: IsAllowedToRegisterMLSClientUseCase - get() = IsAllowedToRegisterMLSClientUseCaseImpl(featureSupport, mlsPublicKeysRepository) + get() = IsAllowedToRegisterMLSClientUseCaseImpl( + featureSupport, + mlsPublicKeysRepository, + userConfigRepository + ) private val syncFeatureConfigsUseCase: SyncFeatureConfigsUseCase get() = SyncFeatureConfigsUseCaseImpl( @@ -1976,12 +2036,13 @@ class UserSessionScope internal constructor( appLockConfigHandler ) - val team: TeamScope get() = TeamScope( - teamRepository = teamRepository, - conversationRepository = conversationRepository, - slowSyncRepository = slowSyncRepository, - selfTeamIdProvider = selfTeamId - ) + val team: TeamScope + get() = TeamScope( + teamRepository = teamRepository, + conversationRepository = conversationRepository, + slowSyncRepository = slowSyncRepository, + selfTeamIdProvider = selfTeamId + ) val service: ServiceScope get() = ServiceScope( @@ -2050,6 +2111,9 @@ class UserSessionScope internal constructor( userScopedLogger ) + val migrateFromPersonalToTeam: MigrateFromPersonalToTeamUseCase + get() = MigrateFromPersonalToTeamUseCaseImpl(userId, userRepository, invalidateTeamId) + internal val getProxyCredentials: GetProxyCredentialsUseCase get() = GetProxyCredentialsUseCaseImpl(sessionManager) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt new file mode 100644 index 00000000000..03896e59a41 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCase.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.analytics + +import com.wire.kalium.logic.configuration.UserConfigRepository + +/** + * Use case that returns the current analytics tracking identifier + */ +interface GetCurrentAnalyticsTrackingIdentifierUseCase { + /** + * Use case [GetCurrentAnalyticsTrackingIdentifierUseCase] operation + * + * @return a [String] containing the current tracking identifier + */ + suspend operator fun invoke(): String? +} + +@Suppress("FunctionNaming") +internal fun GetCurrentAnalyticsTrackingIdentifierUseCase( + userConfigRepository: UserConfigRepository +) = object : GetCurrentAnalyticsTrackingIdentifierUseCase { + + override suspend fun invoke(): String? = userConfigRepository.getCurrentTrackingIdentifier() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt index c6b2a1ff0df..34655254acb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCase.kt @@ -111,7 +111,7 @@ internal class ScheduleNewAssetMessageUseCaseImpl( private val selfDeleteTimer: ObserveSelfDeletionTimerSettingsForConversationUseCase, private val scope: CoroutineScope, private val observeFileSharingStatus: ObserveFileSharingStatusUseCase, - private val validateAssetMimeTypeUseCase: ValidateAssetMimeTypeUseCase, + private val validateAssetFileUseCase: ValidateAssetFileTypeUseCase, private val dispatcher: KaliumDispatcher, ) : ScheduleNewAssetMessageUseCase { @@ -133,7 +133,12 @@ internal class ScheduleNewAssetMessageUseCaseImpl( FileSharingStatus.Value.Disabled -> return ScheduleNewAssetMessageResult.Failure.DisabledByTeam FileSharingStatus.Value.EnabledAll -> { /* no-op*/ } - is FileSharingStatus.Value.EnabledSome -> if (!validateAssetMimeTypeUseCase(assetMimeType, it.state.allowedType)) { + is FileSharingStatus.Value.EnabledSome -> if (!validateAssetFileUseCase( + fileName = assetName, + mimeType = assetMimeType, + allowedExtension = it.state.allowedType + ) + ) { kaliumLogger.e("The asset message trying to be processed has invalid content data") return ScheduleNewAssetMessageResult.Failure.RestrictedFileType } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt new file mode 100644 index 00000000000..b4f887e1bb4 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCase.kt @@ -0,0 +1,111 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.asset + +import com.wire.kalium.logic.kaliumLogger + +/** + * Returns true if the file extension is present in file name and is allowed and false otherwise. + * @param fileName the file name (with extension) to validate. + * @param allowedExtension the list of allowed extension. + */ +interface ValidateAssetFileTypeUseCase { + operator fun invoke( + fileName: String?, + mimeType: String, + allowedExtension: List + ): Boolean +} + +internal class ValidateAssetFileTypeUseCaseImpl : ValidateAssetFileTypeUseCase { + override operator fun invoke( + fileName: String?, + mimeType: String, + allowedExtension: List + ): Boolean { + kaliumLogger.d("Validating file type for $fileName with mimeType $mimeType is empty ${mimeType.isBlank()}") + val extension = if (fileName != null) { + extensionFromFileName(fileName) + } else { + extensionFromMimeType(mimeType) + } + return extension?.let { allowedExtension.contains(it) } ?: false + } + + private fun extensionFromFileName(fileName: String): String? = + fileName.substringAfterLast('.', "").takeIf { it.isNotEmpty() } + + private fun extensionFromMimeType(mimeType: String): String? = fileExtensions[mimeType] + + private companion object { + val fileExtensions = mapOf( + "video/3gpp" to "3gpp", + "audio/aac" to "aac", + "audio/amr" to "amr", + "video/x-msvideo" to "avi", + "image/bmp" to "bmp", + "text/css" to "css", + "text/csv" to "csv", + "application/msword" to "doc", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" to "docx", + "message/rfc822" to "eml", + "audio/flac" to "flac", + "image/gif" to "gif", + "text/html" to "html", + "image/vnd.microsoft.icon" to "ico", + "image/jpeg" to "jpeg", + "image/jpeg" to "jpg", + "image/jpeg" to "jfif", + "application/vnd.apple.keynote" to "key", + "audio/mp4" to "m4a", + "video/x-m4v" to "m4v", + "text/markdown" to "md", + "audio/midi" to "midi", + "video/x-matroska" to "mkv", + "video/quicktime" to "mov", + "audio/mpeg" to "mp3", + "video/mp4" to "mp4", + "video/mpeg" to "mpeg", + "application/vnd.ms-outlook" to "msg", + "application/vnd.oasis.opendocument.spreadsheet" to "ods", + "application/vnd.oasis.opendocument.text" to "odt", + "audio/ogg" to "ogg", + "application/pdf" to "pdf", + "image/jpeg" to "pjp", + "image/pjpeg" to "pjpeg", + "image/png" to "png", + "application/vnd.ms-powerpoint" to "ppt", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" to "pptx", + "image/vnd.adobe.photoshop" to "psd", + "application/rtf" to "rtf", + "application/sql" to "sql", + "image/svg+xml" to "svg", + "application/x-tex" to "tex", + "image/tiff" to "tiff", + "text/plain" to "txt", + "text/x-vcard" to "vcf", + "audio/wav" to "wav", + "video/webm" to "webm", + "image/webp" to "webp", + "video/x-ms-wmv" to "wmv", + "application/vnd.ms-excel" to "xls", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" to "xlsx", + "application/xml" to "xml" + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt index d3be95db8b5..4609d23c225 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCase.kt @@ -31,6 +31,7 @@ import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.client.ClearClientDataUseCase import com.wire.kalium.logic.feature.session.DeregisterTokenUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.UserSessionWorkScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel @@ -106,6 +107,9 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( } LogoutReason.SELF_SOFT_LOGOUT -> clearCurrentClientIdAndFirebaseTokenFlag() + LogoutReason.MIGRATION_TO_CC_FAILED -> prepareForCoreCryptoMigrationRecovery() + }.also { + kaliumLogger.withTextTag(TAG).d("Logout reason: $reason") } userConfigRepository.clearE2EISettings() @@ -115,6 +119,13 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( }.let { if (waitUntilCompletes) it.join() else it } } + private suspend fun prepareForCoreCryptoMigrationRecovery() { + clearClientDataUseCase() + logoutRepository.clearClientRelatedLocalMetadata() + clientRepository.clearRetainedClientId() + pushTokenRepository.setUpdateFirebaseTokenFlag(true) + } + private suspend fun clearCurrentClientIdAndFirebaseTokenFlag() { clientRepository.clearCurrentClientId() clientRepository.clearNewClients() @@ -146,5 +157,6 @@ internal class LogoutUseCaseImpl @Suppress("LongParameterList") constructor( companion object { const val CLEAR_DATA_DELAY = 1000L + const val TAG = "LogoutUseCase" } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt index ab95181d207..cb2c5c105ac 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreBackupUseCase.kt @@ -95,7 +95,13 @@ internal class RestoreBackupUseCaseImpl( importEncryptedBackup(extractedBackupRootPath, password) } } - .fold({ it }, { RestoreBackupResult.Success }) + .fold({ error -> + kaliumLogger.e("$TAG Failed to restore the backup, reason: ${error.failure}") + error + }, { + kaliumLogger.i("$TAG Backup restored successfully") + RestoreBackupResult.Success + }) } private suspend fun importUnencryptedBackup( @@ -140,7 +146,7 @@ internal class RestoreBackupUseCaseImpl( val extractedFilesRootPath = createExtractedFilesRootPath() return extractFiles(tempCompressedFileSource, extractedFilesRootPath) .fold({ - kaliumLogger.e("Failed to extract backup files") + kaliumLogger.e("$TAG Failed to extract backup files") Either.Left(Failure(BackupIOFailure("Failed to extract backup files"))) }, { Either.Right(extractedFilesRootPath) @@ -176,7 +182,7 @@ internal class RestoreBackupUseCaseImpl( return if (backupSize > 0) { // On successful decryption, we still need to extract the zip file to do sanity checks and get the database file extractFiles(kaliumFileSystem.source(extractedBackupPath), extractedBackupRootPath).fold({ - kaliumLogger.e("Failed to extract encrypted backup files") + kaliumLogger.e("$TAG Failed to extract encrypted backup files") Either.Left(Failure(BackupIOFailure("Failed to extract encrypted backup files"))) }, { kaliumFileSystem.delete(extractedBackupPath) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt index a5732a7a2be..be85aeaa09e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/backup/RestoreWebBackupUseCase.kt @@ -79,7 +79,13 @@ internal class RestoreWebBackupUseCaseImpl( importWebBackup(backupRootPath, this) } else { Either.Left(IncompatibleBackup("invoke: The provided backup format is not supported")) - }.fold({ RestoreBackupResult.Failure(it) }, { RestoreBackupResult.Success }) + }.fold({ error -> + kaliumLogger.e("$TAG Failed to restore the backup, reason: $error") + RestoreBackupResult.Failure(error) + }, { + kaliumLogger.i("$TAG Successfully restored the backup") + RestoreBackupResult.Success + }) } private suspend fun importWebBackup( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt index 05fc2c1d16d..59aead0b38f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/CallsScope.kt @@ -43,6 +43,8 @@ import com.wire.kalium.logic.feature.call.usecase.GetAllCallsWithSortedParticipa import com.wire.kalium.logic.feature.call.usecase.GetCallConversationTypeProvider import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCase import com.wire.kalium.logic.feature.call.usecase.GetIncomingCallsUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveConferenceCallingEnabledUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.IsCallRunningUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCase import com.wire.kalium.logic.feature.call.usecase.IsEligibleToStartCallUseCaseImpl @@ -50,8 +52,11 @@ import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCase import com.wire.kalium.logic.feature.call.usecase.IsLastCallClosedUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCase import com.wire.kalium.logic.feature.call.usecase.MuteCallUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveAskCallFeedbackUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEndCallDueToConversationDegradationUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEndCallDueToConversationDegradationUseCaseImpl +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCase +import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallWithSortedParticipantsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCase import com.wire.kalium.logic.feature.call.usecase.ObserveEstablishedCallsUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.ObserveOngoingCallsUseCase @@ -74,6 +79,8 @@ import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCu import com.wire.kalium.logic.feature.call.usecase.UpdateConversationClientsForCurrentCallUseCaseImpl import com.wire.kalium.logic.feature.call.usecase.video.SetVideoSendStateUseCase import com.wire.kalium.logic.feature.call.usecase.video.UpdateVideoStateUseCase +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCase +import com.wire.kalium.logic.feature.user.UpdateNextTimeCallFeedbackUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.util.KaliumDispatcher @@ -126,6 +133,12 @@ class CallsScope internal constructor( callRepository = callRepository, ) + val observeEstablishedCallWithSortedParticipants: ObserveEstablishedCallWithSortedParticipantsUseCase + get() = ObserveEstablishedCallWithSortedParticipantsUseCaseImpl( + callRepository = callRepository, + callingParticipantsOrder = callingParticipantsOrder + ) + val startCall: StartCallUseCase get() = StartCallUseCase( callManager = callManager, @@ -145,7 +158,13 @@ class CallsScope internal constructor( kaliumConfigs ) - val endCall: EndCallUseCase get() = EndCallUseCaseImpl(callManager, callRepository, KaliumDispatcherImpl) + val endCall: EndCallUseCase + get() = EndCallUseCaseImpl( + callManager = callManager, + callRepository = callRepository, + endCallListener = EndCallResultListenerImpl, + shouldAskCallFeedback = shouldAskCallFeedback + ) val endCallOnConversationChange: EndCallOnConversationChangeUseCase get() = EndCallOnConversationChangeUseCaseImpl( @@ -198,6 +217,19 @@ class CallsScope internal constructor( val isEligibleToStartCall: IsEligibleToStartCallUseCase get() = IsEligibleToStartCallUseCaseImpl(userConfigRepository, callRepository) - val observeEndCallDialog: ObserveEndCallDueToConversationDegradationUseCase + val observeConferenceCallingEnabled: ObserveConferenceCallingEnabledUseCase + get() = ObserveConferenceCallingEnabledUseCaseImpl(userConfigRepository) + + val observeEndCallDueToDegradationDialog: ObserveEndCallDueToConversationDegradationUseCase get() = ObserveEndCallDueToConversationDegradationUseCaseImpl(EndCallResultListenerImpl) + val observeAskCallFeedbackUseCase: ObserveAskCallFeedbackUseCase + get() = ObserveAskCallFeedbackUseCase(EndCallResultListenerImpl) + + private val shouldAskCallFeedback: ShouldAskCallFeedbackUseCase by lazy { + ShouldAskCallFeedbackUseCase(userConfigRepository) + } + + val updateNextTimeCallFeedback: UpdateNextTimeCallFeedbackUseCase by lazy { + UpdateNextTimeCallFeedbackUseCase(userConfigRepository) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt index e4aa653be79..eb7bca57c8b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/GlobalCallManager.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.feature.call import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.call.mapper.CallMapper import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.VideoStateChecker @@ -47,6 +48,7 @@ expect class GlobalCallManager { currentClientIdProvider: CurrentClientIdProvider, selfConversationIdProvider: SelfConversationIdProvider, conversationRepository: ConversationRepository, + userConfigRepository: UserConfigRepository, messageSender: MessageSender, callMapper: CallMapper, federatedIdMapper: FederatedIdMapper, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCase.kt index 909af1c8c93..70bf1cdf9d1 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCase.kt @@ -105,7 +105,7 @@ internal class EndCallOnConversationChangeUseCaseImpl( } .filter { it.shouldFinishCall() } .map { - endCallListener.onCallEndedBecauseOfVerificationDegraded(conversationId) + endCallListener.onCallEndedBecauseOfVerificationDegraded() conversationId } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt index 9a529940580..a376b3fc4bd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallResultListener.kt @@ -17,13 +17,13 @@ */ package com.wire.kalium.logic.feature.call.usecase -import com.wire.kalium.logic.data.id.ConversationId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow interface EndCallResultListener { - suspend fun observeCallEndedBecauseOfVerificationDegraded(): Flow - suspend fun onCallEndedBecauseOfVerificationDegraded(conversationId: ConversationId) + suspend fun observeCallEndedResult(): Flow + suspend fun onCallEndedBecauseOfVerificationDegraded() + suspend fun onCallEndedAskForFeedback(shouldAsk: Boolean) } /** @@ -31,11 +31,20 @@ interface EndCallResultListener { */ object EndCallResultListenerImpl : EndCallResultListener { - private val conversationCallEnded = MutableSharedFlow() + private val conversationCallEnded = MutableSharedFlow() - override suspend fun observeCallEndedBecauseOfVerificationDegraded(): Flow = conversationCallEnded + override suspend fun observeCallEndedResult(): Flow = conversationCallEnded - override suspend fun onCallEndedBecauseOfVerificationDegraded(conversationId: ConversationId) { - conversationCallEnded.emit(conversationId) + override suspend fun onCallEndedBecauseOfVerificationDegraded() { + conversationCallEnded.emit(EndCallResult.VerificationDegraded) } + + override suspend fun onCallEndedAskForFeedback(shouldAsk: Boolean) { + conversationCallEnded.emit(EndCallResult.AskForFeedback(shouldAsk)) + } +} + +sealed class EndCallResult { + data object VerificationDegraded : EndCallResult() + data class AskForFeedback(val shouldAsk: Boolean) : EndCallResult() } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt index e8e7541a317..f9a5a674918 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCase.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.CallManager import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCase import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.flow.first @@ -46,6 +47,8 @@ interface EndCallUseCase { internal class EndCallUseCaseImpl( private val callManager: Lazy, private val callRepository: CallRepository, + private val endCallListener: EndCallResultListener, + private val shouldAskCallFeedback: ShouldAskCallFeedbackUseCase, private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl ) : EndCallUseCase { @@ -73,5 +76,6 @@ internal class EndCallUseCaseImpl( callManager.value.endCall(conversationId) callRepository.updateIsCameraOnById(conversationId, false) + endCallListener.onCallEndedAskForFeedback(shouldAskCallFeedback()) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt index e3984958de5..0aea1f39c30 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCase.kt @@ -82,9 +82,7 @@ internal class GetIncomingCallsUseCaseImpl internal constructor( val callIds = calls.map { call -> call.conversationId }.joinToString() logger.d("$TAG; Found calls: $callIds") } - .distinctUntilChanged { old, new -> - old.firstOrNull()?.conversationId == new.firstOrNull()?.conversationId - } + .distinctUntilChanged() } private fun Flow>.onlyCallsInNotMutedConversations(): Flow> = diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt new file mode 100644 index 00000000000..5c3e6915043 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveAskCallFeedbackUseCase.kt @@ -0,0 +1,42 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.usecase + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map + +/** + * The useCase for observing when the ongoing call was ended because of degradation of conversation verification status (Proteus or MLS) + */ +interface ObserveAskCallFeedbackUseCase { + /** + * @return [Flow] that emits only when the call was ended because of degradation of conversation verification status (Proteus or MLS) + */ + suspend operator fun invoke(): Flow +} + +@Suppress("FunctionNaming") +internal fun ObserveAskCallFeedbackUseCase( + endCallListener: EndCallResultListener +) = object : ObserveAskCallFeedbackUseCase { + override suspend fun invoke(): Flow = + endCallListener.observeCallEndedResult() + .filterIsInstance(EndCallResult.AskForFeedback::class) + .map { it.shouldAsk } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCase.kt new file mode 100644 index 00000000000..705ecd97920 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCase.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.usecase + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.event.Event +import com.wire.kalium.logic.functional.fold +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.zip + +/** + * Returns [Unit] only when our conference calling setting changes from false to true, meaning our conference calling + * capability has been enabled. Internally we rely on getting event [Event.FeatureConfig.ConferenceCallingUpdated]. + * This can be used to inform user about the change, for example displaying a dialog about upgrading to enterprise edition. + */ +interface ObserveConferenceCallingEnabledUseCase { + suspend operator fun invoke(): Flow +} + +internal class ObserveConferenceCallingEnabledUseCaseImpl( + private val userConfigRepository: UserConfigRepository, +) : ObserveConferenceCallingEnabledUseCase { + override suspend fun invoke(): Flow { + val enabledFlow = userConfigRepository.observeConferenceCallingEnabled() + .map { isEnabled -> isEnabled.fold({ false }, { it }) } + return enabledFlow + .zip(enabledFlow.drop(1)) { old, new -> old to new } + .filter { (old, new) -> !old && new } + .map { Unit } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEndCallDueToConversationDegradationUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEndCallDueToConversationDegradationUseCase.kt index 830fbc562e2..80f83311945 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEndCallDueToConversationDegradationUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEndCallDueToConversationDegradationUseCase.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.call.usecase import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map /** @@ -34,5 +35,7 @@ internal class ObserveEndCallDueToConversationDegradationUseCaseImpl( private val endCallListener: EndCallResultListener ) : ObserveEndCallDueToConversationDegradationUseCase { override suspend fun invoke(): Flow = - endCallListener.observeCallEndedBecauseOfVerificationDegraded().map { Unit } + endCallListener.observeCallEndedResult() + .filterIsInstance(EndCallResult.VerificationDegraded::class) + .map { Unit } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEstablishedCallWithSortedParticipantsUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEstablishedCallWithSortedParticipantsUseCase.kt new file mode 100644 index 00000000000..f92ee5393f7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveEstablishedCallWithSortedParticipantsUseCase.kt @@ -0,0 +1,51 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.call.usecase + +import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallingParticipantsOrder +import com.wire.kalium.logic.data.id.ConversationId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +/** + * Use case to observe established call with the participants sorted according to the [CallingParticipantsOrder] + */ +interface ObserveEstablishedCallWithSortedParticipantsUseCase { + suspend operator fun invoke(conversationId: ConversationId): Flow +} + +class ObserveEstablishedCallWithSortedParticipantsUseCaseImpl internal constructor( + private val callRepository: CallRepository, + private val callingParticipantsOrder: CallingParticipantsOrder +) : ObserveEstablishedCallWithSortedParticipantsUseCase { + + override suspend operator fun invoke(conversationId: ConversationId): Flow { + return callRepository.observeCurrentCall(conversationId) + .distinctUntilChanged() + .map { call -> + call?.let { + val sortedParticipants = callingParticipantsOrder.reorderItems(call.participants) + call.copy(participants = sortedParticipants) + } + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCase.kt index 0b544f029f3..bd53f5a1e33 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/GetOrRegisterClientUseCase.kt @@ -55,7 +55,7 @@ internal class GetOrRegisterClientUseCaseImpl( ) : GetOrRegisterClientUseCase { override suspend fun invoke(registerClientParam: RegisterClientUseCase.RegisterClientParam): RegisterClientResult { - syncFeatureConfigsUseCase.invoke() + syncFeatureConfigsUseCase() val result: RegisterClientResult = clientRepository.retainedClientId() .nullableFold( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt index 838619af9cb..3feb0cd3b8e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCase.kt @@ -18,8 +18,10 @@ package com.wire.kalium.logic.feature.client +import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.featureFlags.FeatureSupport +import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.isRight import com.wire.kalium.util.DelicateKaliumApi @@ -39,8 +41,12 @@ interface IsAllowedToRegisterMLSClientUseCase { internal class IsAllowedToRegisterMLSClientUseCaseImpl( private val featureSupport: FeatureSupport, private val mlsPublicKeysRepository: MLSPublicKeysRepository, + private val userConfigRepository: UserConfigRepository ) : IsAllowedToRegisterMLSClientUseCase { - override suspend operator fun invoke(): Boolean = - featureSupport.isMLSSupported && mlsPublicKeysRepository.getKeys().isRight() + override suspend operator fun invoke(): Boolean { + return featureSupport.isMLSSupported && + mlsPublicKeysRepository.getKeys().isRight() && + userConfigRepository.isMLSEnabled().fold({ false }, { isEnabled -> isEnabled }) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt new file mode 100644 index 00000000000..c2b7e42bfbb --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerImpl.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.data.client.ProteusMigrationRecoveryHandler +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import com.wire.kalium.logic.kaliumLogger + +internal class ProteusMigrationRecoveryHandlerImpl( + private val logoutUseCase: Lazy +) : ProteusMigrationRecoveryHandler { + + /** + * Handles the migration error of a proteus client storage from CryptoBox to CoreCrypto. + * It will perform a logout, using [LogoutReason.MIGRATION_TO_CC_FAILED] as the reason. + * + * This achieves that the client data is cleared and the user is logged out without losing content. + */ + @Suppress("TooGenericExceptionCaught") + override suspend fun clearClientData(clearLocalFiles: suspend () -> Unit) { + try { + kaliumLogger.withTextTag(TAG).i("Starting the recovery from failed Proteus storage migration") + clearLocalFiles() + logoutUseCase.value(LogoutReason.MIGRATION_TO_CC_FAILED, true) + } catch (e: Exception) { + kaliumLogger.withTextTag(TAG).e("Fatal, error while clearing client data: $e") + throw e + } finally { + kaliumLogger.withTextTag(TAG).i("Finished the recovery from failed Proteus storage migration") + } + } + + private companion object { + const val TAG = "ProteusMigrationRecoveryHandler" + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt index c2d602d9c12..c92236cf238 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/RegisterClientUseCase.kt @@ -36,6 +36,7 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.network.exceptions.AuthenticationCodeFailure import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.exceptions.authenticationCodeFailure @@ -144,10 +145,11 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr model, cookieLabel, verificationCode, - modelPostfix + modelPostfix, ) - }.fold({ - RegisterClientResult.Failure.Generic(it) + }.fold({ error -> + kaliumLogger.withTextTag(TAG).e("There was an error while registering the client $error") + RegisterClientResult.Failure.Generic(error) }, { registerClientParam -> clientRepository.registerClient(registerClientParam) // todo? separate this in mls client usesCase register! separate everything @@ -241,4 +243,8 @@ class RegisterClientUseCaseImpl @OptIn(DelicateKaliumApi::class) internal constr } } } + + private companion object { + const val TAG = "RegisterClientUseCase" + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCase.kt index 4850adf0fca..6c393915427 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCase.kt @@ -24,8 +24,6 @@ import com.wire.kalium.logic.data.client.ClientRepository import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.functional.fold -import com.wire.kalium.logic.functional.getOrElse -import com.wire.kalium.logic.functional.map import com.wire.kalium.util.DelicateKaliumApi /** @@ -52,36 +50,22 @@ internal class VerifyExistingClientUseCaseImpl @OptIn(DelicateKaliumApi::class) return clientRepository.selfListOfClients() .fold({ VerifyExistingClientResult.Failure.Generic(it) - }, { listOfClients -> - val client = listOfClients.firstOrNull { it.id == clientId } + }, { listOfRegisteredClients -> + val registeredClient = listOfRegisteredClients.firstOrNull { it.id == clientId } when { - (client == null) -> VerifyExistingClientResult.Failure.ClientNotRegistered + registeredClient == null -> VerifyExistingClientResult.Failure.ClientNotRegistered - isAllowedToRegisterMLSClient() -> { - registerMLSClientUseCase.invoke(clientId = client.id).map { - if (it is RegisterMLSClientResult.E2EICertificateRequired) - VerifyExistingClientResult.Failure.E2EICertificateRequired(client, selfUserId) - else VerifyExistingClientResult.Success(client) - }.getOrElse { VerifyExistingClientResult.Failure.Generic(it) } - } - - else -> VerifyExistingClientResult.Success(client) - } - - if (client != null) { - if (isAllowedToRegisterMLSClient()) { - registerMLSClientUseCase.invoke(clientId = client.id).fold({ + !registeredClient.isMLSCapable && isAllowedToRegisterMLSClient() -> { + registerMLSClientUseCase.invoke(clientId = registeredClient.id).fold({ VerifyExistingClientResult.Failure.Generic(it) - }) { + }, { if (it is RegisterMLSClientResult.E2EICertificateRequired) - VerifyExistingClientResult.Failure.E2EICertificateRequired(client, selfUserId) - else VerifyExistingClientResult.Success(client) - } - } else { - VerifyExistingClientResult.Success(client) + VerifyExistingClientResult.Failure.E2EICertificateRequired(registeredClient, selfUserId) + else VerifyExistingClientResult.Success(registeredClient) + }) } - } else { - VerifyExistingClientResult.Failure.ClientNotRegistered + + else -> VerifyExistingClientResult.Success(registeredClient) } }) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt index 4bfdc6b76a8..0f3be756ff3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ConversationScope.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.data.conversation.TypingIndicatorOutgoingRepository import com.wire.kalium.logic.data.conversation.TypingIndicatorSenderHandler import com.wire.kalium.logic.data.conversation.TypingIndicatorSenderHandlerImpl import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.QualifiedIdMapper import com.wire.kalium.logic.data.id.SelfTeamIdProvider @@ -48,6 +49,14 @@ import com.wire.kalium.logic.feature.connection.MarkConnectionRequestAsNotifiedU import com.wire.kalium.logic.feature.connection.ObserveConnectionListUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCase import com.wire.kalium.logic.feature.connection.ObservePendingConnectionRequestsUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.AddConversationToFavoritesUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCase +import com.wire.kalium.logic.feature.conversation.folder.ObserveConversationsFromFolderUseCaseImpl +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCase +import com.wire.kalium.logic.feature.conversation.folder.RemoveConversationFromFavoritesUseCaseImpl import com.wire.kalium.logic.feature.conversation.guestroomlink.CanCreatePasswordProtectedLinksUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCase import com.wire.kalium.logic.feature.conversation.guestroomlink.GenerateGuestRoomLinkUseCaseImpl @@ -71,15 +80,17 @@ import com.wire.kalium.logic.feature.team.DeleteTeamConversationUseCaseImpl import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.logic.sync.receiver.conversation.RenamedConversationEventHandler import com.wire.kalium.logic.sync.receiver.handler.CodeUpdateHandlerImpl +import com.wire.kalium.util.KaliumDispatcher import com.wire.kalium.util.KaliumDispatcherImpl import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") class ConversationScope internal constructor( - private val conversationRepository: ConversationRepository, + internal val conversationRepository: ConversationRepository, private val conversationGroupRepository: ConversationGroupRepository, private val connectionRepository: ConnectionRepository, private val userRepository: UserRepository, + private val conversationFolderRepository: ConversationFolderRepository, private val syncManager: SyncManager, private val mlsConversationRepository: MLSConversationRepository, private val currentClientIdProvider: CurrentClientIdProvider, @@ -101,7 +112,8 @@ class ConversationScope internal constructor( private val scope: CoroutineScope, private val kaliumLogger: KaliumLogger, private val refreshUsersWithoutMetadata: RefreshUsersWithoutMetadataUseCase, - private val serverConfigLinks: ServerConfig.Links + private val serverConfigLinks: ServerConfig.Links, + internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { val getConversations: GetConversationsUseCase @@ -116,6 +128,9 @@ class ConversationScope internal constructor( val observeConversationListDetails: ObserveConversationListDetailsUseCase get() = ObserveConversationListDetailsUseCaseImpl(conversationRepository) + val observeConversationListDetailsWithEvents: ObserveConversationListDetailsWithEventsUseCase + get() = ObserveConversationListDetailsWithEventsUseCaseImpl(conversationRepository, conversationFolderRepository, getFavoriteFolder) + val observeConversationMembers: ObserveConversationMembersUseCase get() = ObserveConversationMembersUseCaseImpl(conversationRepository, userRepository) @@ -128,6 +143,9 @@ class ConversationScope internal constructor( val observeConversationDetails: ObserveConversationDetailsUseCase get() = ObserveConversationDetailsUseCase(conversationRepository) + val getConversationProtocolInfo: GetConversationProtocolInfoUseCase + get() = GetConversationProtocolInfoUseCase(conversationRepository) + val notifyConversationIsOpen: NotifyConversationIsOpenUseCase get() = NotifyConversationIsOpenUseCaseImpl( oneOnOneResolver, @@ -335,4 +353,12 @@ class ConversationScope internal constructor( get() = ObserveConversationUnderLegalHoldNotifiedUseCaseImpl(conversationRepository) val syncConversationCode: SyncConversationCodeUseCase get() = SyncConversationCodeUseCase(conversationGroupRepository, serverConfigLinks) + val observeConversationsFromFolder: ObserveConversationsFromFolderUseCase + get() = ObserveConversationsFromFolderUseCaseImpl(conversationFolderRepository) + val getFavoriteFolder: GetFavoriteFolderUseCase + get() = GetFavoriteFolderUseCaseImpl(conversationFolderRepository) + val addConversationToFavorites: AddConversationToFavoritesUseCase + get() = AddConversationToFavoritesUseCaseImpl(conversationFolderRepository) + val removeConversationFromFavorites: RemoveConversationFromFavoritesUseCase + get() = RemoveConversationFromFavoritesUseCaseImpl(conversationFolderRepository) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCase.kt new file mode 100644 index 00000000000..cb7b6c53dce --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCase.kt @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case that get Conversation.ProtocolInfo for a specific conversation. + * @see Conversation.ProtocolInfo + */ +interface GetConversationProtocolInfoUseCase { + sealed class Result { + data class Success(val protocolInfo: Conversation.ProtocolInfo) : Result() + data class Failure(val storageFailure: StorageFailure) : Result() + } + + /** + * @param conversationId the id of the conversation to observe + * @return a [Result] with the [Conversation.ProtocolInfo] of the conversation + */ + suspend operator fun invoke(conversationId: ConversationId): Result +} + +@Suppress("FunctionNaming") +internal fun GetConversationProtocolInfoUseCase( + conversationRepository: ConversationRepository, + dispatcher: KaliumDispatcher = KaliumDispatcherImpl +) = object : GetConversationProtocolInfoUseCase { + override suspend operator fun invoke(conversationId: ConversationId): GetConversationProtocolInfoUseCase.Result = + withContext(dispatcher.io) { + conversationRepository.getConversationProtocolInfo(conversationId) + .fold({ + GetConversationProtocolInfoUseCase.Result.Failure(it) + }, { + GetConversationProtocolInfoUseCase.Result.Success(it) + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt index 9f24b825689..9dab4f5fa45 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCase.kt @@ -22,9 +22,10 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.InteractionAvailability +import com.wire.kalium.logic.data.conversation.interactionAvailability import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.MessageContent -import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.SelfUser import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.data.user.UserRepository @@ -65,24 +66,7 @@ class ObserveConversationInteractionAvailabilityUseCase internal constructor( if (!isProtocolSupported) { // short-circuit to Unsupported Protocol if it's the case return@fold IsInteractionAvailableResult.Success(InteractionAvailability.UNSUPPORTED_PROTOCOL) } - val availability = when (conversationDetails) { - is ConversationDetails.Connection -> InteractionAvailability.DISABLED - is ConversationDetails.Group -> { - if (conversationDetails.isSelfUserMember) InteractionAvailability.ENABLED - else InteractionAvailability.NOT_MEMBER - } - - is ConversationDetails.OneOne -> when { - conversationDetails.otherUser.defederated -> InteractionAvailability.DISABLED - conversationDetails.otherUser.deleted -> InteractionAvailability.DELETED_USER - conversationDetails.otherUser.connectionStatus == ConnectionState.BLOCKED -> InteractionAvailability.BLOCKED_USER - conversationDetails.conversation.legalHoldStatus == Conversation.LegalHoldStatus.DEGRADED -> - InteractionAvailability.LEGAL_HOLD - else -> InteractionAvailability.ENABLED - } - - is ConversationDetails.Self, is ConversationDetails.Team -> InteractionAvailability.DISABLED - } + val availability = conversationDetails.interactionAvailability() IsInteractionAvailableResult.Success(availability) }) } @@ -110,31 +94,3 @@ sealed class IsInteractionAvailableResult { data class Success(val interactionAvailability: InteractionAvailability) : IsInteractionAvailableResult() data class Failure(val coreFailure: CoreFailure) : IsInteractionAvailableResult() } - -enum class InteractionAvailability { - /**User is able to send messages and make calls */ - ENABLED, - - /**Self user is no longer conversation member */ - NOT_MEMBER, - - /**Other user is blocked by self user */ - BLOCKED_USER, - - /**Other team member or public user has been removed */ - DELETED_USER, - - /** - * This indicates that the conversation is using a protocol that self user does not support. - */ - UNSUPPORTED_PROTOCOL, - - /**Conversation type doesn't support messaging */ - DISABLED, - - /** - * One of conversation members is under legal hold and self user is not able to interact with it. - * This applies to 1:1 conversations only when other member is a guest. - */ - LEGAL_HOLD -} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt new file mode 100644 index 00000000000..4f7f9529f61 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsWithEventsUseCaseImpl.kt @@ -0,0 +1,60 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation + +import com.wire.kalium.logic.data.conversation.ConversationDetails +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.ConversationFilter +import com.wire.kalium.logic.data.conversation.ConversationRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * This use case will observe and return the list of conversation details for the current user. + * @see ConversationDetails + */ +fun interface ObserveConversationListDetailsWithEventsUseCase { + suspend operator fun invoke(fromArchive: Boolean, conversationFilter: ConversationFilter): Flow> +} + +internal class ObserveConversationListDetailsWithEventsUseCaseImpl( + private val conversationRepository: ConversationRepository, + private val conversationFolderRepository: ConversationFolderRepository, + private val getFavoriteFolder: GetFavoriteFolderUseCase +) : ObserveConversationListDetailsWithEventsUseCase { + + override suspend operator fun invoke( + fromArchive: Boolean, + conversationFilter: ConversationFilter + ): Flow> { + return if (conversationFilter == ConversationFilter.FAVORITES) { + when (val result = getFavoriteFolder()) { + GetFavoriteFolderUseCase.Result.Failure -> { + flowOf(emptyList()) + } + + is GetFavoriteFolderUseCase.Result.Success -> conversationFolderRepository.observeConversationsFromFolder(result.folder.id) + } + } else { + conversationRepository.observeConversationListDetailsWithEvents(fromArchive, conversationFilter) + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt new file mode 100644 index 00000000000..994aae91ab7 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCase.kt @@ -0,0 +1,71 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will add a conversation to the favorites folder. + */ +interface AddConversationToFavoritesUseCase { + /** + * @param conversationId the id of the conversation + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke(conversationId: ConversationId): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class AddConversationToFavoritesUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : AddConversationToFavoritesUseCase { + override suspend fun invoke( + conversationId: ConversationId + ): AddConversationToFavoritesUseCase.Result = withContext(dispatchers.io) { + conversationFolderRepository.getFavoriteConversationFolder().fold( + { AddConversationToFavoritesUseCase.Result.Failure(it) }, + { folder -> + conversationFolderRepository.addConversationToFolder( + conversationId, + folder.id + ) + .flatMap { + conversationFolderRepository.syncConversationFoldersFromLocal() + } + .fold({ + AddConversationToFavoritesUseCase.Result.Failure(it) + }, { + AddConversationToFavoritesUseCase.Result.Success + }) + } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt new file mode 100644 index 00000000000..c8e7b950850 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCase.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase.Result +import com.wire.kalium.logic.functional.fold + +/** + * This use case will return the favorite folder. + * @return [Result.Success] with [ConversationFolder] in case of success, + * or [Result.Failure] if something went wrong - can't get data from local DB. + */ +fun interface GetFavoriteFolderUseCase { + suspend operator fun invoke(): Result + + sealed class Result { + data class Success(val folder: ConversationFolder) : Result() + data object Failure : Result() + } +} + +internal class GetFavoriteFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, +) : GetFavoriteFolderUseCase { + + override suspend operator fun invoke(): Result { + return conversationFolderRepository.getFavoriteConversationFolder().fold( + { Result.Failure }, + { Result.Success(it) } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt new file mode 100644 index 00000000000..b0ffa60fa93 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCase.kt @@ -0,0 +1,39 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import kotlinx.coroutines.flow.Flow + +/** + * This use case will observe and return the list of conversations from given folder. + * @see ConversationDetailsWithEvents + */ +fun interface ObserveConversationsFromFolderUseCase { + suspend operator fun invoke(folderId: String): Flow> +} + +internal class ObserveConversationsFromFolderUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, +) : ObserveConversationsFromFolderUseCase { + + override suspend operator fun invoke(folderId: String): Flow> { + return conversationFolderRepository.observeConversationsFromFolder(folderId) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt new file mode 100644 index 00000000000..6c927230411 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCase.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * This use case will remove a conversation from the favorites folder. + */ +interface RemoveConversationFromFavoritesUseCase { + /** + * @param conversationId the id of the conversation + * @return the [Result] indicating a successful operation, otherwise a [CoreFailure] + */ + suspend operator fun invoke(conversationId: ConversationId): Result + + sealed interface Result { + data object Success : Result + data class Failure(val cause: CoreFailure) : Result + } +} + +internal class RemoveConversationFromFavoritesUseCaseImpl( + private val conversationFolderRepository: ConversationFolderRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl +) : RemoveConversationFromFavoritesUseCase { + override suspend fun invoke( + conversationId: ConversationId + ): RemoveConversationFromFavoritesUseCase.Result = withContext(dispatchers.io) { + conversationFolderRepository.getFavoriteConversationFolder().fold( + { RemoveConversationFromFavoritesUseCase.Result.Failure(it) }, + { folder -> + conversationFolderRepository.removeConversationFromFolder(conversationId, folder.id) + .flatMap { + conversationFolderRepository.syncConversationFoldersFromLocal() + } + .fold({ + RemoveConversationFromFavoritesUseCase.Result.Failure(it) + }, { + RemoveConversationFromFavoritesUseCase.Result.Success + }) + } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt new file mode 100644 index 00000000000..216290437e3 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/folder/SyncConversationFoldersUseCase.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.functional.Either + +internal interface SyncConversationFoldersUseCase { + suspend operator fun invoke(): Either +} + +/** + * This use case will sync against the backend the conversation folders of the current user. + */ +internal class SyncConversationFoldersUseCaseImpl( + private val conversationRepository: ConversationFolderRepository, +) : SyncConversationFoldersUseCase { + override suspend operator fun invoke(): Either = conversationRepository.fetchConversationFolders() +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolver.kt index 2d0d945ea9d..53aadbd8a50 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolver.kt @@ -68,7 +68,7 @@ internal class MLSOneOnOneConversationResolverImpl( } else { kaliumLogger.d("Establishing mls group for one-on-one with ${userId.toLogString()}") conversationRepository.fetchMlsOneToOneConversation(userId).flatMap { conversation -> - joinExistingMLSConversationUseCase(conversation.id).map { conversation.id } + joinExistingMLSConversationUseCase(conversation.id, conversation.mlsPublicKeys).map { conversation.id } } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt index 5a9359cb840..5737853c8eb 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigrator.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.ConversationGroupRepository import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.user.OtherUser import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.data.user.type.isTeammate @@ -44,7 +45,8 @@ internal class OneOnOneMigratorImpl( private val conversationGroupRepository: ConversationGroupRepository, private val conversationRepository: ConversationRepository, private val messageRepository: MessageRepository, - private val userRepository: UserRepository + private val userRepository: UserRepository, + private val systemMessageInserter: SystemMessageInserter ) : OneOnOneMigrator { override suspend fun migrateToProteus(user: OtherUser): Either = @@ -87,6 +89,12 @@ internal class OneOnOneMigratorImpl( userId = user.id ).map { mlsConversation + }.also { + systemMessageInserter.insertProtocolChangedSystemMessage( + conversationId = mlsConversation, + senderUserId = user.id, + protocol = Conversation.Protocol.MLS + ) } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 10b23a4f3fb..92d7176faaf 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.feature.debug import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.cache.SelfConversationIdProvider +import com.wire.kalium.logic.configuration.notification.NotificationTokenRepository import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.client.ClientRepository import com.wire.kalium.logic.data.client.MLSClientProvider @@ -28,6 +29,7 @@ import com.wire.kalium.logic.data.client.remote.ClientRemoteRepository import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.LegalHoldStatusMapperImpl import com.wire.kalium.logic.data.conversation.MLSConversationRepository +import com.wire.kalium.logic.data.event.EventRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.message.MessageRepository import com.wire.kalium.logic.data.message.ProtoContentMapper @@ -53,6 +55,8 @@ import com.wire.kalium.logic.feature.message.StaleEpochVerifier import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsSenderUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.EphemeralMessageDeletionHandlerImpl +import com.wire.kalium.logic.feature.notificationToken.SendFCMTokenUseCase +import com.wire.kalium.logic.feature.notificationToken.SendFCMTokenToAPIUseCaseImpl import com.wire.kalium.logic.sync.SyncManager import com.wire.kalium.logic.sync.incremental.EventProcessor import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler @@ -78,6 +82,7 @@ class DebugScope internal constructor( private val userRepository: UserRepository, private val userId: UserId, private val assetRepository: AssetRepository, + private val eventRepository: EventRepository, private val syncManager: SyncManager, private val slowSyncRepository: SlowSyncRepository, private val messageSendingScheduler: MessageSendingScheduler, @@ -85,11 +90,15 @@ class DebugScope internal constructor( private val staleEpochVerifier: StaleEpochVerifier, private val eventProcessor: EventProcessor, private val legalHoldHandler: LegalHoldHandler, + private val notificationTokenRepository: NotificationTokenRepository, private val scope: CoroutineScope, - private val logger: KaliumLogger, + logger: KaliumLogger, internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { + val establishSession: EstablishSessionUseCase + get() = EstablishSessionUseCaseImpl(sessionEstablisher) + val breakSession: BreakSessionUseCase get() = BreakSessionUseCaseImpl(proteusClientProvider) @@ -116,6 +125,12 @@ class DebugScope internal constructor( eventProcessor = eventProcessor ) + val synchronizeExternalData: SynchronizeExternalDataUseCase + get() = SynchronizeExternalDataUseCaseImpl( + eventRepository = eventRepository, + eventProcessor = eventProcessor + ) + private val messageSendFailureHandler: MessageSendFailureHandler get() = MessageSendFailureHandlerImpl( userRepository, @@ -168,7 +183,12 @@ class DebugScope internal constructor( messageSendingInterceptor, userRepository, staleEpochVerifier, - { message, expirationData -> ephemeralMessageDeletionHandler.enqueueSelfDeletion(message, expirationData) }, + { message, expirationData -> + ephemeralMessageDeletionHandler.enqueueSelfDeletion( + message, + expirationData + ) + }, scope ) @@ -197,4 +217,11 @@ class DebugScope internal constructor( selfUserId = userId, kaliumLogger = logger ) + + val sendFCMTokenToServer: SendFCMTokenUseCase + get() = SendFCMTokenToAPIUseCaseImpl( + currentClientIdProvider, + clientRepository, + notificationTokenRepository, + ) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/EstablishSessionUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/EstablishSessionUseCase.kt new file mode 100644 index 00000000000..80afa83232d --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/EstablishSessionUseCase.kt @@ -0,0 +1,59 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.debug + +import com.wire.kalium.logger.obfuscateId +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.conversation.Recipient +import com.wire.kalium.logic.data.message.SessionEstablisher +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.kaliumLogger + +interface EstablishSessionUseCase { + + /** + * Establish a proteus session with another client + * + * @param userId the id of the user to whom the session established + * @param clientId the id of the client of the user to whom the session should be established + * @return an [EstablishSessionResult] containing a [CoreFailure] in case anything goes wrong + * and [EstablishSessionResult.Success] in case everything succeeds + */ + suspend operator fun invoke(userId: UserId, clientId: ClientId): EstablishSessionResult +} + +sealed class EstablishSessionResult { + data object Success : EstablishSessionResult() + data class Failure(val coreFailure: CoreFailure) : EstablishSessionResult() +} + +internal class EstablishSessionUseCaseImpl(val sessionEstablisher: SessionEstablisher) : EstablishSessionUseCase { + override suspend fun invoke(userId: UserId, clientId: ClientId): EstablishSessionResult { + return sessionEstablisher.prepareRecipientsForNewOutgoingMessage( + listOf(Recipient(id = userId, clients = listOf(clientId))) + ).fold({ + kaliumLogger.e("Failed to get establish session $it") + EstablishSessionResult.Failure(it) + }, { + kaliumLogger.d("Established session with ${userId.toLogString()} with ${clientId.value.obfuscateId()}") + EstablishSessionResult.Success + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SynchronizeExternalDataUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SynchronizeExternalDataUseCase.kt new file mode 100644 index 00000000000..2940d5f9f2b --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/SynchronizeExternalDataUseCase.kt @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.debug + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.event.EventRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.foldToEitherWhileRight +import com.wire.kalium.logic.sync.incremental.EventProcessor + +fun interface SynchronizeExternalDataUseCase { + + /** + * Consume event data coming from an external source. + * + * @param data NotificationResponse serialized to JSON + * @return an [SynchronizeExternalDataResult] containing a [CoreFailure] in case anything goes wrong + */ + suspend operator fun invoke( + data: String, + ): SynchronizeExternalDataResult + +} + +sealed class SynchronizeExternalDataResult { + data object Success : SynchronizeExternalDataResult() + data class Failure(val coreFailure: CoreFailure) : SynchronizeExternalDataResult() +} + +internal class SynchronizeExternalDataUseCaseImpl( + val eventRepository: EventRepository, + val eventProcessor: EventProcessor +) : SynchronizeExternalDataUseCase { + + override suspend operator fun invoke( + data: String, + ): SynchronizeExternalDataResult { + return eventRepository.parseExternalEvents(data).foldToEitherWhileRight(Unit) { event, _ -> + eventProcessor.processEvent(event) + }.fold({ + return@fold SynchronizeExternalDataResult.Failure(it) + }, { + return@fold SynchronizeExternalDataResult.Success + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CheckCrlRevocationListUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CheckCrlRevocationListUseCase.kt index 6979d9412cd..d8eea576ab8 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CheckCrlRevocationListUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CheckCrlRevocationListUseCase.kt @@ -45,6 +45,6 @@ class CheckCrlRevocationListUseCase internal constructor( } } } - } + } ?: logger.w("No CRLs found.") } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/SyncCertificateRevocationListUseCase.kt similarity index 83% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorker.kt rename to logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/SyncCertificateRevocationListUseCase.kt index a48ee6e4618..d5132c3f3a6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/SyncCertificateRevocationListUseCase.kt @@ -23,38 +23,32 @@ import com.wire.kalium.logic.data.e2ei.RevocationListChecker import com.wire.kalium.logic.data.sync.IncrementalSyncRepository import com.wire.kalium.logic.data.sync.IncrementalSyncStatus import com.wire.kalium.logic.functional.map -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.datetime.Clock /** - * This worker will wait until the sync is done and then check the CRLs if needed. + * This use case will wait until the sync is done and then check the CRLs if needed. * - */ -interface CertificateRevocationListCheckWorker { - suspend fun execute() -} - -/** - * Base implementation of [CertificateRevocationListCheckWorker]. + * Base implementation of [SyncCertificateRevocationListUseCase]. * @param certificateRevocationListRepository The CRL repository. * @param incrementalSyncRepository The incremental sync repository. * @param revocationListChecker The check revocation list use case. * */ -internal class CertificateRevocationListCheckWorkerImpl( +class SyncCertificateRevocationListUseCase internal constructor( private val certificateRevocationListRepository: CertificateRevocationListRepository, private val incrementalSyncRepository: IncrementalSyncRepository, private val revocationListChecker: RevocationListChecker, kaliumLogger: KaliumLogger -) : CertificateRevocationListCheckWorker { +) { private val logger = kaliumLogger.withTextTag("CertificateRevocationListCheckWorker") - override suspend fun execute() { + suspend operator fun invoke() { logger.d("Starting to monitor") incrementalSyncRepository.incrementalSyncState - .filter { it is IncrementalSyncStatus.Live } - .collect { + .first { it is IncrementalSyncStatus.Live } + .let { logger.i("Checking certificate revocation list (CRL)..") certificateRevocationListRepository.getCRLs()?.cRLWithExpirationList?.forEach { crl -> if (crl.expiration < Clock.System.now().epochSeconds.toULong()) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/FetchMLSVerificationStatusUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/FetchMLSVerificationStatusUseCase.kt index 7250f73cb5f..85d8c5254e1 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/FetchMLSVerificationStatusUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/FetchMLSVerificationStatusUseCase.kt @@ -114,13 +114,13 @@ internal class FetchMLSVerificationStatusUseCaseImpl( // check that all identities are valid and name and handle are matching for ((userId, wireIdentity) in ccIdentity) { val persistedMemberInfo = dbData.members[userId] - val isUserVerified = wireIdentity.firstOrNull { + val isUserVerified = wireIdentity.none { it.status != CryptoCertificateStatus.VALID || it.credentialType != CredentialType.X509 || it.x509Identity == null || it.x509Identity?.displayName != persistedMemberInfo?.name || it.x509Identity?.handle?.handle != persistedMemberInfo?.handle - } == null + } if (!isUserVerified) { newStatus = VerificationStatus.NOT_VERIFIED break @@ -141,7 +141,7 @@ internal class FetchMLSVerificationStatusUseCaseImpl( var dbData = epochChangesData val missingUsers = missingUsers( - usersFromDB = epochChangesData.members.keys.map { it }.toSet(), + usersFromDB = epochChangesData.members.keys, usersFromCC = ccIdentities.keys ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetMembersE2EICertificateStatusesUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetMembersE2EICertificateStatusesUseCase.kt index 034d802a9bc..2ce15cbbefc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetMembersE2EICertificateStatusesUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetMembersE2EICertificateStatusesUseCase.kt @@ -20,11 +20,14 @@ package com.wire.kalium.logic.feature.e2ei.usecase import com.wire.kalium.cryptography.CredentialType import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.cryptography.WireIdentity +import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.MLSConversationRepository +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.e2ei.CertificateStatus -import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.functional.map /** * This use case is used to get the e2ei certificates of all the users in Conversation. @@ -35,23 +38,27 @@ interface GetMembersE2EICertificateStatusesUseCase { } class GetMembersE2EICertificateStatusesUseCaseImpl internal constructor( - private val mlsConversationRepository: MLSConversationRepository + private val mlsConversationRepository: MLSConversationRepository, + private val conversationRepository: ConversationRepository ) : GetMembersE2EICertificateStatusesUseCase { override suspend operator fun invoke(conversationId: ConversationId, userIds: List): Map = - mlsConversationRepository.getMembersIdentities(conversationId, userIds).fold( - { mapOf() }, - { - it.mapValues { (_, identities) -> - // todo: we need to check the user name and details! - identities.isUserMLSVerified() + mlsConversationRepository.getMembersIdentities(conversationId, userIds) + .map { identities -> + val usersNameAndHandle = conversationRepository.selectMembersNameAndHandle(conversationId).getOrElse(mapOf()) + + identities.mapValues { (userId, identities) -> + identities.isUserMLSVerified(usersNameAndHandle[userId]) } - } - ) + }.getOrElse(mapOf()) } /** * @return if given user is verified or not */ -fun List.isUserMLSVerified() = this.isNotEmpty() && this.all { - it.x509Identity != null && it.credentialType == CredentialType.X509 && it.status == CryptoCertificateStatus.VALID +fun List.isUserMLSVerified(nameAndHandle: NameAndHandle?) = this.isNotEmpty() && this.all { + it.x509Identity != null + && it.credentialType == CredentialType.X509 + && it.status == CryptoCertificateStatus.VALID + && it.x509Identity?.handle?.handle == nameAndHandle?.handle + && it.x509Identity?.displayName == nameAndHandle?.name } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetUserE2EICertificateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetUserE2EICertificateUseCase.kt index 7b4d0313321..a63e7705fe9 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetUserE2EICertificateUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/GetUserE2EICertificateUseCase.kt @@ -19,8 +19,10 @@ package com.wire.kalium.logic.feature.e2ei.usecase import com.wire.kalium.logic.data.conversation.MLSConversationRepository import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.feature.user.IsE2EIEnabledUseCase import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrNull /** * This use case is used to get the e2ei certificate status of specific user @@ -31,11 +33,14 @@ interface IsOtherUserE2EIVerifiedUseCase { class IsOtherUserE2EIVerifiedUseCaseImpl internal constructor( private val mlsConversationRepository: MLSConversationRepository, - private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase + private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, + private val userRepository: UserRepository ) : IsOtherUserE2EIVerifiedUseCase { override suspend operator fun invoke(userId: UserId): Boolean = - isE2EIEnabledUseCase() && mlsConversationRepository.getUserIdentity(userId).fold( - { false }, - { it.isUserMLSVerified() } - ) + if (isE2EIEnabledUseCase()) { + val nameHandle = userRepository.getNameAndHandle(userId).getOrNull() + mlsConversationRepository.getUserIdentity(userId).fold({ false }, { it.isUserMLSVerified(nameHandle) }) + } else { + false + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt index deac100c120..f137332521c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/ObserveCertificateRevocationForSelfClientUseCase.kt @@ -21,9 +21,7 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.feature.e2ei.MLSClientE2EIStatus -import com.wire.kalium.logic.feature.e2ei.MLSClientIdentity -import com.wire.kalium.logic.functional.isRight -import com.wire.kalium.logic.functional.map +import com.wire.kalium.logic.functional.onSuccess /** * Use case to observe certificate revocation for self client. @@ -44,9 +42,9 @@ internal class ObserveCertificateRevocationForSelfClientUseCaseImpl( override suspend fun invoke() { logger.d("Checking if should notify certificate revocation") - currentClientIdProvider().map { clientId -> - getE2eiCertificate(clientId).run { - if (isRight() && (value as MLSClientIdentity).e2eiStatus == MLSClientE2EIStatus.REVOKED) { + currentClientIdProvider().onSuccess { clientId -> + getE2eiCertificate(clientId).onSuccess { + if (it.e2eiStatus == MLSClientE2EIStatus.REVOKED) { logger.i("Setting that should notify certificate revocation") userConfigRepository.setShouldNotifyForRevokedCertificate(true) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagsSyncWorker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagsSyncWorker.kt index d1b3ce2843a..cb64dfd789c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagsSyncWorker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagsSyncWorker.kt @@ -17,16 +17,10 @@ */ package com.wire.kalium.logic.feature.featureConfig -import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logic.data.sync.IncrementalSyncRepository import com.wire.kalium.logic.data.sync.IncrementalSyncStatus -import com.wire.kalium.logic.logStructuredJson -import kotlinx.coroutines.flow.filter -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes +import kotlinx.coroutines.flow.first /** * Worker that periodically syncs feature flags. @@ -39,43 +33,16 @@ internal class FeatureFlagSyncWorkerImpl( private val incrementalSyncRepository: IncrementalSyncRepository, private val syncFeatureConfigs: SyncFeatureConfigsUseCase, kaliumLogger: KaliumLogger, - private val minIntervalBetweenPulls: Duration = MIN_INTERVAL_BETWEEN_PULLS, - private val clock: Clock = Clock.System ) : FeatureFlagsSyncWorker { - private var lastPullInstant: Instant? = null private val logger = kaliumLogger.withTextTag("FeatureFlagSyncWorker") override suspend fun execute() { logger.d("Starting to monitor") - incrementalSyncRepository.incrementalSyncState.filter { + incrementalSyncRepository.incrementalSyncState.first { it is IncrementalSyncStatus.Live - }.collect { - syncFeatureFlagsIfNeeded() - } - } - - private suspend fun FeatureFlagSyncWorkerImpl.syncFeatureFlagsIfNeeded() { - val now = clock.now() - val wasLastPullRecent = lastPullInstant?.let { lastPull -> - lastPull + minIntervalBetweenPulls > now - } ?: false - logger.logStructuredJson( - level = KaliumLogLevel.INFO, - leadingMessage = "syncFeatureFlagsIfNeeded", - jsonStringKeyValues = mapOf( - "lastPullInstant" to lastPullInstant, - "wasLastPullRecent" to wasLastPullRecent - ) - ) - if (!wasLastPullRecent) { - logger.i("Synching feature configs and updating lastPullInstant") + }.let { syncFeatureConfigs() - lastPullInstant = now } } - - private companion object { - val MIN_INTERVAL_BETWEEN_PULLS = 60.minutes - } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt index eadc29abf6f..bb49c7fda27 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageScope.kt @@ -60,13 +60,15 @@ import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCase import com.wire.kalium.logic.feature.asset.ScheduleNewAssetMessageUseCaseImpl import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCase import com.wire.kalium.logic.feature.asset.UpdateAssetMessageTransferStatusUseCaseImpl +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCaseImpl import com.wire.kalium.logic.feature.message.composite.SendButtonActionConfirmationMessageUseCase -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCase -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCaseImpl import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import com.wire.kalium.logic.feature.message.composite.SendButtonMessageUseCase import com.wire.kalium.logic.feature.message.confirmation.ConfirmationDeliveryHandler import com.wire.kalium.logic.feature.message.confirmation.ConfirmationDeliveryHandlerImpl +import com.wire.kalium.logic.feature.message.confirmation.SendDeliverSignalUseCase +import com.wire.kalium.logic.feature.message.confirmation.SendDeliverSignalUseCaseImpl import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCase import com.wire.kalium.logic.feature.message.draft.GetMessageDraftUseCaseImpl import com.wire.kalium.logic.feature.message.draft.RemoveMessageDraftUseCase @@ -160,8 +162,8 @@ class MessageScope internal constructor( protoContentMapper = protoContentMapper ) - private val validateAssetMimeTypeUseCase: ValidateAssetMimeTypeUseCase - get() = ValidateAssetMimeTypeUseCaseImpl() + private val validateAssetMimeTypeUseCase: ValidateAssetFileTypeUseCase + get() = ValidateAssetFileTypeUseCaseImpl() private val messageContentEncoder = MessageContentEncoder() private val messageSendingInterceptor: MessageSendingInterceptor @@ -177,12 +179,17 @@ class MessageScope internal constructor( kaliumLogger = kaliumLogger ) - internal val confirmationDeliveryHandler: ConfirmationDeliveryHandler = ConfirmationDeliveryHandlerImpl( - syncManager = syncManager, + private val sendDeliverSignalUseCase: SendDeliverSignalUseCase = SendDeliverSignalUseCaseImpl( selfUserId = selfUserId, + messageSender = messageSender, currentClientIdProvider = currentClientIdProvider, + kaliumLogger = kaliumLogger + ) + + internal val confirmationDeliveryHandler: ConfirmationDeliveryHandler = ConfirmationDeliveryHandlerImpl( + syncManager = syncManager, conversationRepository = conversationRepository, - messageSender = messageSender, + sendDeliverSignalUseCase = sendDeliverSignalUseCase, kaliumLogger = kaliumLogger, ) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSender.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSender.kt index 9c6756126aa..4bfee8589f6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSender.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/MessageSender.kt @@ -208,6 +208,8 @@ internal class MessageSenderImpl internal constructor( }.onSuccess { startSelfDeletionIfNeeded(message) } + }.onFailure { + logger.e("Failed to send message ${message::class.qualifiedName}. Failure = $it") } override suspend fun broadcastMessage( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PendingProposalScheduler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PendingProposalScheduler.kt index e7e000e3bdf..4993b062031 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PendingProposalScheduler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/PendingProposalScheduler.kt @@ -24,7 +24,6 @@ import com.wire.kalium.logic.data.conversation.SubconversationRepository import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.sync.IncrementalSyncRepository import com.wire.kalium.logic.data.sync.IncrementalSyncStatus -import com.wire.kalium.logic.featureFlags.KaliumConfigs import com.wire.kalium.logic.functional.distinct import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.kaliumLogger @@ -63,7 +62,6 @@ interface PendingProposalScheduler { } internal class PendingProposalSchedulerImpl( - private val kaliumConfigs: KaliumConfigs, private val incrementalSyncRepository: IncrementalSyncRepository, private val mlsConversationRepository: Lazy, private val subconversationRepository: Lazy, @@ -82,7 +80,7 @@ internal class PendingProposalSchedulerImpl( commitPendingProposalsScope.launch() { incrementalSyncRepository.incrementalSyncState.collectLatest { syncState -> ensureActive() - if (syncState == IncrementalSyncStatus.Live && kaliumConfigs.isMLSSupportEnabled) { + if (syncState == IncrementalSyncStatus.Live) { startCommittingPendingProposals() } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCase.kt index 1d11875af16..0f85600dd07 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCase.kt @@ -160,6 +160,7 @@ class RetryFailedMessageUseCase internal constructor( .map { updatedMessage } // we need to persist new asset remoteData and status } } + .onSuccess { updateAssetMessageTransferStatus(AssetTransferStatus.UPLOADED, message.conversationId, message.id) } .onFailure { kaliumLogger.e("Failed to retry sending asset message. Failure = $it") updateAssetMessageTransferStatus(AssetTransferStatus.FAILED_UPLOAD, message.conversationId, message.id) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandler.kt index 7a8e7a54a7e..d8e168987bd 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandler.kt @@ -17,22 +17,16 @@ */ package com.wire.kalium.logic.feature.message.confirmation -import com.benasher44.uuid.uuid4 +import co.touchlab.stately.collections.ConcurrentMutableMap import com.wire.kalium.logger.KaliumLogLevel import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.CurrentClientIdProvider import com.wire.kalium.logic.data.id.MessageId -import com.wire.kalium.logic.data.message.Message -import com.wire.kalium.logic.data.message.MessageContent -import com.wire.kalium.logic.data.message.receipt.ReceiptType -import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.message.MessageSender import com.wire.kalium.logic.functional.flatMap -import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.logStructuredJson import com.wire.kalium.logic.sync.SyncManager @@ -42,9 +36,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.first -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.datetime.Clock /** * Internal: Handles the send of delivery confirmation of messages. @@ -54,23 +45,20 @@ internal interface ConfirmationDeliveryHandler { suspend fun sendPendingConfirmations() } -@Suppress("LongParameterList") internal class ConfirmationDeliveryHandlerImpl( private val syncManager: SyncManager, - private val selfUserId: UserId, - private val currentClientIdProvider: CurrentClientIdProvider, private val conversationRepository: ConversationRepository, - private val messageSender: MessageSender, - private val pendingConfirmationMessages: MutableMap> = mutableMapOf(), - kaliumLogger: KaliumLogger, + private val sendDeliverSignalUseCase: SendDeliverSignalUseCase, + private val pendingConfirmationMessages: ConcurrentMutableMap> = + ConcurrentMutableMap(), + kaliumLogger: KaliumLogger ) : ConfirmationDeliveryHandler { private val kaliumLogger = kaliumLogger.withTextTag("ConfirmationDeliveryHandler") private val holder = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - private val mutex = Mutex() - override suspend fun enqueueConfirmationDelivery(conversationId: ConversationId, messageId: String) = mutex.withLock { - val conversationMessages = pendingConfirmationMessages[conversationId] ?: mutableSetOf() + override suspend fun enqueueConfirmationDelivery(conversationId: ConversationId, messageId: String) { + val conversationMessages = pendingConfirmationMessages.computeIfAbsent(conversationId) { mutableSetOf() } val isNewMessage = conversationMessages.add(messageId) if (isNewMessage) { kaliumLogger.logStructuredJson( @@ -82,7 +70,6 @@ internal class ConfirmationDeliveryHandlerImpl( "queueCount" to pendingConfirmationMessages.size ) ) - pendingConfirmationMessages[conversationId] = conversationMessages holder.emit(Unit) } } @@ -91,71 +78,40 @@ internal class ConfirmationDeliveryHandlerImpl( override suspend fun sendPendingConfirmations() { holder.debounce(DEBOUNCE_SEND_CONFIRMATION_TIME).collectLatest { syncManager.waitUntilLive() - mutex.withLock { - kaliumLogger.d("Started collecting pending messages for delivery confirmation") - with(pendingConfirmationMessages.iterator()) { - forEach { (conversationId, messages) -> - conversationRepository.observeConversationById(conversationId).first().flatMap { conversation -> - if (conversation.type == Conversation.Type.ONE_ON_ONE) { - sendDeliveredSignal( - conversation = conversation, - messages = messages.toList() - ) - } else { - kaliumLogger.logStructuredJson( - level = KaliumLogLevel.DEBUG, - leadingMessage = "Skipping group conversation: ${conversation?.id?.toLogString()}", - jsonStringKeyValues = mapOf( - "conversationId" to conversation?.id?.toLogString(), - "messages" to messages.joinToString { it.obfuscateId() }, - "messageCount" to messages.size - ) - ) + kaliumLogger.d("Started collecting pending messages for delivery confirmation") + val messagesToSend = pendingConfirmationMessages.block { it.toMap() } + messagesToSend.forEach { (conversationId, messages) -> + conversationRepository.observeConversationById(conversationId).first().flatMap { conversation -> + if (conversation.type == Conversation.Type.ONE_ON_ONE) { + sendDeliverSignalUseCase( + conversation = conversation, + messages = messages.toList() + ).onSuccess { + pendingConfirmationMessages.block { + val currentMessages = it[conversationId] + if (currentMessages != null) { + currentMessages.removeAll(messages.toSet()) + if (currentMessages.isEmpty()) { + it.remove(conversationId) + } + } } - remove() // safely clean the entry [conversationId to messages] - Unit.right() } + } else { + kaliumLogger.logStructuredJson( + level = KaliumLogLevel.DEBUG, + leadingMessage = "Skipping group conversation: ${conversation.id.toLogString()}", + jsonStringKeyValues = mapOf( + "conversationId" to conversation.id.toLogString(), + "messages" to messages.joinToString { it.obfuscateId() }, + "messageCount" to messages.size + ) + ) } + Unit.right() } } - } - } - - private suspend fun sendDeliveredSignal(conversation: Conversation, messages: List) { - currentClientIdProvider().flatMap { currentClientId -> - val message = Message.Signaling( - id = uuid4().toString(), - content = MessageContent.Receipt(ReceiptType.DELIVERED, messages), - conversationId = conversation.id, - date = Clock.System.now(), - senderUserId = selfUserId, - senderClientId = currentClientId, - status = Message.Status.Pending, - isSelfMessage = true, - expirationData = null - ) - messageSender.sendMessage(message).fold({ error -> - kaliumLogger.logStructuredJson( - level = KaliumLogLevel.ERROR, - leadingMessage = "Error while sending delivery confirmation for ${conversation.id.toLogString()}", - jsonStringKeyValues = mapOf( - "conversationId" to conversation.id.toLogString(), - "messages" to messages.joinToString { it.obfuscateId() }, - "error" to error.toString() - ) - ) - }, { - kaliumLogger.logStructuredJson( - level = KaliumLogLevel.DEBUG, - leadingMessage = "Delivery confirmation sent for ${conversation.name} and message count: ${messages.size}", - jsonStringKeyValues = mapOf( - "conversationId" to conversation.id.toLogString(), - "messages" to messages.joinToString { it.obfuscateId() }, - "messageCount" to messages.size - ) - ) - }) - Unit.right() + kaliumLogger.d("Finished collecting pending messages for delivery confirmation") } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCase.kt new file mode 100644 index 00000000000..07adc5595d6 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCase.kt @@ -0,0 +1,94 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.message.confirmation + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logger.KaliumLogLevel +import com.wire.kalium.logger.KaliumLogger +import com.wire.kalium.logger.obfuscateId +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.MessageId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.message.receipt.ReceiptType +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.onFailure +import com.wire.kalium.logic.functional.onSuccess +import com.wire.kalium.logic.logStructuredJson +import kotlinx.datetime.Clock + +/** + * Use case for sending a delivery confirmation signal for a list of messages in a conversation. + */ +interface SendDeliverSignalUseCase { + suspend operator fun invoke(conversation: Conversation, messages: List): Either +} + +internal class SendDeliverSignalUseCaseImpl( + private val selfUserId: UserId, + private val messageSender: MessageSender, + private val currentClientIdProvider: CurrentClientIdProvider, + private val kaliumLogger: KaliumLogger +) : SendDeliverSignalUseCase { + override suspend fun invoke( + conversation: Conversation, + messages: List + ): Either = currentClientIdProvider() + .flatMap { currentClientId -> + val message = Message.Signaling( + id = uuid4().toString(), + content = MessageContent.Receipt(ReceiptType.DELIVERED, messages), + conversationId = conversation.id, + date = Clock.System.now(), + senderUserId = selfUserId, + senderClientId = currentClientId, + status = Message.Status.Pending, + isSelfMessage = true, + expirationData = null + ) + messageSender.sendMessage(message) + .onFailure { error -> + kaliumLogger.logStructuredJson( + level = KaliumLogLevel.ERROR, + leadingMessage = "Error while sending delivery confirmation for ${conversation.id.toLogString()}", + jsonStringKeyValues = mapOf( + "conversationId" to conversation.id.toLogString(), + "messages" to messages.joinToString { it.obfuscateId() }, + "error" to error.toString() + ) + ) + } + .onSuccess { + kaliumLogger.logStructuredJson( + level = KaliumLogLevel.DEBUG, + leadingMessage = "Delivery confirmation sent for ${conversation.id.toLogString()}" + + " and message count: ${messages.size}", + jsonStringKeyValues = mapOf( + "conversationId" to conversation.id.toLogString(), + "messages" to messages.joinToString { it.obfuscateId() }, + "messageCount" to messages.size + ) + ) + } + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCase.kt index a83b73e065e..1105f1a24af 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCase.kt @@ -62,30 +62,34 @@ internal class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl( private val selfUserId: UserId, private val selfConversationIdProvider: SelfConversationIdProvider ) : DeleteEphemeralMessageForSelfUserAsReceiverUseCase { + override suspend fun invoke(conversationId: ConversationId, messageId: String): Either = - messageRepository.markMessageAsDeleted(messageId, conversationId).flatMap { - currentClientIdProvider().flatMap { currentClientId -> - messageRepository.getMessageById(conversationId, messageId) - .flatMap { message -> - sendDeleteMessageToSelf( - message.id, - conversationId, - currentClientId - ).flatMap { - sendDeleteMessageToOriginalSender( - message.id, - message.conversationId, - message.senderUserId, - currentClientId - ) - }.onSuccess { - deleteMessageAssetIfExists(message) - }.flatMap { - messageRepository.deleteMessage(messageId, conversationId) - } + messageRepository.getMessageById(conversationId, messageId) + .onSuccess { message -> + deleteMessageAssetLocallyIfExists(message) + } + .flatMap { message -> + messageRepository.markMessageAsDeleted(messageId, conversationId) + .flatMap { + currentClientIdProvider() + .flatMap { currentClientId -> + sendDeleteMessageToSelf( + message.id, + conversationId, + currentClientId + ).flatMap { + sendDeleteMessageToOriginalSender( + message.id, + message.conversationId, + message.senderUserId, + currentClientId + ) + }.flatMap { + messageRepository.deleteMessage(messageId, conversationId) + } + } } } - } private suspend fun sendDeleteMessageToSelf( messageToDelete: String, @@ -131,13 +135,9 @@ internal class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl( ) } - private suspend fun deleteMessageAssetIfExists(message: Message) { + private suspend fun deleteMessageAssetLocallyIfExists(message: Message) { (message.content as? MessageContent.Asset)?.value?.remoteData?.let { assetToRemove -> - assetRepository.deleteAsset( - assetToRemove.assetId, - assetToRemove.assetDomain, - assetToRemove.assetToken - ).onFailure { + assetRepository.deleteAssetLocally(assetToRemove.assetId).onFailure { kaliumLogger.withFeatureId(ASSETS).w("delete message asset failure: $it") } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt index 13fa1c578da..515895fadd3 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/EphemeralMessageDeletionHandler.kt @@ -62,43 +62,38 @@ internal class EphemeralMessageDeletionHandlerImpl( private val ongoingSelfDeletionMessagesMutex = Mutex() private val ongoingSelfDeletionMessages = mutableMapOf, Unit>() + override fun startSelfDeletion(conversationId: ConversationId, messageId: String) { launch { messageRepository.getMessageById(conversationId, messageId).map { message -> - if (message.expirationData != null && message.status != Message.Status.Pending) { - enqueueSelfDeletion( - message = message, - expirationData = message.expirationData!! - ) - } else { - kaliumLogger.i( - "Self deletion requested for message without expiration data: ${message.content.getType()}" - ) + val expirationData = message.expirationData + when { + expirationData == null -> + kaliumLogger.i("Self deletion requested for message without expiration data: ${message.content.getType()}") + + message.status == Message.Status.Pending -> + logger.log(LoggingSelfDeletionEvent.InvalidMessageStatus(message, expirationData)) + + else -> enqueueSelfDeletion(message, expirationData) } } } } override fun enqueueSelfDeletion(message: Message, expirationData: Message.ExpirationData) { - logger.log( - LoggingSelfDeletionEvent.InvalidMessageStatus( - message, - expirationData - ) - ) - launch { ongoingSelfDeletionMessagesMutex.withLock { val isSelfDeletionOutgoing = ongoingSelfDeletionMessages[message.conversationId to message.id] != null - logger.log( - LoggingSelfDeletionEvent.SelfSelfDeletionAlreadyRequested( - message, - expirationData + if (isSelfDeletionOutgoing) { + logger.log( + LoggingSelfDeletionEvent.SelfSelfDeletionAlreadyRequested( + message, + expirationData + ) ) - ) - - if (isSelfDeletionOutgoing) return@launch + return@launch + } addToOutgoingDeletion(message) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/SelfDeletionEventLogger.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/SelfDeletionEventLogger.kt index fdfe5ba25dc..c0bb1d77772 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/SelfDeletionEventLogger.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/ephemeral/SelfDeletionEventLogger.kt @@ -44,6 +44,7 @@ internal sealed class LoggingSelfDeletionEvent( fun toJson(): String { return EPHEMERAL_LOG_TAG + mapOf( "message" to (message as Message.Sendable).toLogMap(), + "expiration-data" to expirationData.toLogMap(), ) .toMutableMap() .plus(toLogMap()) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdater.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdater.kt index b1da606fafc..5c7cc3f8253 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdater.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdater.kt @@ -15,7 +15,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:Suppress("konsist.useCasesShouldNotAccessNetworkLayerDirectly") package com.wire.kalium.logic.feature.notificationToken @@ -27,7 +26,6 @@ import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.isRight import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.kaliumLogger -import com.wire.kalium.network.api.model.PushTokenBody import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -51,18 +49,18 @@ internal class PushTokenUpdater( notificationTokenRepository.getNotificationToken() .flatMap { notificationToken -> clientRepository.registerToken( - body = PushTokenBody( - senderId = notificationToken.applicationId, - client = clientId.value, - token = notificationToken.token, - transport = notificationToken.transport - ) + senderId = notificationToken.applicationId, + client = clientId.value, + token = notificationToken.token, + transport = notificationToken.transport ) } .onFailure { kaliumLogger.i( "$TAG Error while registering Firebase token " + - "for the client: ${clientId.toString().obfuscateId()} error: $it" + "for the client: ${ + clientId.toString().obfuscateId() + } error: $it" ) } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenUseCase.kt new file mode 100644 index 00000000000..653308ec700 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenUseCase.kt @@ -0,0 +1,93 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.notificationToken + +import com.wire.kalium.logic.configuration.notification.NotificationTokenRepository +import com.wire.kalium.logic.data.client.ClientRepository +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.getOrNull +import com.wire.kalium.logic.functional.nullableFold + +/** + * Sends to the API locally stored FCM push token + */ +interface SendFCMTokenUseCase { + suspend operator fun invoke(): Either +} + +data class SendFCMTokenError( + val status: Reason, + val error: String? = null, +) { + enum class Reason { + CANT_GET_CLIENT_ID, CANT_GET_NOTIFICATION_TOKEN, CANT_REGISTER_TOKEN, + } +} + +class SendFCMTokenToAPIUseCaseImpl internal constructor( + private val currentClientIdProvider: CurrentClientIdProvider, + private val clientRepository: ClientRepository, + private val notificationTokenRepository: NotificationTokenRepository, +) : SendFCMTokenUseCase { + + override suspend fun invoke(): Either { + val clientIdResult = currentClientIdProvider() + val notificationTokenResult = notificationTokenRepository.getNotificationToken() + + val error: SendFCMTokenError? = clientIdResult.nullableFold( + { SendFCMTokenError(SendFCMTokenError.Reason.CANT_GET_CLIENT_ID, it.toString()) }, + { + notificationTokenResult.nullableFold( + { + SendFCMTokenError( + SendFCMTokenError.Reason.CANT_GET_NOTIFICATION_TOKEN, + it.toString() + ) + }, + { null } + ) + } + ) + + if (error != null) { + return Either.Left(error) + } + + val clientId = clientIdResult.getOrNull()!!.value + val notificationToken = notificationTokenResult.getOrNull()!! + + return clientRepository.registerToken( + senderId = notificationToken.applicationId, + client = clientId, + token = notificationToken.token, + transport = notificationToken.transport + ).fold( + { + Either.Left( + SendFCMTokenError( + SendFCMTokenError.Reason.CANT_REGISTER_TOKEN, + it.toString() + ) + ) + }, + { Either.Right(Unit) } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt new file mode 100644 index 00000000000..be386f4e509 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCase.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +@file:Suppress("konsist.useCasesShouldNotAccessNetworkLayerDirectly") + +package com.wire.kalium.logic.feature.personaltoteamaccount + +import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.network.session.SessionManager + +/** + * Use case to check if the user can migrate from personal to team account. + * The user can migrate if the user is not in a team and the server supports the migration. + */ +interface CanMigrateFromPersonalToTeamUseCase { + suspend operator fun invoke(): Boolean +} + +internal class CanMigrateFromPersonalToTeamUseCaseImpl( + val sessionManager: SessionManager, + val serverConfigRepository: ServerConfigRepository, + val selfTeamIdProvider: SelfTeamIdProvider +) : CanMigrateFromPersonalToTeamUseCase { + override suspend fun invoke(): Boolean { + val commonApiVersion = sessionManager.serverConfig().metaData.commonApiVersion.version + val minApi = serverConfigRepository.minimumApiVersionForPersonalToTeamAccountMigration + return selfTeamIdProvider().fold( + { false }, + { teamId -> teamId == null && commonApiVersion >= minApi } + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParser.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParser.kt index 68ef32e854e..e20aa60d0aa 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParser.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParser.kt @@ -35,7 +35,7 @@ class FederatedSearchParser( private val cachedIsFederationEnabled = AtomicReference(null) private val mutex = Mutex() - suspend operator fun invoke(searchQuery: String): Result { + suspend operator fun invoke(searchQuery: String, isOtherDomainAllowed: Boolean): Result { val isFederated = cachedIsFederationEnabled.get() ?: mutex.withLock { @@ -51,7 +51,7 @@ class FederatedSearchParser( } return when { - !isFederated -> Result(searchQuery, selfUserId.domain) + !isFederated || !isOtherDomainAllowed -> Result(searchQuery, selfUserId.domain) searchQuery.matches(regex) -> { val domain = searchQuery.substringAfterLast(DOMAIN_SEPARATOR) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt new file mode 100644 index 00000000000..964b17c01e8 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCase.kt @@ -0,0 +1,82 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.search + +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext + +/** + * Check if FederatedSearchIsAllowed according to MLS configuration of the backend + * and the conversation protocol if a [ConversationId] is provided. + */ +interface IsFederationSearchAllowedUseCase { + suspend operator fun invoke(conversationId: ConversationId?): Boolean +} + +@Suppress("FunctionNaming") +internal fun IsFederationSearchAllowedUseCase( + mlsPublicKeysRepository: MLSPublicKeysRepository, + getDefaultProtocol: GetDefaultProtocolUseCase, + getConversationProtocolInfo: GetConversationProtocolInfoUseCase, + dispatcher: KaliumDispatcher = KaliumDispatcherImpl +) = object : IsFederationSearchAllowedUseCase { + + override suspend operator fun invoke(conversationId: ConversationId?): Boolean = withContext(dispatcher.io) { + val isMlsConfiguredForBackend = hasMLSKeysConfiguredForBackend() + when (isMlsConfiguredForBackend) { + true -> isConversationProtocolAbleToFederate(conversationId) + false -> true + } + } + + private suspend fun hasMLSKeysConfiguredForBackend(): Boolean { + return when (val mlsKeysResult = mlsPublicKeysRepository.getKeys()) { + is Either.Left -> false + is Either.Right -> { + val mlsKeys: MLSPublicKeys = mlsKeysResult.value + mlsKeys.removal != null && mlsKeys.removal?.isNotEmpty() == true + } + } + } + + /** + * MLS is enabled, then we need to check if the protocol for the conversation is able to federate. + */ + private suspend fun isConversationProtocolAbleToFederate(conversationId: ConversationId?): Boolean { + val isProteusTeam = getDefaultProtocol() == SupportedProtocol.PROTEUS + val isOtherDomainAllowed: Boolean = conversationId?.let { + when (val result = getConversationProtocolInfo(it)) { + is GetConversationProtocolInfoUseCase.Result.Failure -> !isProteusTeam + + is GetConversationProtocolInfoUseCase.Result.Success -> + !isProteusTeam && result.protocolInfo !is Conversation.ProtocolInfo.Proteus + } + } ?: !isProteusTeam + return isOtherDomainAllowed + } + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt index 662514c9447..bb5c11a9ff9 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/search/SearchScope.kt @@ -17,12 +17,19 @@ */ package com.wire.kalium.logic.feature.search +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.data.publicuser.SearchUserRepository import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +@Suppress("LongParameterList") class SearchScope internal constructor( + private val mlsPublicKeysRepository: MLSPublicKeysRepository, + private val getDefaultProtocol: GetDefaultProtocolUseCase, + private val getConversationProtocolInfo: GetConversationProtocolInfoUseCase, private val searchUserRepository: SearchUserRepository, private val sessionRepository: SessionRepository, private val selfUserId: UserId, @@ -42,4 +49,7 @@ class SearchScope internal constructor( kaliumConfigs.maxRemoteSearchResultCount ) val federatedSearchParser: FederatedSearchParser get() = FederatedSearchParser(sessionRepository, selfUserId) + + val isFederationSearchAllowedUseCase: IsFederationSearchAllowedUseCase + get() = IsFederationSearchAllowedUseCase(mlsPublicKeysRepository, getDefaultProtocol, getConversationProtocolInfo) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt new file mode 100644 index 00000000000..1e2538afe4d --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCase.kt @@ -0,0 +1,46 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.functional.getOrElse +import com.wire.kalium.logic.functional.map +import com.wire.kalium.util.DateTimeUtil +import kotlinx.datetime.Instant + +/** + * Use case that returns [Boolean] if user should be asked for a feedback about call quality or not. + */ +interface ShouldAskCallFeedbackUseCase { + /** + * @return [Boolean] if user should be asked for a feedback about call quality or not. + */ + suspend operator fun invoke(): Boolean +} + +@Suppress("FunctionNaming") +internal fun ShouldAskCallFeedbackUseCase( + userConfigRepository: UserConfigRepository +) = object : ShouldAskCallFeedbackUseCase { + + override suspend fun invoke(): Boolean = + userConfigRepository.getNextTimeForCallFeedback().map { + it > 0L && DateTimeUtil.currentInstant() > Instant.fromEpochMilliseconds(it) + }.getOrElse(true) + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCase.kt new file mode 100644 index 00000000000..e364ad5f49d --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCase.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user + +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.feature.user.UpdateNextTimeCallFeedbackUseCase.Companion.askingForFeedbackPeriod +import com.wire.kalium.util.DateTimeUtil +import kotlin.time.Duration.Companion.days + +/** + * Use case that updates next time when user should be asked for a feedback about call quality. + */ +interface UpdateNextTimeCallFeedbackUseCase { + /** + * Update next time when user should be asked for a feedback about call quality. + * @param neverAskAgain [Boolean] if user checked "never ask me again" + */ + suspend operator fun invoke(neverAskAgain: Boolean) + + companion object { + val askingForFeedbackPeriod = 3.days + } +} + +@Suppress("FunctionNaming") +internal fun UpdateNextTimeCallFeedbackUseCase( + userConfigRepository: UserConfigRepository +) = object : UpdateNextTimeCallFeedbackUseCase { + + override suspend fun invoke(neverAskAgain: Boolean) { + val nextTimestamp = if (neverAskAgain) -1L + else DateTimeUtil.currentInstant().plus(askingForFeedbackPeriod).toEpochMilliseconds() + + userConfigRepository.updateNextTimeForCallFeedback(nextTimestamp) + } + +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt index b7fc56a5503..8bbc0e12e04 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCase.kt @@ -25,6 +25,8 @@ import com.wire.kalium.logic.data.user.UserRepository import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.map +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.withContext import okio.Path interface UploadUserAvatarUseCase { @@ -40,17 +42,20 @@ interface UploadUserAvatarUseCase { internal class UploadUserAvatarUseCaseImpl( private val userDataSource: UserRepository, - private val assetDataSource: AssetRepository + private val assetDataSource: AssetRepository, + private val dispatcher: KaliumDispatcherImpl = KaliumDispatcherImpl ) : UploadUserAvatarUseCase { override suspend operator fun invoke(imageDataPath: Path, imageDataSize: Long): UploadAvatarResult { - return assetDataSource.uploadAndPersistPublicAsset("image/jpg", imageDataPath, imageDataSize).flatMap { asset -> - userDataSource.updateSelfUser(newAssetId = asset.key).map { asset } - }.fold({ - UploadAvatarResult.Failure(it) - }) { updatedAsset -> - UploadAvatarResult.Success(UserAssetId(updatedAsset.key, updatedAsset.domain)) - } // TODO(assets): remove old assets, non blocking this response, as will imply deleting locally and remotely + return withContext(dispatcher.io) { + assetDataSource.uploadAndPersistPublicAsset("image/jpg", imageDataPath, imageDataSize).flatMap { asset -> + userDataSource.updateSelfUser(newAssetId = asset.key).map { asset } + }.fold({ + UploadAvatarResult.Failure(it) + }) { updatedAsset -> + UploadAvatarResult.Success(UserAssetId(updatedAsset.key, updatedAsset.domain)) + } // TODO(assets): remove old assets, non blocking this response, as will imply deleting locally and remotely + } } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt index b81afca7eb1..9bae507ae54 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/UserScope.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -@file:Suppress("konsist.useCasesShouldNotAccessDaoLayerDirectly") +@file:Suppress("konsist.useCasesShouldNotAccessDaoLayerDirectly", "konsist.useCasesShouldNotAccessNetworkLayerDirectly") package com.wire.kalium.logic.feature.user @@ -24,12 +24,14 @@ import com.wire.kalium.logic.configuration.UserConfigRepository import com.wire.kalium.logic.configuration.server.ServerConfigRepository import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.client.ClientRepository +import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.JoinExistingMLSConversationsUseCase import com.wire.kalium.logic.data.conversation.MLSConversationRepository import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository import com.wire.kalium.logic.data.e2ei.E2EIRepository import com.wire.kalium.logic.data.e2ei.RevocationListChecker import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.properties.UserPropertyRepository import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.sync.IncrementalSyncRepository @@ -49,8 +51,7 @@ import com.wire.kalium.logic.feature.auth.ValidateUserHandleUseCaseImpl import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollmentImpl import com.wire.kalium.logic.feature.conversation.GetAllContactsNotInConversationUseCase -import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorker -import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorkerImpl +import com.wire.kalium.logic.feature.e2ei.SyncCertificateRevocationListUseCase import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCaseImpl import com.wire.kalium.logic.feature.e2ei.usecase.GetMLSClientIdentityUseCase @@ -67,6 +68,8 @@ import com.wire.kalium.logic.feature.featureConfig.FeatureFlagSyncWorkerImpl import com.wire.kalium.logic.feature.featureConfig.FeatureFlagsSyncWorker import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCase +import com.wire.kalium.logic.feature.personaltoteamaccount.CanMigrateFromPersonalToTeamUseCaseImpl import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCase import com.wire.kalium.logic.feature.publicuser.GetAllContactsUseCaseImpl import com.wire.kalium.logic.feature.publicuser.GetKnownUserUseCase @@ -81,6 +84,7 @@ import com.wire.kalium.logic.feature.user.typingIndicator.ObserveTypingIndicator import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCase import com.wire.kalium.logic.feature.user.typingIndicator.PersistTypingIndicatorStatusConfigUseCaseImpl import com.wire.kalium.logic.sync.SyncManager +import com.wire.kalium.network.session.SessionManager import com.wire.kalium.persistence.dao.MetadataDAO @Suppress("LongParameterList") @@ -100,6 +104,7 @@ class UserScope internal constructor( private val clientIdProvider: CurrentClientIdProvider, private val e2EIRepository: E2EIRepository, private val mlsConversationRepository: MLSConversationRepository, + private val conversationRepository: ConversationRepository, private val isSelfATeamMember: IsSelfATeamMemberUseCase, private val updateSelfUserSupportedProtocolsUseCase: UpdateSelfUserSupportedProtocolsUseCase, private val clientRepository: ClientRepository, @@ -108,6 +113,8 @@ class UserScope internal constructor( private val isE2EIEnabledUseCase: IsE2EIEnabledUseCase, private val certificateRevocationListRepository: CertificateRevocationListRepository, private val incrementalSyncRepository: IncrementalSyncRepository, + private val sessionManager: SessionManager, + private val selfTeamIdProvider: SelfTeamIdProvider, private val checkRevocationList: RevocationListChecker, private val syncFeatureConfigs: SyncFeatureConfigsUseCase, private val userScopedLogger: KaliumLogger @@ -133,7 +140,8 @@ class UserScope internal constructor( val getUserE2eiCertificateStatus: IsOtherUserE2EIVerifiedUseCase get() = IsOtherUserE2EIVerifiedUseCaseImpl( mlsConversationRepository = mlsConversationRepository, - isE2EIEnabledUseCase = isE2EIEnabledUseCase + isE2EIEnabledUseCase = isE2EIEnabledUseCase, + userRepository = userRepository ) val getUserE2eiCertificates: GetUserE2eiCertificatesUseCase get() = GetUserE2eiCertificatesUseCaseImpl( @@ -142,7 +150,8 @@ class UserScope internal constructor( ) val getMembersE2EICertificateStatuses: GetMembersE2EICertificateStatusesUseCase get() = GetMembersE2EICertificateStatusesUseCaseImpl( - mlsConversationRepository = mlsConversationRepository + mlsConversationRepository = mlsConversationRepository, + conversationRepository = conversationRepository ) val deleteAsset: DeleteAssetUseCase get() = DeleteAssetUseCaseImpl(assetRepository) val setUserHandle: SetUserHandleUseCase get() = SetUserHandleUseCase(accountRepository, validateUserHandleUseCase, syncManager) @@ -205,14 +214,14 @@ class UserScope internal constructor( kaliumLogger = userScopedLogger, ) - val certificateRevocationListCheckWorker: CertificateRevocationListCheckWorker by lazy { - CertificateRevocationListCheckWorkerImpl( - certificateRevocationListRepository = certificateRevocationListRepository, - incrementalSyncRepository = incrementalSyncRepository, - revocationListChecker = checkRevocationList, - kaliumLogger = userScopedLogger, - ) - } + val syncCertificateRevocationListUseCase: SyncCertificateRevocationListUseCase + get() = + SyncCertificateRevocationListUseCase( + certificateRevocationListRepository = certificateRevocationListRepository, + incrementalSyncRepository = incrementalSyncRepository, + revocationListChecker = checkRevocationList, + kaliumLogger = userScopedLogger, + ) val featureFlagsSyncWorker: FeatureFlagsSyncWorker by lazy { FeatureFlagSyncWorkerImpl( @@ -221,4 +230,11 @@ class UserScope internal constructor( kaliumLogger = userScopedLogger, ) } + val isPersonalToTeamAccountSupportedByBackend: CanMigrateFromPersonalToTeamUseCase by lazy { + CanMigrateFromPersonalToTeamUseCaseImpl( + sessionManager = sessionManager, + serverConfigRepository = serverConfigRepository, + selfTeamIdProvider = selfTeamIdProvider + ) + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt new file mode 100644 index 00000000000..5f2ca1c35bf --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCase.kt @@ -0,0 +1,101 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user.migration + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.network.exceptions.KaliumException + +/** + * Use case to migrate user personal account to team account. + * This needs at least API V7 to work. + */ +interface MigrateFromPersonalToTeamUseCase { + suspend operator fun invoke(teamName: String): MigrateFromPersonalToTeamResult +} + +sealed class MigrateFromPersonalToTeamResult { + data object Success : MigrateFromPersonalToTeamResult() + data class Error(val failure: MigrateFromPersonalToTeamFailure) : + MigrateFromPersonalToTeamResult() +} + +sealed class MigrateFromPersonalToTeamFailure { + + data class UnknownError(val coreFailure: CoreFailure) : MigrateFromPersonalToTeamFailure() + class UserAlreadyInTeam : MigrateFromPersonalToTeamFailure() { + companion object { + const val ERROR_LABEL = "user-already-in-a-team" + } + } + + data object NoNetwork : MigrateFromPersonalToTeamFailure() +} + +internal class MigrateFromPersonalToTeamUseCaseImpl internal constructor( + private val selfUserId: UserId, + private val userRepository: UserRepository, + private val invalidateTeamId: () -> Unit +) : MigrateFromPersonalToTeamUseCase { + override suspend operator fun invoke( + teamName: String, + ): MigrateFromPersonalToTeamResult { + return userRepository.migrateUserToTeam(teamName).fold({ error -> + return when (error) { + is NetworkFailure.ServerMiscommunication -> { + if (error.kaliumException is KaliumException.InvalidRequestError) { + val response = error.kaliumException.errorResponse + if (response.label == MigrateFromPersonalToTeamFailure.UserAlreadyInTeam.ERROR_LABEL) { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UserAlreadyInTeam() + ) + } else { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError(error) + ) + } + } else { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError(error) + ) + } + } + + is NetworkFailure.NoNetworkConnection -> { + MigrateFromPersonalToTeamResult.Error(MigrateFromPersonalToTeamFailure.NoNetwork) + } + + else -> { + MigrateFromPersonalToTeamResult.Error( + MigrateFromPersonalToTeamFailure.UnknownError( + error + ) + ) + } + } + }, { user -> + userRepository.updateTeamId(selfUserId, TeamId(user.teamId)) + invalidateTeamId() + MigrateFromPersonalToTeamResult.Success + }) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/FeatureSupportImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/FeatureSupportImpl.kt index cb11a6b34e1..fd37ac693ae 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/FeatureSupportImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/FeatureSupportImpl.kt @@ -24,9 +24,8 @@ interface FeatureSupport { @Suppress("MagicNumber") class FeatureSupportImpl( - kaliumConfigs: KaliumConfigs, apiVersion: Int ) : FeatureSupport { - override val isMLSSupported: Boolean = kaliumConfigs.isMLSSupportEnabled && apiVersion >= 5 + override val isMLSSupported: Boolean = apiVersion >= 6 } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt index f285c56623d..1ca24af381a 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/featureFlags/KaliumConfigs.kt @@ -26,7 +26,6 @@ import kotlin.time.Duration.Companion.hours data class KaliumConfigs( val forceConstantBitrateCalls: Boolean = false, val fileRestrictionState: BuildFileRestrictionState = BuildFileRestrictionState.NoRestriction, - var isMLSSupportEnabled: Boolean = true, // Disabling db-encryption will crash on android-api level below 30 val shouldEncryptData: Boolean = true, val encryptProteusStorage: Boolean = false, @@ -43,13 +42,15 @@ data class KaliumConfigs( val certPinningConfig: Map> = emptyMap(), val mockedRequests: List? = null, val mockNetworkStateObserver: NetworkStateObserver? = null, + val mockedWebSocket: Boolean = false, // Interval between attempts to advance the proteus to MLS migration val mlsMigrationInterval: Duration = 24.hours, // limit for the number of team members to fetch during slow sync // if null there will be no limit and all team members will be fetched // preferably it should be a multiple of TeamRepository.FETCH_TEAM_MEMBER_PAGE_SIZE val limitTeamMembersFetchDuringSlowSync: Int? = null, - val maxRemoteSearchResultCount: Int = 30 + val maxRemoteSearchResultCount: Int = 30, + val enableCalling: Boolean = true ) sealed interface BuildFileRestrictionState { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt index fb655297cb8..7249c4da12f 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/network/SessionManagerImpl.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.withContext import kotlin.coroutines.CoroutineContext +// TODO: Move this class to logic module @OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") class SessionManagerImpl internal constructor( diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCase.kt index 74c863ddbfe..e2558d788ce 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCase.kt @@ -46,13 +46,13 @@ internal class ObserveSyncStateUseCaseImpl internal constructor( override operator fun invoke(): Flow = combine(slowSyncRepository.slowSyncStatus, incrementalSyncRepository.incrementalSyncState) { slowStatus, incrementalStatus -> when (slowStatus) { - is SlowSyncStatus.Failed -> SyncState.Failed(slowStatus.failure) + is SlowSyncStatus.Failed -> SyncState.Failed(slowStatus.failure, slowStatus.retryDelay) is SlowSyncStatus.Ongoing -> SyncState.SlowSync SlowSyncStatus.Pending -> SyncState.Waiting SlowSyncStatus.Complete -> { when (incrementalStatus) { IncrementalSyncStatus.Live -> SyncState.Live - is IncrementalSyncStatus.Failed -> SyncState.Failed(incrementalStatus.failure) + is IncrementalSyncStatus.Failed -> SyncState.Failed(incrementalStatus.failure, incrementalStatus.retryDelay) IncrementalSyncStatus.FetchingPendingEvents -> SyncState.GatheringPendingEvents IncrementalSyncStatus.Pending -> SyncState.GatheringPendingEvents } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt new file mode 100644 index 00000000000..145a813480e --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/SyncManagerLogger.kt @@ -0,0 +1,99 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.sync + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logger.KaliumLogLevel +import com.wire.kalium.logger.KaliumLogger +import com.wire.kalium.logic.logStructuredJson +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration + +/** + * Logs the sync process by providing structured logs. + * It logs the sync process start and completion with the syncId as a unique identifier. + */ +internal class SyncManagerLogger( + private val logger: KaliumLogger, + private val syncId: String, + private val syncType: SyncType, + private val syncStartedMoment: Instant +) { + + /** + * Logs the sync process start. + */ + fun logSyncStarted() { + logger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.SYNC).logStructuredJson( + level = KaliumLogLevel.INFO, + leadingMessage = "Started sync process", + jsonStringKeyValues = mapOf( + "syncMetadata" to mapOf( + "id" to syncId, + "status" to SyncStatus.STARTED.name, + "type" to syncType.name + ) + ) + ) + } + + /** + * Logs the sync process completion. + * Optionally, it can pass the duration of the sync process, + * useful for incremental sync that can happen between collecting states. + * + * @param duration optional the duration of the sync process. + */ + fun logSyncCompleted(duration: Duration = Clock.System.now() - syncStartedMoment) { + val logMap = mapOf( + "id" to syncId, + "status" to SyncStatus.COMPLETED.name, + "type" to syncType.name, + "performanceData" to mapOf("timeTakenInMillis" to duration.inWholeMilliseconds) + ) + + logger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.SYNC).logStructuredJson( + level = KaliumLogLevel.INFO, + leadingMessage = "Completed sync process", + jsonStringKeyValues = mapOf("syncMetadata" to logMap) + ) + } +} + +internal enum class SyncStatus { + STARTED, + COMPLETED +} + +internal enum class SyncType { + SLOW, + INCREMENTAL +} + +/** + * Provides a new [SyncManagerLogger] instance with the given parameters. + * @param syncType the [SyncType] that will log. + * @param syncId the unique identifier for the sync process. + * @param syncStartedMoment the moment when the sync process started. + */ +internal fun KaliumLogger.provideNewSyncManagerLogger( + syncType: SyncType, + syncId: String = uuid4().toString(), + syncStartedMoment: Instant = Clock.System.now() +) = SyncManagerLogger(this, syncId, syncType, syncStartedMoment) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventGatherer.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventGatherer.kt index 8e9ce00fb45..707cbcbcace 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventGatherer.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventGatherer.kt @@ -34,6 +34,8 @@ import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.KaliumSyncException +import com.wire.kalium.logic.util.ServerTimeHandler +import com.wire.kalium.logic.util.ServerTimeHandlerImpl import com.wire.kalium.network.api.base.authenticated.notification.WebSocketEvent import com.wire.kalium.network.exceptions.KaliumException import io.ktor.http.HttpStatusCode @@ -52,6 +54,7 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transformWhile +import kotlinx.datetime.toInstant /** * Responsible for fetching events from a remote source, orchestrating between events missed since @@ -77,6 +80,7 @@ internal interface EventGatherer { internal class EventGathererImpl( private val eventRepository: EventRepository, private val incrementalSyncRepository: IncrementalSyncRepository, + private val serverTimeHandler: ServerTimeHandler = ServerTimeHandlerImpl(), logger: KaliumLogger = kaliumLogger, ) : EventGatherer { @@ -165,6 +169,7 @@ internal class EventGathererImpl( private suspend fun FlowCollector.onWebSocketOpen() { logger.i("Websocket Open") + handleTimeDrift() eventRepository .pendingEvents() .onEach { result -> @@ -181,6 +186,12 @@ internal class EventGathererImpl( _currentSource.value = EventSource.LIVE } + private suspend fun handleTimeDrift() { + eventRepository.fetchServerTime()?.let { + serverTimeHandler.computeTimeOffset(it.toInstant().epochSeconds) + } + } + private fun throwPendingEventException(failure: CoreFailure) { val networkCause = (failure as? NetworkFailure.ServerMiscommunication)?.rootCause val isEventNotFound = networkCause is KaliumException.InvalidRequestError diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessor.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessor.kt index 5a4be8d4395..666a8fce093 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessor.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessor.kt @@ -36,6 +36,10 @@ import com.wire.kalium.logic.sync.receiver.UserEventReceiver import com.wire.kalium.logic.sync.receiver.UserPropertiesEventReceiver import com.wire.kalium.logic.util.EventLoggingStatus import com.wire.kalium.logic.util.createEventProcessingLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext /** * Handles incoming events from remote. @@ -71,6 +75,7 @@ internal class EventProcessorImpl( private val featureConfigEventReceiver: FeatureConfigEventReceiver, private val userPropertiesEventReceiver: UserPropertiesEventReceiver, private val federationEventReceiver: FederationEventReceiver, + private val processingScope: CoroutineScope, logger: KaliumLogger = kaliumLogger, ) : EventProcessor { @@ -80,13 +85,23 @@ internal class EventProcessorImpl( override var disableEventProcessing: Boolean = false - override suspend fun processEvent(eventEnvelope: EventEnvelope): Either { + override suspend fun processEvent(eventEnvelope: EventEnvelope): Either = processingScope.async { val (event, deliveryInfo) = eventEnvelope if (disableEventProcessing) { logger.w("Skipping processing of ${event.toLogString()} due to debug option") - return Either.Right(Unit) + Either.Right(Unit) + } else { + withContext(NonCancellable) { + doProcess(event, deliveryInfo, eventEnvelope) + } } + }.await() + private suspend fun doProcess( + event: Event, + deliveryInfo: EventDeliveryInfo, + eventEnvelope: EventEnvelope + ): Either { return when (event) { is Event.Conversation -> conversationEventReceiver.onEvent(event, deliveryInfo) is Event.User -> userEventReceiver.onEvent(event, deliveryInfo) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt index 63d79bdda58..38e2ec5c2cc 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/incremental/IncrementalSyncManager.kt @@ -18,6 +18,7 @@ package com.wire.kalium.logic.sync.incremental +import com.benasher44.uuid.uuid4 import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logger.KaliumLogger.Companion.ApplicationFlow.SYNC import com.wire.kalium.logic.data.event.Event @@ -28,6 +29,8 @@ import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.SyncExceptionHandler +import com.wire.kalium.logic.sync.SyncType +import com.wire.kalium.logic.sync.provideNewSyncManagerLogger import com.wire.kalium.logic.sync.slow.SlowSyncManager import com.wire.kalium.logic.util.ExponentialDurationHelper import com.wire.kalium.logic.util.ExponentialDurationHelperImpl @@ -41,12 +44,15 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.cancellable +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.runningFold import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select +import kotlinx.datetime.Clock import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -111,10 +117,12 @@ internal class IncrementalSyncManager( onFailure = { failure -> logger.i("$TAG ExceptionHandler error $failure") syncScope.launch { - incrementalSyncRepository.updateIncrementalSyncState(IncrementalSyncStatus.Failed(failure)) + val delay = exponentialDurationHelper.next() + incrementalSyncRepository.updateIncrementalSyncState( + IncrementalSyncStatus.Failed(failure, delay) + ) incrementalSyncRecoveryHandler.recover(failure = failure) { - val delay = exponentialDurationHelper.next() logger.i("$TAG Triggering delay($delay) and waiting for reconnection") delayUntilConnectedOrPolicyUpgrade(delay) logger.i("$TAG Delay and waiting for connection finished - retrying") @@ -179,16 +187,25 @@ internal class IncrementalSyncManager( incrementalSyncWorker .processEventsWhilePolicyAllowsFlow() .cancellable() - .collect { - val newState = when (it) { - EventSource.PENDING -> IncrementalSyncStatus.FetchingPendingEvents + .runningFold(uuid4().toString() to Clock.System.now()) { syncData, eventSource -> + val syncLogger = kaliumLogger.provideNewSyncManagerLogger(SyncType.INCREMENTAL, syncData.first) + val newState = when (eventSource) { + EventSource.PENDING -> { + syncLogger.logSyncStarted() + IncrementalSyncStatus.FetchingPendingEvents + } + EventSource.LIVE -> { + syncLogger.logSyncCompleted(duration = Clock.System.now() - syncData.second) exponentialDurationHelper.reset() IncrementalSyncStatus.Live } } incrementalSyncRepository.updateIncrementalSyncState(newState) - } + + // when the source is LIVE, we need to generate a new syncId since it means the previous one is done + if (eventSource == EventSource.LIVE) uuid4().toString() to Clock.System.now() else syncData + }.collect() incrementalSyncRepository.updateIncrementalSyncState(IncrementalSyncStatus.Pending) logger.i("$TAG IncrementalSync stopped.") } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiver.kt index 6381ff6c8ca..d6e5ae16ed6 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiver.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.sync.receiver.conversation.AccessUpdateEventHandler import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler import com.wire.kalium.logic.sync.receiver.conversation.DeletedConversationEventHandler import com.wire.kalium.logic.sync.receiver.conversation.MLSWelcomeEventHandler @@ -56,7 +57,8 @@ internal class ConversationEventReceiverImpl( private val codeUpdatedHandler: CodeUpdatedHandler, private val codeDeletedHandler: CodeDeletedHandler, private val typingIndicatorHandler: TypingIndicatorHandler, - private val protocolUpdateEventHandler: ProtocolUpdateEventHandler + private val protocolUpdateEventHandler: ProtocolUpdateEventHandler, + private val accessUpdateEventHandler: AccessUpdateEventHandler ) : ConversationEventReceiver { override suspend fun onEvent(event: Event.Conversation, deliveryInfo: EventDeliveryInfo): Either { // TODO: Make sure errors are accounted for by each handler. @@ -108,11 +110,7 @@ internal class ConversationEventReceiverImpl( Either.Right(Unit) } - is Event.Conversation.AccessUpdate -> { - /* no-op */ - Either.Right(Unit) - } - + is Event.Conversation.AccessUpdate -> accessUpdateEventHandler.handle(event) is Event.Conversation.ConversationMessageTimer -> conversationMessageTimerEventHandler.handle(event) is Event.Conversation.CodeDeleted -> codeDeletedHandler.handle(event) is Event.Conversation.CodeUpdated -> codeUpdatedHandler.handle(event) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt index 3967f34d8ed..31ff54e6e9b 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiver.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.sync.receiver import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo import com.wire.kalium.logic.functional.Either @@ -31,7 +32,8 @@ import com.wire.kalium.logic.util.createEventProcessingLogger internal interface UserPropertiesEventReceiver : EventReceiver internal class UserPropertiesEventReceiverImpl internal constructor( - private val userConfigRepository: UserConfigRepository + private val userConfigRepository: UserConfigRepository, + private val conversationFolderRepository: ConversationFolderRepository ) : UserPropertiesEventReceiver { override suspend fun onEvent(event: Event.UserProperty, deliveryInfo: EventDeliveryInfo): Either { @@ -43,6 +45,10 @@ internal class UserPropertiesEventReceiverImpl internal constructor( is Event.UserProperty.TypingIndicatorModeSet -> { handleTypingIndicatorMode(event) } + + is Event.UserProperty.FoldersUpdate -> { + handleFoldersUpdate(event) + } } } @@ -65,4 +71,14 @@ internal class UserPropertiesEventReceiverImpl internal constructor( .onSuccess { logger.logSuccess() } .onFailure { logger.logFailure(it) } } + + private suspend fun handleFoldersUpdate( + event: Event.UserProperty.FoldersUpdate + ): Either { + val logger = kaliumLogger.createEventProcessingLogger(event) + return conversationFolderRepository + .updateConversationFolders(event.folders) + .onSuccess { logger.logSuccess() } + .onFailure { logger.logFailure(it) } + } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandler.kt index 57ac934a25b..b99adccea0e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandler.kt @@ -24,11 +24,12 @@ import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.data.message.MessageRepository import com.wire.kalium.logic.data.message.PersistMessageUseCase -import com.wire.kalium.logic.data.message.hasValidData -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCase +import com.wire.kalium.logic.data.message.getType +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase import com.wire.kalium.logic.functional.onFailure import com.wire.kalium.logic.functional.onSuccess import com.wire.kalium.logic.kaliumLogger +import com.wire.kalium.logic.sync.receiver.conversation.message.hasValidData internal interface AssetMessageHandler { suspend fun handle(message: Message.Regular) @@ -38,30 +39,54 @@ internal class AssetMessageHandlerImpl( private val messageRepository: MessageRepository, private val persistMessage: PersistMessageUseCase, private val userConfigRepository: UserConfigRepository, - private val validateAssetMimeTypeUseCase: ValidateAssetMimeTypeUseCase + private val validateAssetMimeTypeUseCase: ValidateAssetFileTypeUseCase ) : AssetMessageHandler { override suspend fun handle(message: Message.Regular) { - val messageContent = message.content - if (messageContent !is MessageContent.Asset) { + if (message.content !is MessageContent.Asset) { kaliumLogger.e("The asset message trying to be processed has invalid content data") return } + + val messageContent = message.content as MessageContent.Asset + userConfigRepository.isFileSharingEnabled().onSuccess { val isThisAssetAllowed = when (it.state) { - FileSharingStatus.Value.Disabled -> false - FileSharingStatus.Value.EnabledAll -> true + FileSharingStatus.Value.Disabled -> AssetRestrictionContinuationStrategy.Restrict + FileSharingStatus.Value.EnabledAll -> AssetRestrictionContinuationStrategy.Continue - is FileSharingStatus.Value.EnabledSome -> validateAssetMimeTypeUseCase( - messageContent.value.mimeType, - it.state.allowedType - ) + is FileSharingStatus.Value.EnabledSome -> { + // If the asset message is missing the name, but it does have full + // asset data then we can not decide now if it is allowed or not + // it is safe to continue and the code later will check the original + // asset message and decide if it is allowed or not + if (validateAssetMimeTypeUseCase( + fileName = messageContent.value.name, + mimeType = messageContent.value.mimeType, + allowedExtension = it.state.allowedType + ) + ) { + AssetRestrictionContinuationStrategy.Continue + } else { + if (messageContent.value.name.isNullOrEmpty() && messageContent.value.isAssetDataComplete) { + AssetRestrictionContinuationStrategy.RestrictIfThereIsNotOldMessageWithTheSameAssetID + } else { + AssetRestrictionContinuationStrategy.Restrict + } + } + } } - if (isThisAssetAllowed) { - processNonRestrictedAssetMessage(message, messageContent) - } else { - persistRestrictedAssetMessage(message, messageContent) + when (isThisAssetAllowed) { + AssetRestrictionContinuationStrategy.Continue -> processNonRestrictedAssetMessage(message, messageContent, false) + AssetRestrictionContinuationStrategy.RestrictIfThereIsNotOldMessageWithTheSameAssetID -> processNonRestrictedAssetMessage( + message, + messageContent, + true + ) + + AssetRestrictionContinuationStrategy.Restrict -> persistRestrictedAssetMessage(message, messageContent) + } } } @@ -77,23 +102,34 @@ internal class AssetMessageHandlerImpl( persistMessage(newMessage) } - private suspend fun processNonRestrictedAssetMessage(processedMessage: Message.Regular, assetContent: MessageContent.Asset) { + private suspend fun processNonRestrictedAssetMessage( + processedMessage: Message.Regular, + assetContent: MessageContent.Asset, + restrictIfNotAFollowUpMessage: Boolean + ) { messageRepository.getMessageById(processedMessage.conversationId, processedMessage.id).onFailure { // No asset message was received previously, so just persist the preview of the asset message // Web/Mac clients split the asset message delivery into 2. One with the preview metadata (assetName, assetSize...) and // with empty encryption keys and the second with empty metadata but all the correct encryption keys. We just want to // hide the preview of generic asset messages with empty encryption keys as a way to avoid user interaction with them. - val initialMessage = processedMessage.copy( - visibility = if (assetContent.value.shouldBeDisplayed) Message.Visibility.VISIBLE else Message.Visibility.HIDDEN - ) - persistMessage(initialMessage) + + if (restrictIfNotAFollowUpMessage) { + persistRestrictedAssetMessage(processedMessage, assetContent) + } else { + val initialMessage = processedMessage.copy( + visibility = if (assetContent.value.isAssetDataComplete) Message.Visibility.VISIBLE else Message.Visibility.HIDDEN + ) + persistMessage(initialMessage) + } }.onSuccess { persistedMessage -> val validDecryptionKeys = assetContent.value.remoteData // Check the second asset message is from the same original sender if (isSenderVerified(persistedMessage, processedMessage) && persistedMessage is Message.Regular) { // The second asset message received from Web/Mac clients contains the full asset decryption keys, so we need to update // the preview message persisted previously with the rest of the data - persistMessage(updateAssetMessageWithDecryptionKeys(persistedMessage, validDecryptionKeys)) + updateAssetMessageWithDecryptionKeys(persistedMessage, validDecryptionKeys)?.let { + persistMessage(it) + } } else { kaliumLogger.e("The previously persisted message has a different sender id than the one we are trying to process") } @@ -106,8 +142,21 @@ internal class AssetMessageHandlerImpl( private fun updateAssetMessageWithDecryptionKeys( persistedMessage: Message.Regular, remoteData: AssetContent.RemoteData - ): Message.Regular { - val assetMessageContent = persistedMessage.content as MessageContent.Asset + ): Message.Regular? { + val assetMessageContent = when (persistedMessage.content) { + is MessageContent.Asset -> persistedMessage.content as MessageContent.Asset + is MessageContent.RestrictedAsset -> { + // original message was a restricted asset message, ignoring + return null + } + + is MessageContent.FailedDecryption, + is MessageContent.Knock, + is MessageContent.Location, + is MessageContent.Composite, + is MessageContent.Text, + is MessageContent.Unknown -> error("Invalid asset message content type ${persistedMessage.content.getType()}") + } // The message was previously received with just metadata info, so let's update it with the raw data info return persistedMessage.copy( content = assetMessageContent.copy( @@ -120,3 +169,9 @@ internal class AssetMessageHandlerImpl( ) } } + +private sealed interface AssetRestrictionContinuationStrategy { + data object Continue : AssetRestrictionContinuationStrategy + data object Restrict : AssetRestrictionContinuationStrategy + data object RestrictIfThereIsNotOldMessageWithTheSameAssetID : AssetRestrictionContinuationStrategy +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt new file mode 100644 index 00000000000..8671cdd280c --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateEventHandler.kt @@ -0,0 +1,49 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.sync.receiver.conversation + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.ConversationMapper +import com.wire.kalium.logic.data.event.Event +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.di.MapperProvider +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.wrapStorageRequest +import com.wire.kalium.persistence.dao.conversation.ConversationDAO + +interface AccessUpdateEventHandler { + suspend fun handle(event: Event.Conversation.AccessUpdate): Either +} + +@Suppress("FunctionNaming") +fun AccessUpdateEventHandler( + selfUserId: UserId, + conversationDAO: ConversationDAO, + conversationMapper: ConversationMapper = MapperProvider.conversationMapper(selfUserId) +) = object : AccessUpdateEventHandler { + + override suspend fun handle(event: Event.Conversation.AccessUpdate): Either = + wrapStorageRequest { + conversationDAO.updateAccess( + conversationID = event.conversationId.toDao(), + accessList = conversationMapper.fromModelToDAOAccess(event.access), + accessRoleList = conversationMapper.fromModelToDAOAccessRole(event.accessRole) + ) + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt index b1db1765b00..fea1b061e74 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/NewConversationEventHandler.kt @@ -59,10 +59,17 @@ internal class NewConversationEventHandlerImpl( .flatMap { isNewUnhandledConversation -> resolveConversationIfOneOnOne(selfUserTeamId, event) .flatMap { - conversationRepository.updateConversationModifiedDate(event.conversationId, DateTimeUtil.currentInstant()) + conversationRepository.updateConversationModifiedDate( + event.conversationId, + DateTimeUtil.currentInstant() + ) } .flatMap { - userRepository.fetchUsersIfUnknownByIds(event.conversation.members.otherMembers.map { it.id.toModel() }.toSet()) + userRepository.fetchUsersIfUnknownByIds( + event.conversation.members.otherMembers.map { + it.id.toModel() + }.toSet() + ) } .map { isNewUnhandledConversation } }.onSuccess { isNewUnhandledConversation -> @@ -73,9 +80,13 @@ internal class NewConversationEventHandlerImpl( } } - private suspend fun resolveConversationIfOneOnOne(selfUserTeamId: TeamId?, event: Event.Conversation.NewConversation) = + private suspend fun resolveConversationIfOneOnOne( + selfUserTeamId: TeamId?, + event: Event.Conversation.NewConversation + ) = if (event.conversation.toConversationType(selfUserTeamId) == ConversationEntity.Type.ONE_ON_ONE) { - val otherUserId = event.conversation.members.otherMembers.first().id.toModel() + val otherUserId = + event.conversation.members.otherMembers.first().id.toModel() oneOnOneResolver.resolveOneOnOneConversationWithUserId( userId = otherUserId, invalidateCurrentKnownProtocols = true @@ -94,13 +105,20 @@ internal class NewConversationEventHandlerImpl( event: Event.Conversation.NewConversation ) { if (isNewUnhandledConversation) { - newGroupConversationSystemMessagesCreator.conversationStarted(event.senderUserId, event.conversation, event.dateTime) + newGroupConversationSystemMessagesCreator.conversationStarted( + event.senderUserId, + event.conversation, + event.dateTime + ) newGroupConversationSystemMessagesCreator.conversationResolvedMembersAdded( event.conversationId.toDao(), event.conversation.members.otherMembers.map { it.id.toModel() }, event.dateTime ) - newGroupConversationSystemMessagesCreator.conversationReadReceiptStatus(event.conversation, event.dateTime) + newGroupConversationSystemMessagesCreator.conversationReadReceiptStatus( + event.conversation, + event.dateTime + ) newGroupConversationSystemMessagesCreator.conversationStartedUnverifiedWarning( event.conversation.id.toModel(), event.dateTime diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt index e9646942f4d..88f29b61b65 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/MLSMessageFailureHandler.kt @@ -42,6 +42,7 @@ internal object MLSMessageFailureHandler { is MLSFailure.StaleProposal -> MLSMessageFailureResolution.Ignore is MLSFailure.StaleCommit -> MLSMessageFailureResolution.Ignore is MLSFailure.MessageEpochTooOld -> MLSMessageFailureResolution.Ignore + is MLSFailure.InternalErrors -> MLSMessageFailureResolution.Ignore else -> MLSMessageFailureResolution.InformUser } } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt index ff54b70d4b1..c4ae6921d38 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandler.kt @@ -59,44 +59,46 @@ internal class NewMessageEventHandlerImpl( override suspend fun handleNewProteusMessage(event: Event.Conversation.NewMessage, deliveryInfo: EventDeliveryInfo) { val eventLogger = logger.createEventProcessingLogger(event) - proteusMessageUnpacker.unpackProteusMessage(event) - .onFailure { - val logMap = mapOf( - "event" to event.toLogMap(), - "errorInfo" to "$it", - "protocol" to "Proteus" - ) + proteusMessageUnpacker.unpackProteusMessage(event) { + processApplicationMessage(it, deliveryInfo) + it + }.onSuccess { + eventLogger.logSuccess( + "protocol" to "Proteus", + "messageType" to it.messageTypeDescription, + ) + }.onFailure { + val logMap = mapOf( + "event" to event.toLogMap(), + "errorInfo" to "$it", + "protocol" to "Proteus" + ) - if (it is ProteusFailure && it.proteusException.code == ProteusException.Code.DUPLICATE_MESSAGE) { - logger.i("Ignoring duplicate event: ${logMap.toJsonElement()}") - return - } + if (it is ProteusFailure && it.proteusException.code == ProteusException.Code.DUPLICATE_MESSAGE) { + logger.i("Ignoring duplicate event: ${logMap.toJsonElement()}") + return + } + + logger.e("Failed to decrypt event: ${logMap.toJsonElement()}") - logger.e("Failed to decrypt event: ${logMap.toJsonElement()}") + val errorCode = if (it is ProteusFailure) it.proteusException.intCode else null - applicationMessageHandler.handleDecryptionError( - eventId = event.id, - conversationId = event.conversationId, - messageInstant = event.messageInstant, + applicationMessageHandler.handleDecryptionError( + eventId = event.id, + conversationId = event.conversationId, + messageInstant = event.messageInstant, + senderUserId = event.senderUserId, + senderClientId = event.senderClientId, + content = MessageContent.FailedDecryption( + encodedData = event.encryptedExternalContent?.data, + errorCode = errorCode, + isDecryptionResolved = false, senderUserId = event.senderUserId, - senderClientId = event.senderClientId, - content = MessageContent.FailedDecryption( - encodedData = event.encryptedExternalContent?.data, - isDecryptionResolved = false, - senderUserId = event.senderUserId, - clientId = ClientId(event.senderClientId.value) - ) - ) - eventLogger.logFailure(it, "protocol" to "Proteus") - }.onSuccess { - if (it is MessageUnpackResult.ApplicationMessage) { - processApplicationMessage(it, deliveryInfo) - } - eventLogger.logSuccess( - "protocol" to "Proteus", - "messageType" to it.messageTypeDescription, + clientId = ClientId(event.senderClientId.value) ) - } + ) + eventLogger.logFailure(it, "protocol" to "Proteus") + } } override suspend fun handleNewMLSMessage(event: Event.Conversation.NewMLSMessage, deliveryInfo: EventDeliveryInfo) { diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt index 7d039df3d23..0a7ceb43724 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpacker.kt @@ -27,6 +27,7 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.ProteusFailure +import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.message.PlainMessageBlob @@ -34,7 +35,6 @@ import com.wire.kalium.logic.data.message.ProtoContent import com.wire.kalium.logic.data.message.ProtoContentMapper import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.di.MapperProvider -import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.map @@ -46,7 +46,10 @@ import io.ktor.utils.io.core.toByteArray internal interface ProteusMessageUnpacker { - suspend fun unpackProteusMessage(event: Event.Conversation.NewMessage): Either + suspend fun unpackProteusMessage( + event: Event.Conversation.NewMessage, + handleMessage: suspend (applicationMessage: MessageUnpackResult.ApplicationMessage) -> T + ): Either } @@ -59,7 +62,10 @@ internal class ProteusMessageUnpackerImpl( private val logger get() = kaliumLogger.withFeatureId(KaliumLogger.Companion.ApplicationFlow.EVENT_RECEIVER) - override suspend fun unpackProteusMessage(event: Event.Conversation.NewMessage): Either { + override suspend fun unpackProteusMessage( + event: Event.Conversation.NewMessage, + handleMessage: suspend (applicationMessage: MessageUnpackResult.ApplicationMessage) -> T + ): Either { val decodedContentBytes = Base64.decodeFromBase64(event.content.toByteArray()) val cryptoSessionId = CryptoSessionId( idMapper.toCryptoQualifiedIDId(event.senderUserId), @@ -68,37 +74,50 @@ internal class ProteusMessageUnpackerImpl( return proteusClientProvider.getOrError() .flatMap { wrapProteusRequest { - it.decrypt(decodedContentBytes, cryptoSessionId) + it.decrypt(decodedContentBytes, cryptoSessionId) { + val plainMessageBlob = PlainMessageBlob(it) + getReadableMessageContent(plainMessageBlob, event.encryptedExternalContent).map { readableContent -> + val appMessage = MessageUnpackResult.ApplicationMessage( + conversationId = event.conversationId, + instant = event.messageInstant, + senderUserId = event.senderUserId, + senderClientId = event.senderClientId, + content = readableContent + ) + handleMessage(appMessage) + } + } } - } - .map { PlainMessageBlob(it) } - .flatMap { plainMessageBlob -> getReadableMessageContent(plainMessageBlob, event.encryptedExternalContent) } - .onFailure { - when (it) { - is CoreFailure.Unknown -> logger.e("UnknownFailure when processing message: $it", it.rootCause) + }.flatMap { it } + .onFailure { logUnpackingError(it, event, cryptoSessionId) } + } - is ProteusFailure -> { - val loggableException = - "{ \"code\": \"${it.proteusException.code.name}\", \"message\": \"${it.proteusException.message}\", " + - "\"error\": \"${it.proteusException.stackTraceToString()}\"," + - "\"senderClientId\": \"${event.senderClientId.value.obfuscateId()}\"," + - "\"senderUserId\": \"${event.senderUserId.value.obfuscateId()}\"," + - "\"cryptoClientId\": \"${cryptoSessionId.cryptoClientId.value.obfuscateId()}\"," + - "\"cryptoUserId\": \"${cryptoSessionId.userId.value.obfuscateId()}\"}" - logger.e("ProteusFailure when processing message detail: $loggableException") - } + private fun logUnpackingError( + it: CoreFailure, + event: Event.Conversation.NewMessage, + cryptoSessionId: CryptoSessionId + ) { + when (it) { + is CoreFailure.Unknown -> logger.e("UnknownFailure when processing message: $it", it.rootCause) - else -> logger.e("Failure when processing message: $it") - } - }.map { readableContent -> - MessageUnpackResult.ApplicationMessage( - conversationId = event.conversationId, - instant = event.messageInstant, - senderUserId = event.senderUserId, - senderClientId = event.senderClientId, - content = readableContent - ) + is ProteusFailure -> { + val loggableException = """ + { + "code": "${it.proteusException.code.name}", + "intCode": "${it.proteusException.intCode}", + "message": "${it.proteusException.message}", + "error": "${it.proteusException.stackTraceToString()}", + "senderClientId": "${event.senderClientId.value.obfuscateId()}", + "senderUserId": "${event.senderUserId.value.obfuscateId()}", + "cryptoClientId": "${cryptoSessionId.cryptoClientId.value.obfuscateId()}", + "cryptoUserId": "${cryptoSessionId.userId.value.obfuscateId()}" + } + """.trimIndent() + logger.e("ProteusFailure when processing message detail: $loggableException") } + + else -> logger.e("Failure when processing message: $it") + } } private fun getReadableMessageContent( @@ -110,7 +129,9 @@ internal class ProteusMessageUnpackerImpl( logger.d("Solving external content '$protoContent', EncryptedData='$it'") solveExternalContentForProteusMessage(protoContent, encryptedData) } ?: run { - val rootCause = IllegalArgumentException("Null external content when processing external message instructions.") + val rootCause = IllegalArgumentException( + "Null external content when processing external message instructions." + ) Either.Left(CoreFailure.Unknown(rootCause)) } } @@ -124,7 +145,9 @@ internal class ProteusMessageUnpackerImpl( PlainMessageBlob(decryptedExternalMessage) }.map(protoContentMapper::decodeFromProtobuf).flatMap { decodedProtobuf -> if (decodedProtobuf !is ProtoContent.Readable) { - val rootCause = IllegalArgumentException("матрёшка! External message can't contain another external message inside!") + val rootCause = IllegalArgumentException( + "матрёшка! External message can't contain another external message inside!" + ) Either.Left(CoreFailure.Unknown(rootCause)) } else { Either.Right(decodedProtobuf) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/MessageTextEditHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/MessageTextEditHandler.kt index 61e01d5b796..aa07f66e28c 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/MessageTextEditHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/handler/MessageTextEditHandler.kt @@ -63,7 +63,7 @@ internal class MessageTextEditHandlerImpl internal constructor( && editStatus is Message.EditStatus.Edited ) { // if the locally stored message is also already edited, we check which one is newer - if (editStatus.lastEditInstant < message.date) { + if (editStatus.lastEditInstant > message.date) { // our local pending or failed edit is newer than one we got from the backend so we update locally only message id and date messageRepository.updateTextMessage( conversationId = message.conversationId, @@ -100,5 +100,4 @@ internal class MessageTextEditHandlerImpl internal constructor( ) } } - } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt index 49f0567d0a1..fb9f20448c0 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncCriteriaProvider.kt @@ -116,6 +116,7 @@ internal class SlowSlowSyncCriteriaProviderImpl( LogoutReason.SESSION_EXPIRED -> "Logout: SESSION_EXPIRED" LogoutReason.REMOVED_CLIENT -> "Logout: REMOVED_CLIENT" LogoutReason.DELETED_ACCOUNT -> "Logout: DELETED_ACCOUNT" + LogoutReason.MIGRATION_TO_CC_FAILED -> "Logout: MIGRATION_TO_CC_FAILED" null -> null }?.let { MissingRequirement(it) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt index 0b8fc8d4114..a7bce64bfa5 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncManager.kt @@ -26,7 +26,9 @@ import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.functional.combine import com.wire.kalium.logic.kaliumLogger import com.wire.kalium.logic.sync.SyncExceptionHandler +import com.wire.kalium.logic.sync.SyncType import com.wire.kalium.logic.sync.incremental.IncrementalSyncManager +import com.wire.kalium.logic.sync.provideNewSyncManagerLogger import com.wire.kalium.logic.sync.slow.migration.SyncMigrationStepsProvider import com.wire.kalium.logic.sync.slow.migration.steps.SyncMigrationStep import com.wire.kalium.logic.util.ExponentialDurationHelper @@ -70,11 +72,15 @@ internal class SlowSyncManager( private val syncMigrationStepsProvider: () -> SyncMigrationStepsProvider, logger: KaliumLogger = kaliumLogger, kaliumDispatcher: KaliumDispatcher = KaliumDispatcherImpl, - private val exponentialDurationHelper: ExponentialDurationHelper = ExponentialDurationHelperImpl(MIN_RETRY_DELAY, MAX_RETRY_DELAY) + private val exponentialDurationHelper: ExponentialDurationHelper = ExponentialDurationHelperImpl( + MIN_RETRY_DELAY, + MAX_RETRY_DELAY + ) ) { @OptIn(ExperimentalCoroutinesApi::class) - private val scope = CoroutineScope(SupervisorJob() + kaliumDispatcher.default.limitedParallelism(1)) + private val scope = + CoroutineScope(SupervisorJob() + kaliumDispatcher.default.limitedParallelism(1)) private val logger = logger.withFeatureId(SYNC) private val coroutineExceptionHandler = SyncExceptionHandler( @@ -84,9 +90,9 @@ internal class SlowSyncManager( onFailure = { failure -> logger.i("SlowSync ExceptionHandler error $failure") scope.launch { - slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(failure)) + val delay = exponentialDurationHelper.next() + slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(failure, delay)) slowSyncRecoveryHandler.recover(failure) { - val delay = exponentialDurationHelper.next() logger.i("SlowSync Triggering delay($delay) and waiting for reconnection") networkStateObserver.delayUntilConnectedWithInternetAgain(delay) logger.i("SlowSync Delay and waiting for connection finished - retrying") @@ -101,30 +107,34 @@ internal class SlowSyncManager( startMonitoring() } - private suspend fun isSlowSyncNeededFlow(): Flow = slowSyncRepository.observeLastSlowSyncCompletionInstant() - .map { latestSlowSync -> - logger.i("Last SlowSync was performed on '$latestSlowSync'") - val lastVersion = slowSyncRepository.getSlowSyncVersion() - when { - (lastVersion != null) && (CURRENT_VERSION > lastVersion) -> { - logger.i("Last saved SlowSync version is $lastVersion, current is $CURRENT_VERSION") - SlowSyncParam.MigrationNeeded(oldVersion = lastVersion, newVersion = CURRENT_VERSION) - } + private suspend fun isSlowSyncNeededFlow(): Flow = + slowSyncRepository.observeLastSlowSyncCompletionInstant() + .map { latestSlowSync -> + logger.i("Last SlowSync was performed on '$latestSlowSync'") + val lastVersion = slowSyncRepository.getSlowSyncVersion() + when { + (lastVersion != null) && (CURRENT_VERSION > lastVersion) -> { + logger.i("Last saved SlowSync version is $lastVersion, current is $CURRENT_VERSION") + SlowSyncParam.MigrationNeeded( + oldVersion = lastVersion, + newVersion = CURRENT_VERSION + ) + } - latestSlowSync == null -> { - SlowSyncParam.NotPerformedBefore - } + latestSlowSync == null -> { + SlowSyncParam.NotPerformedBefore + } - DateTimeUtil.currentInstant() > (latestSlowSync + MIN_TIME_BETWEEN_SLOW_SYNCS) -> { - logger.i("Slow sync too old - last slow sync was performed on '$latestSlowSync'") - SlowSyncParam.LastSlowSyncTooOld - } + DateTimeUtil.currentInstant() > (latestSlowSync + MIN_TIME_BETWEEN_SLOW_SYNCS) -> { + logger.i("Slow sync too old - last slow sync was performed on '$latestSlowSync'") + SlowSyncParam.LastSlowSyncTooOld + } - else -> { - SlowSyncParam.Success + else -> { + SlowSyncParam.Success + } } } - } private fun startMonitoring() { scope.launch(coroutineExceptionHandler) { @@ -139,7 +149,10 @@ internal class SlowSyncManager( } } - private suspend fun handleCriteriaResolution(syncCriteriaResolution: SyncCriteriaResolution, isSlowSyncNeeded: SlowSyncParam) { + private suspend fun handleCriteriaResolution( + syncCriteriaResolution: SyncCriteriaResolution, + isSlowSyncNeeded: SlowSyncParam + ) { if (syncCriteriaResolution is SyncCriteriaResolution.Ready) { // START SYNC IF NEEDED logger.i("SlowSync criteria ready, checking if SlowSync is needed or already performed") @@ -176,11 +189,14 @@ internal class SlowSyncManager( } private suspend fun performSlowSync(migrationSteps: List) { + val syncLogger = kaliumLogger.provideNewSyncManagerLogger(SyncType.SLOW) + syncLogger.logSyncStarted() logger.i("Starting SlowSync as all criteria are met and it wasn't performed recently") slowSyncWorker.slowSyncStepsFlow(migrationSteps).cancellable().collect { step -> logger.i("Performing SlowSyncStep $step") slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Ongoing(step)) } + syncLogger.logSyncCompleted() logger.i("SlowSync completed. Updating last completion instant") slowSyncRepository.setSlowSyncVersion(CURRENT_VERSION) slowSyncRepository.setLastSlowSyncCompletionInstant(DateTimeUtil.currentInstant()) @@ -194,7 +210,7 @@ internal class SlowSyncManager( * Useful when a new step is added to Slow Sync, or when we fix some bug in Slow Sync, * and we'd like to get all users to take advantage of the fix. */ - const val CURRENT_VERSION = 7 + const val CURRENT_VERSION = 9 val MIN_RETRY_DELAY = 1.seconds val MAX_RETRY_DELAY = 10.minutes diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt index 35fc040ee08..2022b6c0f32 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorker.kt @@ -26,6 +26,7 @@ import com.wire.kalium.logic.data.event.EventRepository import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.feature.connection.SyncConnectionsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.legalhold.FetchLegalHoldForSelfUserFromRemoteUseCase @@ -71,6 +72,7 @@ internal class SlowSyncWorkerImpl( private val joinMLSConversations: JoinExistingMLSConversationsUseCase, private val fetchLegalHoldForSelfUserFromRemoteUseCase: FetchLegalHoldForSelfUserFromRemoteUseCase, private val oneOnOneResolver: OneOnOneResolver, + private val syncConversationFolders: SyncConversationFoldersUseCase, logger: KaliumLogger = kaliumLogger ) : SlowSyncWorker { @@ -102,6 +104,7 @@ internal class SlowSyncWorkerImpl( .continueWithStep(SlowSyncStep.CONTACTS, syncContacts::invoke) .continueWithStep(SlowSyncStep.JOINING_MLS_CONVERSATIONS, joinMLSConversations::invoke) .continueWithStep(SlowSyncStep.RESOLVE_ONE_ON_ONE_PROTOCOLS, oneOnOneResolver::resolveAllOneOnOneConversations) + .continueWithStep(SlowSyncStep.CONVERSATION_FOLDERS, syncConversationFolders::invoke) .flatMap { saveLastProcessedEventIdIfNeeded(lastProcessedEventIdToSaveOnSuccess) } diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/ServerTimeHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/ServerTimeHandler.kt new file mode 100644 index 00000000000..53e7f5177ce --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/ServerTimeHandler.kt @@ -0,0 +1,54 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.util + +import com.wire.kalium.logic.kaliumLogger +import kotlinx.datetime.Clock + +internal interface ServerTimeHandler { + fun computeTimeOffset(serverTime: Long) + fun toServerTimestamp(localTimestamp: Long = Clock.System.now().epochSeconds): Long +} + +internal class ServerTimeHandlerImpl : ServerTimeHandler { + + /** + * Used to store the difference (offset) between the server time and the local client time. + * And it will be used to adjust timestamps between server and client times. + */ + private var timeOffset: Long = 0 + + /** + * Compute the time offset between the server and the client + * @param serverTime the server time to compute the offset + */ + override fun computeTimeOffset(serverTime: Long) { + kaliumLogger.i("ServerTimeHandler: computing time offset between server and client..") + val offset = Clock.System.now().epochSeconds - serverTime + timeOffset = offset + } + + /** + * Convert local timestamp to server timestamp + * @param localTimestamp timestamp from client to convert + * @return the timestamp adjusted with the client/server time shift + */ + override fun toServerTimestamp(localTimestamp: Long): Long { + return localTimestamp - timeOffset + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/TimeLogger.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/TimeLogger.kt new file mode 100644 index 00000000000..9c98636c4c1 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/util/TimeLogger.kt @@ -0,0 +1,55 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.util + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant + +/** + * The `TimeLogger` class is designed to measure the duration of a process for benchmarking purposes. + * It takes a `processName` as a parameter to identify the process being timed. + * + * Usage: + * - Call `start()` to begin tracking the time for a process. + * - Call `finish()` to log the time taken since `start()` was invoked. + * + * @property processName The name of the process being measured. + * + * Methods: + * - `start()`: Logs the process start and records the current time. + * - `finish()`: Logs the process end, calculates the elapsed time, and prints the duration in milliseconds. + * + * This class is useful for performance analysis and optimization by providing a simple way to track + * execution times for different parts of code. + */ +class TimeLogger(private val processName: String) { + + private lateinit var startTime: Instant + fun start() { + println("$processName starting") + startTime = Clock.System.now() + + } + + fun finish() { + val endTime = Clock.System.now() + val duration = endTime - startTime + println("$processName finished after: ${duration.inWholeMilliseconds} milliseconds") + } + +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/corefailure/WrapProteusRequestTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/corefailure/WrapProteusRequestTest.kt index 6c2b0522d26..cb4ea1bff92 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/corefailure/WrapProteusRequestTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/corefailure/WrapProteusRequestTest.kt @@ -40,7 +40,7 @@ class WrapProteusRequestTest { @Test fun whenApiRequestReturnNoInternetConnection_thenCorrectErrorIsPropagated() { - val expected = ProteusException(null, ProteusException.Code.PANIC, RuntimeException()) + val expected = ProteusException(null, ProteusException.Code.PANIC, 15, RuntimeException()) val actual = wrapProteusRequest { throw expected } assertIs>(actual) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallHelperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallHelperTest.kt new file mode 100644 index 00000000000..945ee3eebad --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallHelperTest.kt @@ -0,0 +1,165 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.call + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.network.api.authenticated.conversation.SubconversationResponse +import io.mockative.Mock +import io.mockative.classOf +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CallHelperTest { + + @Test + fun givenMlsProtocol_whenShouldEndSFTOneOnOneCallIsCalled_thenReturnCorrectValue() = + runTest { + val (_, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true)) + .arrange() + + // one participant in the call + val shouldEndSFTOneOnOneCall1 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.ONE_ON_ONE, + newCallParticipants = listOf(participantMinimized1), + previousCallParticipants = listOf(participant1) + ) + assertFalse { shouldEndSFTOneOnOneCall1 } + + // Audio not lost for the second participant + val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.GROUP, + newCallParticipants = listOf(participantMinimized1, participantMinimized2), + previousCallParticipants = listOf(participant1, participant2) + ) + assertFalse { shouldEndSFTOneOnOneCall2 } + + // Audio lost for the second participant + val shouldEndSFTOneOnOneCall3 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = CONVERSATION_MLS_PROTOCOL_INFO, + conversationType = Conversation.Type.ONE_ON_ONE, + previousCallParticipants = listOf(participant1, participant2), + newCallParticipants = listOf( + participantMinimized1, + participantMinimized2.copy(hasEstablishedAudio = false) + ) + ) + assertTrue { shouldEndSFTOneOnOneCall3 } + } + + @Test + fun givenProteusProtocol_whenShouldEndSFTOneOnOneCallIsCalled_thenReturnCorrectValue() = + runTest { + + val (_, mLSCallHelper) = Arrangement() + .withShouldUseSFTForOneOnOneCallsReturning(Either.Right(true)) + .arrange() + + // participants list has 2 items for the new list and the previous list + val shouldEndSFTOneOnOneCall1 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = Conversation.ProtocolInfo.Proteus, + conversationType = Conversation.Type.ONE_ON_ONE, + newCallParticipants = listOf(participantMinimized1, participantMinimized2), + previousCallParticipants = listOf(participant1, participant2) + ) + assertFalse { shouldEndSFTOneOnOneCall1 } + + // new participants list has 1 participant + val shouldEndSFTOneOnOneCall2 = mLSCallHelper.shouldEndSFTOneOnOneCall( + conversationId = conversationId, + callProtocol = Conversation.ProtocolInfo.Proteus, + conversationType = Conversation.Type.ONE_ON_ONE, + newCallParticipants = listOf(participantMinimized1), + previousCallParticipants = listOf(participant1, participant2) + ) + assertTrue { shouldEndSFTOneOnOneCall2 } + } + + private class Arrangement { + + @Mock + val userConfigRepository = mock(classOf()) + + private val mLSCallHelper: CallHelper = CallHelperImpl() + + fun arrange() = this to mLSCallHelper + + fun withShouldUseSFTForOneOnOneCallsReturning(result: Either) = + apply { + every { userConfigRepository.shouldUseSFTForOneOnOneCalls() }.returns(result) + } + } + + companion object { + val conversationId = ConversationId(value = "convId", domain = "domainId") + val CONVERSATION_MLS_PROTOCOL_INFO = Conversation.ProtocolInfo.MLS( + GroupID("GROUP_ID"), + Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + 5UL, + Instant.parse("2021-03-30T15:36:00.000Z"), + cipherSuite = CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + val participant1 = Participant( + id = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + name = "name", + isMuted = true, + isSpeaking = false, + isCameraOn = false, + avatarAssetId = null, + isSharingScreen = false, + hasEstablishedAudio = true, + accentId = 0 + ) + val participant2 = participant1.copy( + id = QualifiedID("participantId2", "participantDomain2"), + clientId = "efgh" + ) + val participantMinimized1 = ParticipantMinimized( + id = QualifiedID("participantId", "participantDomain"), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + val participantMinimized2 = participantMinimized1.copy( + id = QualifiedID("participantId2", "participantDomain2"), + clientId = "efgh" + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt index 18cc5052316..e1c9cb91886 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallRepositoryTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.cryptography.CryptoQualifiedClientId import com.wire.kalium.cryptography.MLSClient import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.callerId +import com.wire.kalium.logic.data.call.CallRepositoryTest.Arrangement.Companion.participant import com.wire.kalium.logic.data.call.mapper.CallMapperImpl import com.wire.kalium.logic.data.client.MLSClientProvider import com.wire.kalium.logic.data.conversation.ClientId @@ -84,6 +85,7 @@ import kotlinx.datetime.Clock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -173,10 +175,7 @@ class CallRepositoryTest { ConversationDetails.Group( Arrangement.groupConversation, false, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -192,7 +191,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.STARTED, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -213,10 +212,7 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -242,7 +238,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.STARTED, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -269,10 +265,7 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -288,7 +281,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -314,10 +307,7 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -343,7 +333,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -373,10 +363,7 @@ class CallRepositoryTest { Either.Right( ConversationDetails.Group( Arrangement.groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -392,7 +379,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -427,7 +414,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.STARTED, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -466,7 +453,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.STARTED, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -504,7 +491,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -550,7 +537,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -587,7 +574,7 @@ class CallRepositoryTest { callRepository.createCall( conversationId = Arrangement.conversationId, status = CallStatus.INCOMING, - callerId = callerId.value, + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -1474,10 +1461,212 @@ class CallRepositoryTest { assertEquals(activeSpeakers, callRepository.getCallMetadataProfile().data[Arrangement.conversationId]?.activeSpeakers) } + @Test + fun givenCallWithActiveSpeakers_whenGetFullParticipants_thenOnlySpeakingUsers() = runTest { + val (_, callRepository) = Arrangement().arrange() + val mutedParticipant = ParticipantMinimized( + id = QualifiedID("participantId", ""), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd0", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + val unMutedParticipant = mutedParticipant.copy( + id = QualifiedID("anotherParticipantId", ""), + userId = QualifiedID("anotherParticipantId", "participantDomain"), + clientId = "abcd1", + isMuted = false + ) + val activeSpeakers = mapOf( + mutedParticipant.userId to listOf(mutedParticipant.clientId), + unMutedParticipant.userId to listOf(unMutedParticipant.clientId), + ) + + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf( + Arrangement.conversationId to createCallMetadata().copy( + participants = listOf(mutedParticipant, unMutedParticipant), + maxParticipants = 0 + ) + ) + ) + ) + + // when + callRepository.updateParticipantsActiveSpeaker(Arrangement.conversationId, activeSpeakers) + + // then + val fullParticipants = callRepository.getCallMetadataProfile().data[Arrangement.conversationId]?.getFullParticipants() + + assertEquals( + false, + fullParticipants?.first { it.id == mutedParticipant.id && it.clientId == mutedParticipant.clientId }?.isSpeaking + ) + assertEquals( + true, + fullParticipants?.first { it.id == unMutedParticipant.id && it.clientId == unMutedParticipant.clientId }?.isSpeaking + ) + } + + @Test + fun givenCallWithParticipantsNotSharingScreen_whenOneStartsToShare_thenSharingMetadataHasProperValues() = runTest { + // given + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + val (_, callRepository) = Arrangement() + .givenGetKnownUserMinimizedSucceeds() + .arrange() + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertEquals(0L, callMetadata.screenShareMetadata.completedScreenShareDurationInMillis) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(otherParticipant.id)) + } + + @Test + fun givenCallWithParticipantsNotSharingScreen_whenTwoStartsAndOneStops_thenSharingMetadataHasProperValues() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain")) + val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain")) + val participantsList = listOf(participant, secondParticipant, thirdParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant, thirdParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.size == 1) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.containsKey(thirdParticipant.id)) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenOneStopsToShare_thenSharingMetadataHasProperValues() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = false)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.activeScreenShares.isEmpty()) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenTheSameParticipantIsSharingMultipleTime_thenSharingMetadataHasUserIdOnlyOnce() = + runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val otherParticipant = participant.copy(id = QualifiedID("anotherParticipantId", "participantDomain")) + val participantsList = listOf(participant, otherParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = false)) + ) + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, otherParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 1) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.contains(otherParticipant.id.toString())) + } + + @Test + fun givenCallWithParticipantsSharingScreen_whenTwoParticipantsAreSharing_thenSharingMetadataHasBothOfUsersIds() = runTest { + // given + val (_, callRepository) = Arrangement() + .arrange() + val secondParticipant = participant.copy(id = QualifiedID("secondParticipantId", "participantDomain")) + val thirdParticipant = participant.copy(id = QualifiedID("thirdParticipantId", "participantDomain")) + val participantsList = listOf(participant, secondParticipant, thirdParticipant) + callRepository.updateCallMetadataProfileFlow( + callMetadataProfile = CallMetadataProfile( + data = mapOf(Arrangement.conversationId to createCallMetadata().copy(participants = participantsList)) + ) + ) + + // when + callRepository.updateCallParticipants( + Arrangement.conversationId, + listOf(participant, secondParticipant.copy(isSharingScreen = true), thirdParticipant.copy(isSharingScreen = true)) + ) + val callMetadata = callRepository.getCallMetadataProfile()[Arrangement.conversationId] + + // then + assertNotNull(callMetadata) + assertTrue(callMetadata.screenShareMetadata.uniqueSharingUsers.size == 2) + assertEquals( + setOf(secondParticipant.id.toString(), thirdParticipant.id.toString()), + callMetadata.screenShareMetadata.uniqueSharingUsers + ) + } + private fun provideCall(id: ConversationId, status: CallStatus) = Call( conversationId = id, status = status, - callerId = "callerId@domain", + callerId = UserId("callerId", "domain"), participants = listOf(), isMuted = false, isCameraOn = false, @@ -1502,6 +1691,7 @@ class CallRepositoryTest { ) private fun createCallMetadata() = CallMetadata( + callerId = callerId, isMuted = true, isCameraOn = false, isCbrEnabled = false, @@ -1767,8 +1957,6 @@ class CallRepositoryTest { conversation = oneOnOneConversation, otherUser = TestUser.OTHER, userType = UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val mlsProtocolInfo = Conversation.ProtocolInfo.MLS( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt index 4b9e7f4de46..ff57b4aba81 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/CallingParticipantsOrderTest.kt @@ -190,7 +190,8 @@ class CallingParticipantsOrderTest { isMuted = false, isCameraOn = false, name = "self user", - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant2 = Participant( id = QualifiedID("participant2", "domain"), @@ -198,7 +199,8 @@ class CallingParticipantsOrderTest { isMuted = false, isCameraOn = true, name = "user name", - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant3 = Participant( id = QualifiedID("participant3", "domain"), @@ -206,7 +208,8 @@ class CallingParticipantsOrderTest { isMuted = false, isCameraOn = false, name = "A random name", - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant4 = Participant( id = QualifiedID("participant4", "domain"), @@ -215,7 +218,8 @@ class CallingParticipantsOrderTest { isCameraOn = false, isSharingScreen = true, name = "A random name", - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant11 = Participant( id = selfUserId, @@ -223,7 +227,8 @@ class CallingParticipantsOrderTest { isMuted = false, isCameraOn = false, name = "self user", - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participants = listOf(participant1, participant2, participant3, participant4, participant11) val otherParticipants = listOf(participant2, participant3, participant4, participant11) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsFilterTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsFilterTest.kt index abfc1126a1e..9ef6480fddd 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsFilterTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsFilterTest.kt @@ -105,7 +105,8 @@ class ParticipantsFilterTest { name = "Alok", isCameraOn = true, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant11 = Participant( id = selfUserId, @@ -113,7 +114,8 @@ class ParticipantsFilterTest { name = "Alok", isCameraOn = false, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant2 = Participant( id = userId2, @@ -121,7 +123,8 @@ class ParticipantsFilterTest { name = "Max", isCameraOn = true, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant3 = Participant( id = userId3, @@ -129,7 +132,8 @@ class ParticipantsFilterTest { name = "Hisoka", isCameraOn = false, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participants = listOf(participant1, participant2, participant3, participant11) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsOrderByNameTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsOrderByNameTest.kt index b6e590864ce..028e73a02b1 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsOrderByNameTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/call/ParticipantsOrderByNameTest.kt @@ -40,7 +40,8 @@ class ParticipantsOrderByNameTest { name = "Alok", isCameraOn = true, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant2 = Participant( id = UserId("participant2", "domain"), @@ -48,7 +49,8 @@ class ParticipantsOrderByNameTest { name = "Max", isCameraOn = true, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participant3 = Participant( id = UserId("participant3", "domain"), @@ -56,7 +58,8 @@ class ParticipantsOrderByNameTest { name = "Hisoka", isCameraOn = true, isMuted = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) val participants = listOf(participant2, participant1, participant3) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt index 8263608edf6..4999236394a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/client/ClientRepositoryTest.kt @@ -36,11 +36,11 @@ import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed -import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.authenticated.client.ClientDTO import com.wire.kalium.network.api.authenticated.client.ClientTypeDTO import com.wire.kalium.network.api.authenticated.client.DeviceTypeDTO import com.wire.kalium.network.api.authenticated.client.SimpleClientResponse +import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.utils.NetworkResponse @@ -257,7 +257,7 @@ class ClientRepositoryTest { deviceType = DeviceTypeDTO.Desktop, label = null, model = "Mac ox", - capabilities = null, + capabilities = listOf(), mlsPublicKeys = null, cookie = null ), @@ -269,7 +269,7 @@ class ClientRepositoryTest { deviceType = DeviceTypeDTO.Phone, label = null, model = "iphone 15", - capabilities = null, + capabilities = listOf(), mlsPublicKeys = null, cookie = null ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt index 7bc8eba600a..340eaff4957 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationGroupRepositoryTest.kt @@ -422,7 +422,7 @@ class ConversationGroupRepositoryTest { }.wasInvoked(once) coVerify { - mlsConversationRepository.establishMLSGroup(any(), any(), eq(true)) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), eq(true)) }.wasInvoked(once) coVerify { @@ -465,7 +465,7 @@ class ConversationGroupRepositoryTest { }.wasInvoked(once) coVerify { - mlsConversationRepository.establishMLSGroup(any(), any(), eq(true)) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), eq(true)) }.wasInvoked(once) coVerify { @@ -905,7 +905,10 @@ class ConversationGroupRepositoryTest { }.wasInvoked(exactly = once) coVerify { - arrangement.joinExistingMLSConversation.invoke(eq(ADD_MEMBER_TO_CONVERSATION_SUCCESSFUL_RESPONSE.event.qualifiedConversation.toModel())) + arrangement.joinExistingMLSConversation.invoke( + ADD_MEMBER_TO_CONVERSATION_SUCCESSFUL_RESPONSE.event.qualifiedConversation.toModel(), + null + ) }.wasInvoked(exactly = once) coVerify { @@ -950,7 +953,10 @@ class ConversationGroupRepositoryTest { }.wasInvoked(exactly = once) coVerify { - arrangement.joinExistingMLSConversation.invoke(eq(ADD_MEMBER_TO_CONVERSATION_SUCCESSFUL_RESPONSE.event.qualifiedConversation.toModel())) + arrangement.joinExistingMLSConversation.invoke( + ADD_MEMBER_TO_CONVERSATION_SUCCESSFUL_RESPONSE.event.qualifiedConversation.toModel(), + null + ) }.wasInvoked(exactly = once) coVerify { @@ -1282,9 +1288,10 @@ class ConversationGroupRepositoryTest { @Test fun givenAConversationFailsWithUnreachableAndNotFromUsersInRequest_whenAddingMembers_thenRetryIsNotExecutedAndCreateSysMessage() = runTest { + val conversation = TestConversation.CONVERSATION.copy(id = ConversationId("valueConvo", "domainConvo")) // given val (arrangement, conversationGroupRepository) = Arrangement() - .withConversationDetailsById(TestConversation.CONVERSATION) + .withConversationDetailsById(conversation) .withProtocolInfoById(PROTEUS_PROTOCOL_INFO) .withFetchUsersIfUnknownByIdsSuccessful() .withAddMemberAPIFailsFirstWithUnreachableThenSucceed( @@ -1309,11 +1316,9 @@ class ConversationGroupRepositoryTest { coVerify { arrangement.newGroupConversationSystemMessagesCreator.conversationFailedToAddMembers( - conversationId = any(), - userIdList = matches { - it.containsAll(expectedInitialUsersNotFromUnreachableInformed) - }, - type = any() + conversationId = conversation.id, + userIdList = expectedInitialUsersNotFromUnreachableInformed, + type = MessageContent.MemberChange.FailedToAdd.Type.Federation ) }.wasInvoked(once) } @@ -1723,11 +1728,10 @@ class ConversationGroupRepositoryTest { legalHoldHandler ) - suspend fun withMlsConversationEstablished(additionResult: MLSAdditionResult): Arrangement { + suspend fun withMlsConversationEstablished(additionResult: MLSAdditionResult): Arrangement = apply { coEvery { - mlsConversationRepository.establishMLSGroup(any(), any(), any()) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), any()) }.returns(Either.Right(additionResult)) - return this } /** @@ -1773,7 +1777,7 @@ class ConversationGroupRepositoryTest { suspend fun withJoinExistingMlsConversationSucceeds() = apply { coEvery { - joinExistingMLSConversation.invoke(any()) + joinExistingMLSConversation.invoke(any(), any()) }.returns(Either.Right(Unit)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt index 335b64421fd..8001981da4c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationMapperTest.kt @@ -19,10 +19,16 @@ package com.wire.kalium.logic.data.conversation import com.wire.kalium.logic.data.connection.ConnectionStatusMapper +import com.wire.kalium.logic.data.conversation.ConversationRepositoryTest.Companion.MESSAGE_PREVIEW_ENTITY import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.data.message.MessagePreview +import com.wire.kalium.logic.data.message.MessagePreviewContent import com.wire.kalium.logic.data.user.AvailabilityStatusMapper import com.wire.kalium.logic.data.user.type.DomainUserTypeMapper +import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO @@ -35,7 +41,11 @@ import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.model.ConversationId import com.wire.kalium.network.api.model.UserId import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.message.MessagePreviewEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity import io.mockative.Mock import io.mockative.any import io.mockative.every @@ -45,6 +55,7 @@ import io.mockative.verify import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs class ConversationMapperTest { @@ -69,6 +80,9 @@ class ConversationMapperTest { @Mock val conversationMemberMapper = mock(ConversationRoleMapper::class) + @Mock + val messageMapper = mock(MessageMapper::class) + private lateinit var conversationMapper: ConversationMapper @BeforeTest @@ -81,7 +95,8 @@ class ConversationMapperTest { userAvailabilityStatusMapper, domainUserTypeMapper, connectionStatusMapper, - conversationMemberMapper + conversationMemberMapper, + messageMapper, ) } @@ -234,6 +249,170 @@ class ConversationMapperTest { assertEquals(ConversationEntity.Type.GROUP, result) } + @Test + fun givenAccessList_whenMappingFromModelToDAOAccess_thenCorrectValuesShouldBeReturned() { + // given + val accessList = setOf( + Conversation.Access.PRIVATE, + Conversation.Access.CODE, + Conversation.Access.INVITE, + Conversation.Access.LINK, + Conversation.Access.SELF_INVITE + ) + + val expected = listOf( + ConversationEntity.Access.PRIVATE, + ConversationEntity.Access.CODE, + ConversationEntity.Access.INVITE, + ConversationEntity.Access.LINK, + ConversationEntity.Access.SELF_INVITE + ) + + // when + val result = conversationMapper.fromModelToDAOAccess(accessList) + + // then + assertEquals(expected, result) + } + + @Test + fun givenAccessRoleList_whenMappingFromModelToDAOAccessRole_thenCorrectValuesShouldBeReturned() { + // given + val accessRoleList = setOf( + Conversation.AccessRole.SERVICE, + Conversation.AccessRole.GUEST, + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.NON_TEAM_MEMBER, + Conversation.AccessRole.EXTERNAL + ) + + val expected = listOf( + ConversationEntity.AccessRole.SERVICE, + ConversationEntity.AccessRole.GUEST, + ConversationEntity.AccessRole.TEAM_MEMBER, + ConversationEntity.AccessRole.NON_TEAM_MEMBER, + ConversationEntity.AccessRole.EXTERNAL + ) + + // when + val result = conversationMapper.fromModelToDAOAccessRole(accessRoleList) + + // then + assertEquals(expected, result) + } + + @Test + fun givenAccessList_whenMappingFromApiModelToAccessModel_thenCorrectValuesShouldBeReturned() { + // given + val accessList = setOf( + ConversationAccessDTO.PRIVATE, + ConversationAccessDTO.CODE, + ConversationAccessDTO.INVITE, + ConversationAccessDTO.LINK, + ConversationAccessDTO.SELF_INVITE + ) + + val expected = setOf( + Conversation.Access.PRIVATE, + Conversation.Access.CODE, + Conversation.Access.INVITE, + Conversation.Access.LINK, + Conversation.Access.SELF_INVITE + ) + + // when + val result = conversationMapper.fromApiModelToAccessModel(accessList) + + // then + assertEquals(expected, result) + } + + @Test + fun givenAccessRoleList_whenMappingFromApiModelToAccessModel_thenCorrectValuesShouldBeReturned() { + // given + val accessRoleList = setOf( + ConversationAccessRoleDTO.SERVICE, + ConversationAccessRoleDTO.GUEST, + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.NON_TEAM_MEMBER, + ConversationAccessRoleDTO.EXTERNAL + ) + + val expected = setOf( + Conversation.AccessRole.SERVICE, + Conversation.AccessRole.GUEST, + Conversation.AccessRole.TEAM_MEMBER, + Conversation.AccessRole.NON_TEAM_MEMBER, + Conversation.AccessRole.EXTERNAL + ) + + // when + val result = conversationMapper.fromApiModelToAccessRoleModel(accessRoleList) + + // then + assertEquals(expected, result) + } + + private fun mockPreviewMessage(content: MessagePreviewContent) = MessagePreview( + id = MESSAGE_PREVIEW_ENTITY.id, + conversationId = TestConversation.CONVERSATION.id, + content = content, + visibility = Message.Visibility.VISIBLE, + isSelfMessage = false, + senderUserId = TestUser.OTHER.id, + ) + private fun testConversationLastMessage( + lastMessage: MessagePreviewEntity? = null, + messageDraft: MessageDraftEntity? = null, + archived: Boolean = false, + assertion: (MessagePreview?) -> Unit + ) { + every { + protocolInfoMapper.fromEntity(any()) + }.returns(Conversation.ProtocolInfo.Proteus) + every { + conversationStatusMapper.fromMutedStatusDaoModel(any()) + }.returns(MutedConversationStatus.AllAllowed) + every { + messageMapper.fromEntityToMessagePreview(any()) + }.returns(mockPreviewMessage(MessagePreviewContent.WithUser.Text("sender", "message"))) + every { + messageMapper.fromDraftToMessagePreview(any()) + }.returns(mockPreviewMessage(MessagePreviewContent.Draft("draft"))) + val conversation = ConversationDetailsWithEventsEntity( + conversationViewEntity = TestConversation.VIEW_ENTITY.copy(archived = archived), + lastMessage = lastMessage, + messageDraft = messageDraft, + unreadEvents = ConversationUnreadEventEntity(TestConversation.VIEW_ENTITY.id, mapOf()), + ) + assertion(conversationMapper.fromDaoModelToDetailsWithEvents(conversation).lastMessage) + } + + @Test + fun givenConversationWithDraftAndLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + messageDraft = MESSAGE_DRAFT_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + ) { lastMessage -> assertIs(lastMessage?.content) } // draft is always newer than last message + + @Test + fun givenConversationWithLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + ) { lastMessage -> assertIs(lastMessage?.content) } + + @Test + fun givenConversationWithNoLastMessageAndDraft_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage { lastMessage -> assertEquals(null, lastMessage) } + + @Test + fun givenArchivedConversationWithDraftAndLastMessage_whenMappingFromDAODetailsWithEventsToModel_thenReturnProperLastMessage() = + testConversationLastMessage( + lastMessage = MESSAGE_PREVIEW_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + messageDraft = MESSAGE_DRAFT_ENTITY.copy(conversationId = TestConversation.VIEW_ENTITY.id), + archived = true, + ) { lastMessage -> assertEquals(null, lastMessage) } // do not return last message if conversation is archived + private companion object { val ORIGINAL_CONVERSATION_ID = ConversationId("original", "oDomain") val SELF_USER_TEAM_ID = TeamId("teamID") @@ -247,6 +426,8 @@ class ConversationMapperTest { val OTHER_MEMBERS = listOf(ConversationMemberDTO.Other(service = null, id = UserId("other1", "domain1"), conversationRole = "wire_admin")) val MEMBERS_RESPONSE = ConversationMembersResponse(SELF_MEMBER_RESPONSE, OTHER_MEMBERS) + val MESSAGE_DRAFT_ENTITY = MessageDraftEntity(TestConversation.VIEW_ENTITY.id, "text", null, null, listOf()) + val CONVERSATION_RESPONSE = ConversationResponse( "creator", MEMBERS_RESPONSE, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt new file mode 100644 index 00000000000..dbc66271f5d --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryExtensionsTest.kt @@ -0,0 +1,119 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.data.conversation + +import app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.paging.PagingSource +import app.cash.paging.PagingState +import com.wire.kalium.logic.data.message.MessageMapper +import com.wire.kalium.logic.framework.TestConversationDetails +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions +import com.wire.kalium.persistence.dao.message.KaliumPager +import io.mockative.Mock +import io.mockative.any +import io.mockative.eq +import io.mockative.every +import io.mockative.matches +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class ConversationRepositoryExtensionsTest { + private val fakePagingSource = object : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = null + + override suspend fun load(params: LoadParams): LoadResult = + LoadResult.Error(NotImplementedError("STUB for tests. Not implemented.")) + } + + @Test + fun givenParameters_whenPaginatedConversationDetailsWithEvents_thenShouldCallDaoExtensionsWithRightParameters() = runTest { + val pagingConfig = PagingConfig(20) + val pager = Pager(pagingConfig) { fakePagingSource } + val kaliumPager = KaliumPager(pager, fakePagingSource, StandardTestDispatcher()) + val (arrangement, conversationRepositoryExtensions) = Arrangement() + .withConversationExtensionsReturningPager(kaliumPager) + .arrange() + val searchQuery = "search" + conversationRepositoryExtensions.getPaginatedConversationDetailsWithEventsBySearchQuery( + queryConfig = ConversationQueryConfig( + searchQuery = searchQuery, + fromArchive = false, + onlyInteractionEnabled = false, + newActivitiesOnTop = false, + ), + pagingConfig = pagingConfig, + startingOffset = 0L + ) + verify { + arrangement.conversationDaoExtensions + .getPagerForConversationDetailsWithEventsSearch( + queryConfig = matches { + it.searchQuery == searchQuery && !it.fromArchive && !it.onlyInteractionEnabled && !it.newActivitiesOnTop + }, + pagingConfig = eq(pagingConfig), + startingOffset = any() + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationDaoExtensions: ConversationExtensions = mock(ConversationExtensions::class) + + @Mock + private val conversationDAO: ConversationDAO = mock(ConversationDAO::class) + + @Mock + private val conversationMapper: ConversationMapper = mock(ConversationMapper::class) + + @Mock + private val messageMapper: MessageMapper = mock(MessageMapper::class) + private val conversationRepositoryExtensions: ConversationRepositoryExtensions by lazy { + ConversationRepositoryExtensionsImpl(conversationDAO, conversationMapper) + } + + init { + every { + messageMapper.fromEntityToMessage(any()) + }.returns(TestMessage.TEXT_MESSAGE) + every { + conversationMapper.fromDaoModelToDetails(any()) + }.returns(TestConversationDetails.CONVERSATION_GROUP) + every { + conversationDAO.platformExtensions + }.returns(conversationDaoExtensions) + } + + fun withConversationExtensionsReturningPager(kaliumPager: KaliumPager) = apply { + every { + conversationDaoExtensions.getPagerForConversationDetailsWithEventsSearch(any(), any(), any()) + }.returns(kaliumPager) + } + + fun arrange() = this to conversationRepositoryExtensions + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt index 1e4a82d7c6e..c7108c8b7b7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/ConversationRepositoryTest.kt @@ -48,10 +48,8 @@ import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangement import com.wire.kalium.logic.util.arrangement.dao.MemberDAOArrangementImpl import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed -import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol.MLS -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO import com.wire.kalium.network.api.authenticated.conversation.ConversationMembersResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationNameUpdateEvent @@ -69,6 +67,8 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation import com.wire.kalium.network.api.authenticated.conversation.model.ConversationProtocolDTO import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO import com.wire.kalium.network.api.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.client.ClientApi +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.exceptions.KaliumException @@ -81,6 +81,7 @@ import com.wire.kalium.persistence.dao.client.ClientTypeEntity import com.wire.kalium.persistence.dao.client.DeviceTypeEntity import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.message.MessageDAO @@ -112,12 +113,12 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlinx.datetime.Clock import kotlinx.datetime.Instant +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertNotNull -import kotlin.test.assertTrue import com.wire.kalium.network.api.model.ConversationId as APIConversationId import com.wire.kalium.persistence.dao.client.Client as ClientEntity @@ -707,13 +708,12 @@ class ConversationRepositoryTest { .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals( MapperProvider.messageMapper(TestUser.SELF.id).fromEntityToMessagePreview(messagePreviewEntity), @@ -751,13 +751,12 @@ class ConversationRepositoryTest { .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) assertEquals(null, conversation.lastMessage) @@ -765,6 +764,8 @@ class ConversationRepositoryTest { } } + // TODO: bring back once pagination is implemented + @Ignore @Test fun givenAGroupConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = runTest { // given @@ -776,41 +777,47 @@ class ConversationRepositoryTest { .arrange() // when - conversationRepository.observeConversationDetailsById(TestConversation.ID).test { + conversationRepository.observeConversationDetailsById(conversationEntity.id.toModel()).test { // then val conversationDetail = awaitItem() - assertIs>(conversationDetail) - assertTrue { conversationDetail.value.lastMessage == null } + assertIs(conversationDetail) +// assertTrue { conversationDetail.lastMessage == null } awaitComplete() } } - @Test - fun givenAOneToOneConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = - runTest { - // given - val conversationEntity = TestConversation.VIEW_ENTITY.copy( - type = ConversationEntity.Type.ONE_ON_ONE, - otherUserId = QualifiedIDEntity("otherUser", "domain") - ) - - val (_, conversationRepository) = Arrangement() - .withExpectedObservableConversationDetails(conversationEntity) - .arrange() - - // when - conversationRepository.observeConversationDetailsById(TestConversation.ID).test { - // then - val conversationDetail = awaitItem() - - assertIs>(conversationDetail) - assertTrue { conversationDetail.value.lastMessage == null } - - awaitComplete() - } - } + // TODO: bring back once pagination is implemented +// @Test +// fun givenAOneToOneConversationHasNotNewMessages_whenGettingConversationDetails_ThenReturnZeroUnreadMessageCount() = +// runTest { +// // given +// val conversationIdEntity = ConversationIDEntity("some_value", "some_domain") +// val conversationId = QualifiedID("some_value", "some_domain") +// val shouldFetchFromArchivedConversations = false +// val conversationEntity = TestConversation.VIEW_ENTITY.copy( +// id = conversationIdEntity, +// type = ConversationEntity.Type.ONE_ON_ONE, +// otherUserId = QualifiedIDEntity("otherUser", "domain"), +// ) +// val conversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity(conversationViewEntity = conversationEntity) +// +// val (_, conversationRepository) = Arrangement() +// .withConversationDetailsWithEvents(listOf(conversationDetailsWithEventsEntity)) +// .arrange() +// +// // when +// conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { +// // then +// val conversation = awaitItem().first { it.conversationDetails.conversation.id == conversationId } +// +// assertIs(conversation.conversationDetails) +// assertTrue { conversation.lastMessage == null } +// +// awaitComplete() +// } +// } @Test fun givenAGroupConversationHasNewMessages_whenObservingConversationListDetails_ThenCorrectlyGetUnreadMessageCount() = runTest { @@ -838,13 +845,12 @@ class ConversationRepositoryTest { .arrange() // when - conversationRepository.observeConversationListDetails(shouldFetchFromArchivedConversations).test { + conversationRepository.observeConversationListDetailsWithEvents(shouldFetchFromArchivedConversations).test { val result = awaitItem() - assertContains(result.map { it.conversation.id }, conversationId) - val conversation = result.first { it.conversation.id == conversationId } + assertContains(result.map { it.conversationDetails.conversation.id }, conversationId) + val conversation = result.first { it.conversationDetails.conversation.id == conversationId } - assertIs(conversation) assertEquals(conversation.unreadEventCount[UnreadEventType.MESSAGE], unreadMessagesCount) awaitComplete() @@ -1424,10 +1430,10 @@ class ConversationRepositoryTest { memberDAO, conversationApi, messageDAO, + messageDraftDAO, clientDao, clientApi, - conversationMetaDataDAO, - messageDraftDAO + conversationMetaDataDAO ) @@ -1508,7 +1514,7 @@ class ConversationRepositoryTest { suspend fun withConversations(conversations: List) = apply { coEvery { - conversationDAO.getAllConversationDetails(any()) + conversationDAO.getAllConversationDetails(any(), any()) }.returns(flowOf(conversations)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt index 37e358d4d5a..dac38b1d1cf 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/MLSConversationRepositoryTest.kt @@ -40,6 +40,7 @@ import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arr import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.CRYPTO_CLIENT_ID import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.E2EI_CONVERSATION_CLIENT_INFO_ENTITY import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.KEY_PACKAGE +import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.MLS_PUBLIC_KEY import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.ROTATE_BUNDLE import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.TEST_FAILURE import com.wire.kalium.logic.data.conversation.MLSConversationRepositoryTest.Arrangement.Companion.WIRE_IDENTITY @@ -53,7 +54,7 @@ import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.keypackage.KeyPackageLimitsProvider import com.wire.kalium.logic.data.keypackage.KeyPackageRepository import com.wire.kalium.logic.data.mls.CipherSuite -import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeys +import com.wire.kalium.logic.data.mls.MLSPublicKeys import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.framework.TestClient @@ -98,26 +99,30 @@ import io.mockative.matches import io.mockative.mock import io.mockative.once import io.mockative.twice +import io.mockative.verify import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.coroutines.yield import kotlinx.datetime.Instant +import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs class MLSConversationRepositoryTest { + @BeforeTest + @Test - fun givenCommitMessage_whenDecryptingMessage_thenEmitEpochChange() = runTest(TestKaliumDispatcher.default) { + fun givenCommitMessage_whenDecryptingMessage_thenEmitEpochChange() = runTest { val (arrangement, mlsConversationRepository) = Arrangement(testKaliumDispatcher) .withGetMLSClientSuccessful() .withDecryptMLSMessageSuccessful(Arrangement.DECRYPTED_MESSAGE_BUNDLE) .arrange() - val epochChange = async(TestKaliumDispatcher.default) { + val epochChange = async() { arrangement.epochsFlow.first() } yield() @@ -168,7 +173,7 @@ class MLSConversationRepositoryTest { .withSendCommitBundleSuccessful() .arrange() - val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1)) + val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = null) result.shouldSucceed() coVerify { @@ -280,6 +285,84 @@ class MLSConversationRepositoryTest { }.wasNotInvoked() } + @Test + fun givenPublicKeysIsNotNull_whenCallingEstablishMLSGroup_ThenGetPublicKeysRepositoryNotCalled() = runTest { + val (arrangement, mlsConversationRepository) = Arrangement(kaliumDispatcher = testKaliumDispatcher) + .withGetDefaultCipherSuite(CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + .withCommitPendingProposalsReturningNothing() + .withClaimKeyPackagesSuccessful() + .withGetMLSClientSuccessful() + .withKeyForCipherSuite() + .withAddMLSMemberSuccessful() + .withSendCommitBundleSuccessful() + .arrange() + + val result = + mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = MLS_PUBLIC_KEY) + result.shouldSucceed() + + coVerify { + arrangement.mlsClient.createConversation( + groupId = eq(Arrangement.RAW_GROUP_ID), + externalSenders = any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsClient.addMember( + groupId = eq(Arrangement.RAW_GROUP_ID), + membersKeyPackages = any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsMessageApi.sendCommitBundle(any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsClient.commitAccepted(eq(Arrangement.RAW_GROUP_ID)) + }.wasInvoked(once) + + coVerify { + arrangement.mlsPublicKeysRepository.getKeyForCipherSuite(any()) + }.wasNotInvoked() + } + + @Test + fun givenPublicKeysIsNull_whenCallingEstablishMLSGroup_ThenGetPublicKeysRepositoryIsCalled() = runTest { + val (arrangement, mlsConversationRepository) = Arrangement(testKaliumDispatcher) + .withGetDefaultCipherSuite(CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519) + .withCommitPendingProposalsReturningNothing() + .withClaimKeyPackagesSuccessful() + .withGetMLSClientSuccessful() + .withKeyForCipherSuite() + .withAddMLSMemberSuccessful() + .withSendCommitBundleSuccessful() + .arrange() + + val result = + mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = null) + result.shouldSucceed() + + coVerify { + arrangement.mlsClient.createConversation(eq(Arrangement.RAW_GROUP_ID), any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsClient.addMember(eq(Arrangement.RAW_GROUP_ID), any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsMessageApi.sendCommitBundle(any()) + }.wasInvoked(once) + + coVerify { + arrangement.mlsClient.commitAccepted(eq(Arrangement.RAW_GROUP_ID)) + }.wasInvoked(once) + + coVerify { + arrangement.mlsPublicKeysRepository.getKeyForCipherSuite(any()) + }.wasInvoked(once) + } + @Test fun givenNewCrlDistributionPoints_whenEstablishingMLSGroup_thenCheckRevocationList() = runTest { val (arrangement, mlsConversationRepository) = Arrangement(testKaliumDispatcher) @@ -329,7 +412,7 @@ class MLSConversationRepositoryTest { .withWaitUntilLiveSuccessful() .arrange() - val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1)) + val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = null) result.shouldSucceed() coVerify { @@ -357,7 +440,7 @@ class MLSConversationRepositoryTest { .withSendCommitBundleFailing(Arrangement.MLS_STALE_MESSAGE_ERROR) .arrange() - val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1)) + val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = null) result.shouldFail() coVerify { @@ -385,7 +468,7 @@ class MLSConversationRepositoryTest { .withSendCommitBundleSuccessful() .arrange() - val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1)) + val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, listOf(TestConversation.USER_1), publicKeys = null) result.shouldSucceed() coVerify { @@ -410,7 +493,7 @@ class MLSConversationRepositoryTest { .withSendCommitBundleSuccessful() .arrange() - val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, emptyList()) + val result = mlsConversationRepository.establishMLSGroup(Arrangement.GROUP_ID, emptyList(), publicKeys = null) result.shouldSucceed() coVerify { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt new file mode 100644 index 00000000000..77407d39b67 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/conversation/folders/ConversationFolderRepositoryTest.kt @@ -0,0 +1,282 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.conversation.folders + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.FolderWithConversations +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.di.MapperProvider +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.util.shouldFail +import com.wire.kalium.logic.util.shouldSucceed +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO +import com.wire.kalium.network.api.authenticated.properties.PropertyKey +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.api.model.ErrorResponse +import com.wire.kalium.network.exceptions.KaliumException +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity +import io.ktor.http.HttpStatusCode +import io.ktor.util.reflect.instanceOf +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.mock +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ConversationFolderRepositoryTest { + + @Test + fun givenFavoriteFolderExistsWhenFetchingFavoriteFolderThenShouldReturnFolderSuccessfully() = runTest { + // given + val folder = ConversationFolderEntity(id = "folder1", name = "Favorites", type = ConversationFolderTypeEntity.FAVORITE) + val arrangement = Arrangement().withFavoriteConversationFolder(folder) + + // when + val result = arrangement.repository.getFavoriteConversationFolder() + + // then + result.shouldSucceed { + assertEquals(folder.toModel(), it) + } + coVerify { arrangement.conversationFolderDAO.getFavoriteConversationFolder() }.wasInvoked() + } + + @Test + fun givenConversationsInFolderWhenObservingConversationsFromFolderThenShouldEmitConversationsList() = runTest { + // given + val folderId = "folder1" + val conversation = ConversationDetailsWithEventsEntity( + conversationViewEntity = TestConversation.VIEW_ENTITY, + lastMessage = null, + messageDraft = null, + unreadEvents = ConversationUnreadEventEntity(TestConversation.VIEW_ENTITY.id, mapOf()), + ) + + val conversations = listOf(conversation) + val arrangement = Arrangement().withConversationsFromFolder(folderId, conversations) + + // when + val resultFlow = arrangement.repository.observeConversationsFromFolder(folderId) + + // then + val emittedConversations = resultFlow.first() + assertEquals(arrangement.conversationMapper.fromDaoModelToDetailsWithEvents(conversations.first()), emittedConversations.first()) + } + + @Test + fun givenFolderDataWhenUpdatingConversationFoldersThenFoldersShouldBeUpdatedInDatabaseSuccessfully() = runTest { + // given + val folders = listOf( + FolderWithConversations( + id = "folder1", name = "Favorites", type = FolderType.FAVORITE, + conversationIdList = listOf() + ) + ) + val arrangement = Arrangement().withSuccessfulFolderUpdate() + + // when + val result = arrangement.repository.updateConversationFolders(folders) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked() + } + + @Test + fun givenNetworkFailureWhenFetchingConversationFoldersThenShouldReturnNetworkFailure() = runTest { + // given + val arrangement = Arrangement().withFetchConversationLabels(NetworkResponse.Error(KaliumException.NoNetwork())) + + // when + val result = arrangement.repository.fetchConversationFolders() + + // then + result.shouldFail { failure -> + failure.instanceOf(NetworkFailure.NoNetworkConnection::class) + } + } + + @Test + fun given404ErrorWhenFetchingFoldersThenShouldCreateEmptyLabelList() = runTest { + // given + val arrangement = Arrangement() + .withSetProperty(NetworkResponse.Success(Unit, emptyMap(), HttpStatusCode.OK.value)) + .withFetchConversationLabels( + NetworkResponse.Error( + KaliumException.InvalidRequestError( + errorResponse = ErrorResponse( + code = HttpStatusCode.NotFound.value, + message = "", + label = "" + ) + ) + ) + ) + + // when + val result = arrangement.repository.fetchConversationFolders() + + // then + result.shouldSucceed() + coVerify { + arrangement.userPropertiesApi.setProperty( + eq(PropertyKey.WIRE_LABELS), + any() + ) + }.wasInvoked() + + coVerify { arrangement.conversationFolderDAO.updateConversationFolders(any()) }.wasInvoked() + } + + @Test + fun givenValidConversationAndFolderWhenAddingConversationThenShouldAddSuccessfully() = runTest { + // given + val folderId = "folder1" + val conversationId = TestConversation.ID + val arrangement = Arrangement() + .withAddConversationToFolder() + .withGetFoldersWithConversations() + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.addConversationToFolder(conversationId, folderId) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.addConversationToFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked() + } + + @Test + fun givenValidConversationAndFolderWhenRemovingConversationThenShouldRemoveSuccessfully() = runTest { + // given + val folderId = "folder1" + val conversationId = TestConversation.ID + val arrangement = Arrangement() + .withRemoveConversationFromFolder() + .withGetFoldersWithConversations() + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.removeConversationFromFolder(conversationId, folderId) + + // then + result.shouldSucceed() + coVerify { arrangement.conversationFolderDAO.removeConversationFromFolder(eq(conversationId.toDao()), eq(folderId)) }.wasInvoked() + } + + @Test + fun givenLocalFoldersWhenSyncingFoldersThenShouldUpdateSuccessfully() = runTest { + // given + val folders = listOf( + FolderWithConversations( + id = "folder1", + name = "Favorites", + type = FolderType.FAVORITE, + conversationIdList = emptyList() + ) + ) + val arrangement = Arrangement() + .withGetFoldersWithConversations(folders) + .withUpdateLabels(NetworkResponse.Success(Unit, mapOf(), 200)) + + // when + val result = arrangement.repository.syncConversationFoldersFromLocal() + + // then + result.shouldSucceed() + coVerify { arrangement.userPropertiesApi.updateLabels(any()) }.wasInvoked() + coVerify { arrangement.conversationFolderDAO.getFoldersWithConversations() }.wasInvoked() + } + + private class Arrangement { + + @Mock + val conversationFolderDAO = mock(ConversationFolderDAO::class) + + @Mock + val userPropertiesApi = mock(PropertiesApi::class) + + private val selfUserId = TestUser.SELF.id + + val conversationMapper = MapperProvider.conversationMapper(selfUserId) + + val repository = ConversationFolderDataSource( + conversationFolderDAO = conversationFolderDAO, + userPropertiesApi = userPropertiesApi, + selfUserId = selfUserId + ) + + suspend fun withFavoriteConversationFolder(folder: ConversationFolderEntity): Arrangement { + coEvery { conversationFolderDAO.getFavoriteConversationFolder() }.returns(folder) + return this + } + + suspend fun withConversationsFromFolder(folderId: String, conversations: List): Arrangement { + coEvery { conversationFolderDAO.observeConversationListFromFolder(folderId) }.returns(flowOf(conversations)) + return this + } + + suspend fun withSuccessfulFolderUpdate(): Arrangement { + coEvery { conversationFolderDAO.updateConversationFolders(any()) }.returns(Unit) + return this + } + + suspend fun withFetchConversationLabels(response: NetworkResponse): Arrangement { + coEvery { userPropertiesApi.getLabels() }.returns(response) + return this + } + + suspend fun withSetProperty(response: NetworkResponse): Arrangement { + coEvery { userPropertiesApi.setProperty(any(), any()) }.returns(response) + return this + } + + suspend fun withUpdateLabels(response: NetworkResponse): Arrangement { + coEvery { userPropertiesApi.updateLabels(any()) }.returns(response) + return this + } + + suspend fun withGetFoldersWithConversations(folders: List = emptyList()): Arrangement { + coEvery { conversationFolderDAO.getFoldersWithConversations() }.returns(folders.map { it.toDao() }) + return this + } + + suspend fun withAddConversationToFolder(): Arrangement { + coEvery { conversationFolderDAO.addConversationToFolder(any(), any()) }.returns(Unit) + return this + } + + suspend fun withRemoveConversationFromFolder(): Arrangement { + coEvery { conversationFolderDAO.removeConversationFromFolder(any(), any()) }.returns(Unit) + return this + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt index 7e7d7f0edf2..8601cd4988b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt @@ -914,7 +914,7 @@ class E2EIRepositoryTest { arrangement.coreCryptoCentral.registerTrustAnchors(eq(Arrangement.RANDOM_BYTE_ARRAY.decodeToString())) }.wasInvoked(once) - verify { + coVerify { arrangement.userConfigRepository.setShouldFetchE2EITrustAnchors(eq(false)) }.wasInvoked(once) } @@ -933,7 +933,7 @@ class E2EIRepositoryTest { // then result.shouldSucceed() - verify { + coVerify { arrangement.userConfigRepository.getShouldFetchE2EITrustAnchor() }.wasInvoked(once) } @@ -1099,18 +1099,19 @@ class E2EIRepositoryTest { }.returns(result) } - fun withGetShouldFetchE2EITrustAnchors(result: Boolean) = apply { - every { + suspend fun withGetShouldFetchE2EITrustAnchors(result: Boolean) = apply { + coEvery { userConfigRepository.getShouldFetchE2EITrustAnchor() }.returns(result) } - fun withSetShouldFetchE2EIGetTrustAnchors() = apply { - every { + suspend fun withSetShouldFetchE2EIGetTrustAnchors() = apply { + coEvery { userConfigRepository.setShouldFetchE2EITrustAnchors(any()) }.returns(Unit) } + suspend fun withAcmeDirectoriesApiSucceed() = apply { coEvery { acmeApi.getACMEDirectories(any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/event/EventRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/event/EventRepositoryTest.kt index a1515f27195..ddff0e1063b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/event/EventRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/event/EventRepositoryTest.kt @@ -52,16 +52,18 @@ import kotlinx.datetime.Instant import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.assertNull class EventRepositoryTest { @Test fun givenPendingEvents_whenGettingPendingEvents_thenReturnPendingFirstFollowedByComplete() = runTest { val pendingEventPayload = EventContentDTO.Conversation.NewMessageDTO( - TestConversation.NETWORK_ID, - UserId("value", "domain"), - Instant.UNIX_FIRST_DATE, - MessageEventData("text", "senderId", "recipient") + qualifiedConversation = TestConversation.NETWORK_ID, + qualifiedFrom = UserId("value", "domain"), + time = Instant.UNIX_FIRST_DATE, + data = MessageEventData("text", "senderId", "recipient") ) val pendingEvent = EventResponse("pendingEventId", listOf(pendingEventPayload)) val notificationsPageResponse = NotificationResponse("time", false, listOf(pendingEvent)) @@ -136,6 +138,34 @@ class EventRepositoryTest { } } + @Test + fun givenAPIFailure_whenFetchingServerTime_thenReturnNull() = runTest { + val (_, eventRepository) = Arrangement() + .withGetServerTimeReturning(NetworkResponse.Error(KaliumException.NoNetwork())) + .arrange() + + val result = eventRepository.fetchServerTime() + + assertNull(result) + } + + + @Test + fun givenAPISucceeds_whenFetchingServerTime_thenReturnTime() = runTest { + val result = NetworkResponse.Success( + value = "123434545", + headers = mapOf(), + httpCode = HttpStatusCode.OK.value + ) + val (_, eventRepository) = Arrangement() + .withGetServerTimeReturning(result) + .arrange() + + val time = eventRepository.fetchServerTime() + + assertNotNull(time) + } + private companion object { const val LAST_PROCESSED_EVENT_ID_KEY = "last_processed_event_id" } @@ -158,12 +188,6 @@ class EventRepositoryTest { } } - suspend fun withDeleteMetadataSucceeding() = apply { - coEvery { - metaDAO.deleteValue(any()) - }.returns(Unit) - } - suspend fun withLastStoredEventId(value: String?) = apply { coEvery { metaDAO.valueByKey(LAST_PROCESSED_EVENT_ID_KEY) @@ -176,15 +200,15 @@ class EventRepositoryTest { }.returns(result) } - suspend fun withLastNotificationRemote(result: NetworkResponse) = apply { + suspend fun withOldestNotificationReturning(result: NetworkResponse) = apply { coEvery { - notificationApi.mostRecentNotification(any()) + notificationApi.oldestNotification(any()) }.returns(result) } - suspend fun withOldestNotificationReturning(result: NetworkResponse) = apply { + suspend fun withGetServerTimeReturning(result: NetworkResponse) = apply { coEvery { - notificationApi.oldestNotification(any()) + notificationApi.getServerTime(any()) }.returns(result) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/id/QualifiedIdTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/id/QualifiedIdTest.kt new file mode 100644 index 00000000000..15ddc78f92e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/id/QualifiedIdTest.kt @@ -0,0 +1,72 @@ +import com.wire.kalium.logic.data.id.QualifiedID +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + + +class QualifiedIdTest { + @Test + fun givenIdsWithoutDomains_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id1", "") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenOneIdWithoutDomain_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id1", "domain") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenIdsWithSameDomains_whenEqualsIgnoringBlankDomain_thenReturnsTrue() { + // Given + val qualifiedId1 = QualifiedID("id1", "domain") + val qualifiedId2 = QualifiedID("id1", "domain") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(result) + } + + @Test + fun givenIdsWithDifferentDomains_whenEqualsIgnoringBlankDomain_thenReturnsFalse() { + // Given + val qualifiedId1 = QualifiedID("id1", "domain1") + val qualifiedId2 = QualifiedID("id1", "domain2") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertTrue(!result) + } + + @Test + fun givenIdsWithDifferentValues_whenEqualsIgnoringBlankDomain_thenReturnsFalse() { + // Given + val qualifiedId1 = QualifiedID("id1", "") + val qualifiedId2 = QualifiedID("id2", "") + + // When + val result = qualifiedId1.equalsIgnoringBlankDomain(qualifiedId2) + + // Then + assertFalse(result) + } +} \ No newline at end of file diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt index 20daf4044a2..2462cc408d9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/MessageRepositoryTest.kt @@ -163,7 +163,7 @@ class MessageRepositoryTest { }.wasInvoked(exactly = once) coVerify { - messageDAO.insertOrIgnoreMessage(eq(mappedEntity), any(), any()) + messageDAO.insertOrIgnoreMessage(eq(mappedEntity), any()) }.wasInvoked(exactly = once) } } @@ -764,7 +764,7 @@ class MessageRepositoryTest { suspend fun withInsertOrIgnoreMessage(result: InsertMessageResult) = apply { coEvery { - messageDAO.insertOrIgnoreMessage(any(), any(), any()) + messageDAO.insertOrIgnoreMessage(any(), any()) }.returns(result) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCaseTest.kt new file mode 100644 index 00000000000..5d57fd7140e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/PersistMessageUseCaseTest.kt @@ -0,0 +1,148 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.data.message + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.notification.NotificationEventsManager +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.util.shouldFail +import com.wire.kalium.logic.util.shouldSucceed +import com.wire.kalium.persistence.dao.message.InsertMessageResult +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class PersistMessageUseCaseTest { + + @Test + fun givenMessageRepositoryFailure_whenPersistingMessage_thenReturnFailure() = runTest { + val (arrangement, persistMessage) = Arrangement() + .withPersistMessageFailure() + .withReceiptMode() + .arrange() + val message = TestMessage.TEXT_MESSAGE + + val result = persistMessage.invoke(message) + + result.shouldFail() + + coVerify { + arrangement.messageRepository.persistMessage(any(), any()) + }.wasInvoked(once) + + coVerify { + arrangement.messageRepository.getReceiptModeFromGroupConversationByQualifiedID(any()) + }.wasInvoked(once) + } + + @Test + fun givenAMessageAndMessageRepositorySuccess_whenPersistingMessage_thenScheduleRegularNotificationChecking() = + runTest { + val (arrangement, persistMessage) = Arrangement() + .withPersistMessageSuccess() + .withReceiptMode() + .arrange() + val message = TestMessage.TEXT_MESSAGE.copy( + senderUserId = UserId("id", "domain"), + ) + + val result = persistMessage.invoke(message) + + result.shouldSucceed() + + coVerify { + arrangement.messageRepository.persistMessage(any(), any()) + }.wasInvoked(once) + + coVerify { + arrangement.messageRepository.getReceiptModeFromGroupConversationByQualifiedID(any()) + }.wasInvoked(once) + + coVerify { + arrangement.notificationEventsManager.scheduleRegularNotificationChecking() + }.wasInvoked(once) + } + + @Test + fun givenSelfMessageAndMessageRepositorySuccess_whenPersistingMessage_thenDoNotScheduleRegularNotificationChecking() = + runTest { + val (arrangement, persistMessage) = Arrangement() + .withPersistMessageSuccess() + .withReceiptMode() + .arrange() + val message = TestMessage.TEXT_MESSAGE + + val result = persistMessage.invoke(message) + + result.shouldSucceed() + + coVerify { + arrangement.messageRepository.persistMessage(any(), any()) + }.wasInvoked(once) + + coVerify { + arrangement.messageRepository.getReceiptModeFromGroupConversationByQualifiedID(any()) + }.wasInvoked(once) + + coVerify { + arrangement.notificationEventsManager.scheduleRegularNotificationChecking() + }.wasNotInvoked() + } + + private class Arrangement { + @Mock + val messageRepository = mock(MessageRepository::class) + + @Mock + val notificationEventsManager = mock(NotificationEventsManager::class) + + fun arrange() = this to PersistMessageUseCaseImpl( + messageRepository = messageRepository, + selfUserId = TestUser.USER_ID, + notificationEventsManager = notificationEventsManager + ) + + suspend fun withPersistMessageSuccess() = apply { + coEvery { + messageRepository.persistMessage(any(), any()) + }.returns(Either.Right(InsertMessageResult.INSERTED_NEED_TO_NOTIFY_USER)) + } + + suspend fun withPersistMessageFailure() = apply { + coEvery { + messageRepository.persistMessage(any(), any()) + }.returns(Either.Left(CoreFailure.InvalidEventSenderID)) + } + + suspend fun withReceiptMode() = apply { + coEvery { + messageRepository.getReceiptModeFromGroupConversationByQualifiedID(any()) + }.returns(Either.Right(Conversation.ReceiptMode.ENABLED)) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt index 7fc7dffc73e..9bdd5147807 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ProtoContentMapperTest.kt @@ -31,6 +31,7 @@ import com.wire.kalium.protobuf.messages.Confirmation import com.wire.kalium.protobuf.messages.GenericMessage import com.wire.kalium.protobuf.messages.MessageEdit import com.wire.kalium.protobuf.messages.Text +import com.wire.kalium.protobuf.messages.UnknownStrategy import io.ktor.utils.io.core.toByteArray import kotlin.test.BeforeTest import kotlin.test.Test @@ -91,7 +92,8 @@ class ProtoContentMapperTest { val assetName = "Mocked-Asset.bin" val mockedAsset = assetName.toByteArray() val protobuf = GenericMessage( - TEST_MESSAGE_UUID, GenericMessage.Content.Asset( + messageId = TEST_MESSAGE_UUID, + content = GenericMessage.Content.Asset( Asset( original = Asset.Original( mimeType = "file/binary", @@ -193,7 +195,12 @@ class ProtoContentMapperTest { fun givenEditedTextGenericMessage_whenMappingFromProtoData_thenTheReturnValueShouldHaveTheCorrectEditedMessageId() { val replacedMessageId = "replacedMessageId" val textContent = MessageEdit.Content.Text(Text("textContent")) - val genericMessage = GenericMessage(TEST_MESSAGE_UUID, GenericMessage.Content.Edited(MessageEdit(replacedMessageId, textContent))) + val genericMessage = GenericMessage( + messageId = TEST_MESSAGE_UUID, + content = GenericMessage.Content.Edited( + MessageEdit(replacedMessageId, textContent) + ) + ) val protobufBlob = PlainMessageBlob(genericMessage.encodeToByteArray()) val result = protoContentMapper.decodeFromProtobuf(protobufBlob) @@ -208,7 +215,10 @@ class ProtoContentMapperTest { fun givenEditedTextGenericMessage_whenMappingFromProtoData_thenTheReturnValueShouldHaveTheCorrectUpdatedContent() { val replacedMessageId = "replacedMessageId" val textContent = MessageEdit.Content.Text(Text("textContent")) - val genericMessage = GenericMessage(TEST_MESSAGE_UUID, GenericMessage.Content.Edited(MessageEdit(replacedMessageId, textContent))) + val genericMessage = GenericMessage( + messageId = TEST_MESSAGE_UUID, + content = GenericMessage.Content.Edited(MessageEdit(replacedMessageId, textContent)) + ) val protobufBlob = PlainMessageBlob(genericMessage.encodeToByteArray()) val result = protoContentMapper.decodeFromProtobuf(protobufBlob) @@ -324,8 +334,8 @@ class ProtoContentMapperTest { val messageUid = "uid" val protobuf = GenericMessage( - messageUid, - GenericMessage.Content.Confirmation(Confirmation(Confirmation.Type.fromValue(-1), messageUid)) + messageId = messageUid, + content = GenericMessage.Content.Confirmation(Confirmation(Confirmation.Type.fromValue(-1), messageUid)) ) val decoded = protoContentMapper.decodeFromProtobuf(PlainMessageBlob(protobuf.encodeToByteArray())) @@ -333,6 +343,62 @@ class ProtoContentMapperTest { assertIs(decoded.messageContent) } + @Test + fun givenNonParseableContentWithDefaultUnknownStrategy_whenMappingFromProto_thenShouldReturnIgnoredContent() { + val protobuf = GenericMessage( + messageId = "uid" + ) + + val decoded = protoContentMapper.decodeFromProtobuf(PlainMessageBlob(protobuf.encodeToByteArray())) + + assertIs(decoded) + assertIs(decoded.messageContent) + } + + @Test + fun givenNonParseableContentWithUnknownStrategyIgnore_whenMappingFromProto_thenShouldReturnIgnoredContent() { + val protobuf = GenericMessage( + messageId = "uid", + unknownStrategy = UnknownStrategy.IGNORE + ) + + val decoded = protoContentMapper.decodeFromProtobuf(PlainMessageBlob(protobuf.encodeToByteArray())) + + assertIs(decoded) + assertIs(decoded.messageContent) + } + + @Test + fun givenNonParseableContentWithUnknownStrategyDiscardAndWarn_whenMappingFromProto_thenShouldReturnUnknownContentWithoutByteData() { + val protobuf = GenericMessage( + messageId = "uid", + unknownStrategy = UnknownStrategy.DISCARD_AND_WARN + ) + + val decoded = protoContentMapper.decodeFromProtobuf(PlainMessageBlob(protobuf.encodeToByteArray())) + + assertIs(decoded) + val content = decoded.messageContent + assertIs(content) + assertEquals(content.encodedData, null) + } + + @Test + fun givenNonParseableContentWithUnknownStrategyWarnUserAllowEntry_whenMappingFromProto_thenShouldReturnUnknownContentWithByteData() { + val protobuf = GenericMessage( + messageId = "uid", + unknownStrategy = UnknownStrategy.WARN_USER_ALLOW_RETRY + ) + val protobufByteArray = protobuf.encodeToByteArray() + + val decoded = protoContentMapper.decodeFromProtobuf(PlainMessageBlob(protobufByteArray)) + + assertIs(decoded) + val content = decoded.messageContent + assertIs(content) + assertEquals(content.encodedData, protobufByteArray) + } + @Test fun givenExternalMessageInstructions_whenEncodingToProtoAndBack_thenTheResultContentShouldEqualTheOriginal() { val messageUid = TEST_MESSAGE_UUID diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ReactionsMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ReactionsMapperTest.kt index 204d3fbad76..d8a70287f98 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ReactionsMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ReactionsMapperTest.kt @@ -59,7 +59,8 @@ class ReactionsMapperTest { userType = UserTypeEntity.STANDARD, deleted = false, connectionStatus = ConnectionEntity.State.ACCEPTED, - userAvailabilityStatus = UserAvailabilityStatusEntity.NONE + userAvailabilityStatus = UserAvailabilityStatusEntity.NONE, + accentId = 0 ) val expectedMessageReactionEntity = MessageReactionEntity( @@ -71,7 +72,8 @@ class ReactionsMapperTest { userTypeEntity = UserTypeEntity.STANDARD, deleted = false, connectionStatus = ConnectionEntity.State.ACCEPTED, - availabilityStatus = UserAvailabilityStatusEntity.NONE + availabilityStatus = UserAvailabilityStatusEntity.NONE, + accentId = 0 ) val (_, reactionsMapper) = Arrangement() @@ -99,7 +101,8 @@ class ReactionsMapperTest { userTypeEntity = UserTypeEntity.STANDARD, deleted = false, connectionStatus = ConnectionEntity.State.ACCEPTED, - availabilityStatus = UserAvailabilityStatusEntity.NONE + availabilityStatus = UserAvailabilityStatusEntity.NONE, + accentId = 0 ) val expectedMessageReaction = MessageReaction( @@ -113,7 +116,8 @@ class ReactionsMapperTest { userType = UserType.INTERNAL, isUserDeleted = false, connectionStatus = ConnectionState.ACCEPTED, - availabilityStatus = UserAvailabilityStatus.NONE + availabilityStatus = UserAvailabilityStatus.NONE, + accentId = 0 ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest.kt index bad8ecd2941..a3eb57969ef 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/ephemeral/DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest.kt @@ -19,10 +19,12 @@ package com.wire.kalium.logic.data.message.ephemeral import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.AssetContent import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageContent -import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.data.message.MessageEncryptionAlgorithm import com.wire.kalium.logic.data.message.MessageTarget +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsReceiverUseCase import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl import com.wire.kalium.logic.functional.Either @@ -37,10 +39,10 @@ import com.wire.kalium.logic.util.arrangement.repository.AssetRepositoryArrangem import com.wire.kalium.logic.util.arrangement.repository.MessageRepositoryArrangement import com.wire.kalium.logic.util.arrangement.repository.MessageRepositoryArrangementImpl import com.wire.kalium.logic.util.shouldSucceed -import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import io.mockative.any -import io.mockative.matches import io.mockative.coVerify +import io.mockative.matchers.EqualsMatcher +import io.mockative.matches import io.mockative.once import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant @@ -50,27 +52,15 @@ class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest { @Test fun givenMessage_whenDeleting_then2DeleteMessagesAreSentForSelfAndOriginalSender() = runTest { - val messageId = "messageId" - val conversationId = ConversationId("conversationId", "conversationDomain.com") - val currentClientId = CURRENT_CLIENT_ID - - val senderUserID = UserId("senderUserId", "senderUserDomain.com") - val message = Message.Regular( - id = messageId, - content = MessageContent.Text("text"), - conversationId = conversationId, - date = Instant.DISTANT_FUTURE, - senderUserId = senderUserID, - senderClientId = currentClientId, - status = Message.Status.Pending, - editStatus = Message.EditStatus.NotEdited, - isSelfMessage = true - ) + val message = MESSAGE_REGULAR + val messageId = message.id + val conversationId = message.conversationId + val senderUserId = message.senderUserId val (arrangement, useCase) = Arrangement() .arrange { withMarkAsDeleted(Either.Right(Unit)) withCurrentClientIdSuccess(CURRENT_CLIENT_ID) - withSelfConversationIds(SELF_CONVERSION_ID) + withSelfConversationIds(SELF_CONVERSATION_ID) withGetMessageById(Either.Right(message)) withSendMessageSucceed() withDeleteMessage(Either.Right(Unit)) @@ -85,7 +75,7 @@ class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest { coVerify { arrangement.messageSender.sendMessage( matches { - it.conversationId == SELF_CONVERSION_ID.first() && + it.conversationId == SELF_CONVERSATION_ID.first() && it.content == MessageContent.DeleteForMe(messageId, conversationId) }, matches { @@ -101,7 +91,7 @@ class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest { it.content == MessageContent.DeleteMessage(messageId) }, matches { - it == MessageTarget.Users(listOf(senderUserID)) + it == MessageTarget.Users(listOf(senderUserId)) } ) }.wasInvoked(exactly = once) @@ -111,10 +101,61 @@ class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest { }.wasInvoked(exactly = once) } + @Test + fun givenAssetMessage_whenDeleting_thenDeleteAssetLocally() = runTest { + val assetContent = ASSET_IMAGE_CONTENT + val message = MESSAGE_REGULAR.copy( + content = MessageContent.Asset(assetContent) + ) + val (arrangement, useCase) = Arrangement() + .arrange { + withCurrentClientIdSuccess(CURRENT_CLIENT_ID) + withSelfConversationIds(SELF_CONVERSATION_ID) + withSendMessageSucceed() + withDeleteMessage(Either.Right(Unit)) + withMarkAsDeleted(Either.Right(Unit), EqualsMatcher(message.id), EqualsMatcher(message.conversationId)) + withGetMessageById(Either.Right(message), EqualsMatcher(message.id), EqualsMatcher(message.conversationId)) + withDeleteAssetLocally(Either.Right(Unit), EqualsMatcher(assetContent.remoteData.assetId)) + } + + useCase(message.conversationId, message.id).shouldSucceed() + + coVerify { + arrangement.assetRepository.deleteAssetLocally(assetContent.remoteData.assetId) + }.wasInvoked(exactly = once) + } + private companion object { - val selfUserId = UserId("selfUserId", "selfUserDomain.sy") - val SELF_CONVERSION_ID = listOf(ConversationId("selfConversationId", "selfConversationDomain.com")) + val SELF_USER_ID = UserId("selfUserId", "selfUserDomain.sy") + val SENDER_USER_ID = UserId("senderUserId", "senderDomain") + val SELF_CONVERSATION_ID = listOf(ConversationId("selfConversationId", "selfConversationDomain.com")) val CURRENT_CLIENT_ID = ClientId("currentClientId") + val ASSET_CONTENT_REMOTE_DATA = AssetContent.RemoteData( + otrKey = ByteArray(0), + sha256 = ByteArray(16), + assetId = "asset-id", + assetToken = "==some-asset-token", + assetDomain = "some-asset-domain.com", + encryptionAlgorithm = MessageEncryptionAlgorithm.AES_GCM + ) + val ASSET_IMAGE_CONTENT = AssetContent( + 0L, + "name", + "image/jpg", + AssetContent.AssetMetadata.Image(100, 100), + ASSET_CONTENT_REMOTE_DATA + ) + val MESSAGE_REGULAR = Message.Regular( + id = "messageId", + content = MessageContent.Text("text"), + conversationId = ConversationId("conversationId", "conversationDomain"), + date = Instant.DISTANT_FUTURE, + senderUserId = SENDER_USER_ID, + senderClientId = CURRENT_CLIENT_ID, + status = Message.Status.Pending, + editStatus = Message.EditStatus.NotEdited, + isSelfMessage = true + ) } private class Arrangement : @@ -128,7 +169,7 @@ class DeleteEphemeralMessageForSelfUserAsReceiverUseCaseTest { DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl( messageRepository = messageRepository, messageSender = messageSender, - selfUserId = selfUserId, + selfUserId = SELF_USER_ID, selfConversationIdProvider = selfConversationIdProvider, assetRepository = assetRepository, currentClientIdProvider = currentClientIdProvider diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapperTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapperTest.kt index 4cbaf6517ef..27e82c06925 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapperTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/message/receipt/ReceiptsMapperTest.kt @@ -118,7 +118,8 @@ class ReceiptsMapperTest { isUserDeleted = false, connectionStatus = ConnectionEntity.State.ACCEPTED, availabilityStatus = UserAvailabilityStatusEntity.NONE, - date = date + date = date, + accentId = 0 ) val expectedDetailedReceipt = DetailedReceipt( @@ -132,7 +133,8 @@ class ReceiptsMapperTest { userType = UserType.INTERNAL, isUserDeleted = false, connectionStatus = ConnectionState.ACCEPTED, - availabilityStatus = UserAvailabilityStatus.NONE + availabilityStatus = UserAvailabilityStatus.NONE, + accentId = 0 ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/PreKeyRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/PreKeyRepositoryTest.kt index 3a3df9fee55..a185091158c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/PreKeyRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/prekey/PreKeyRepositoryTest.kt @@ -177,7 +177,7 @@ class PreKeyRepositoryTest { @Test fun givenCreatingSessionsThrows_whenPreparingSessions_thenItShouldFail() = runTest { - val exception = ProteusException("PANIC!!!11!eleven!", ProteusException.Code.PANIC) + val exception = ProteusException("PANIC!!!11!eleven!", ProteusException.Code.PANIC, 15) val preKey = PreKeyDTO(42, "encodedData") val userPreKeysResult = diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt index b4890d9af59..f47c7bff692 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/properties/UserPropertyRepositoryTest.kt @@ -20,6 +20,7 @@ package com.wire.kalium.logic.data.properties import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi @@ -101,7 +102,9 @@ class UserPropertyRepositoryTest { @Mock val userConfigRepository = mock(UserConfigRepository::class) - private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository) + private val selfUserId = TestUser.SELF.id + + private val userPropertyRepository = UserPropertyDataSource(propertiesApi, userConfigRepository, selfUserId) suspend fun withUpdateReadReceiptsSuccess() = apply { coEvery { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncRepositoryTest.kt index b420231fbdc..0d676a51692 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/sync/IncrementalSyncRepositoryTest.kt @@ -41,6 +41,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals +import kotlin.time.Duration import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.seconds @@ -87,7 +88,7 @@ class IncrementalSyncRepositoryTest { IncrementalSyncStatus.FetchingPendingEvents, IncrementalSyncStatus.Live, IncrementalSyncStatus.Pending, - IncrementalSyncStatus.Failed(NetworkFailure.NoNetworkConnection(null)), + IncrementalSyncStatus.Failed(NetworkFailure.NoNetworkConnection(null), Duration.ZERO), IncrementalSyncStatus.FetchingPendingEvents ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt index 7a952bac93b..eb7cf4dab64 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/user/UserRepositoryTest.kt @@ -44,15 +44,17 @@ import com.wire.kalium.logic.test_util.TestNetworkException.generic import com.wire.kalium.logic.test_util.TestNetworkResponseError import com.wire.kalium.logic.util.shouldFail import com.wire.kalium.logic.util.shouldSucceed -import com.wire.kalium.network.api.base.authenticated.TeamsApi -import com.wire.kalium.network.api.base.authenticated.self.SelfApi import com.wire.kalium.network.api.authenticated.teams.TeamMemberDTO import com.wire.kalium.network.api.authenticated.teams.TeamMemberListNonPaginated +import com.wire.kalium.network.api.authenticated.user.CreateUserTeamDTO import com.wire.kalium.network.api.authenticated.userDetails.ListUserRequest import com.wire.kalium.network.api.authenticated.userDetails.ListUsersDTO import com.wire.kalium.network.api.authenticated.userDetails.QualifiedUserIdListRequest -import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.authenticated.userDetails.qualifiedIds +import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.self.SelfApi +import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi import com.wire.kalium.network.api.model.LegalHoldStatusDTO import com.wire.kalium.network.api.model.UserProfileDTO import com.wire.kalium.network.utils.NetworkResponse @@ -693,7 +695,8 @@ class UserRepositoryTest { id = QualifiedIDEntity("id", "domain"), name = "Max", userType = UserTypeEntity.ADMIN, - completeAssetId = null + completeAssetId = null, + accentId = 0 ) val (arrangement, userRepository) = Arrangement() .withUserDAOReturning(userMinimized) @@ -800,6 +803,37 @@ class UserRepositoryTest { }.wasInvoked(exactly = once) } + @Test + fun givenApiRequestSucceeds_whenPersonalUserUpgradesToTeam_thenShouldSucceed() = runTest { + // given + val (arrangement, userRepository) = Arrangement() + .withRemoteGetSelfReturningDeletedUser() + .withMigrateUserToTeamSuccess() + .arrange() + // when + val result = userRepository.migrateUserToTeam("teamName") + // then + result.shouldSucceed() + coVerify { + arrangement.upgradePersonalToTeamApi.migrateToTeam(any()) + }.wasInvoked(exactly = once) + } + + @Test + fun givenApiRequestFails_whenPersonalUserUpgradesToTeam_thenShouldPropagateError() = runTest { + // given + val (arrangement, userRepository) = Arrangement() + .withMigrateUserToTeamFailure() + .arrange() + // when + val result = userRepository.migrateUserToTeam("teamName") + // then + result.shouldFail() + coVerify { + arrangement.upgradePersonalToTeamApi.migrateToTeam(any()) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val userDAO = mock(UserDAO::class) @@ -828,20 +862,25 @@ class UserRepositoryTest { @Mock val legalHoldHandler: LegalHoldHandler = mock(LegalHoldHandler::class) + @Mock + val upgradePersonalToTeamApi: UpgradePersonalToTeamApi = + mock(UpgradePersonalToTeamApi::class) + val selfUserId = TestUser.SELF.id val userRepository: UserRepository by lazy { UserDataSource( - userDAO, - metadataDAO, - clientDAO, - selfApi, - userDetailsApi, - teamsApi, - sessionRepository, - selfUserId, - selfTeamIdProvider, - legalHoldHandler + userDAO = userDAO, + metadataDAO = metadataDAO, + clientDAO = clientDAO, + selfApi = selfApi, + userDetailsApi = userDetailsApi, + teamsApi = teamsApi, + sessionRepository = sessionRepository, + selfUserId = selfUserId, + selfTeamIdProvider = selfTeamIdProvider, + legalHoldHandler = legalHoldHandler, + upgradePersonalToTeamApi = upgradePersonalToTeamApi, ) } @@ -1028,6 +1067,24 @@ class UserRepositoryTest { }.returns(NetworkResponse.Success(result, mapOf(), 200)) } + suspend fun withMigrateUserToTeamSuccess() = apply { + coEvery { + upgradePersonalToTeamApi.migrateToTeam(any()) + }.returns( + NetworkResponse.Success( + CreateUserTeamDTO("teamId", "teamName"), + mapOf(), + 200 + ) + ) + } + + suspend fun withMigrateUserToTeamFailure() = apply { + coEvery { + upgradePersonalToTeamApi.migrateToTeam(any()) + }.returns(NetworkResponse.Error(generic)) + } + suspend inline fun arrange(block: (Arrangement.() -> Unit) = { }): Pair { withSelfUserIdFlowMetadataReturning(flowOf(TestUser.JSON_QUALIFIED_ID)) coEvery { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCaseTest.kt new file mode 100644 index 00000000000..566e174e0e6 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/analytics/GetCurrentAnalyticsTrackingIdentifierUseCaseTest.kt @@ -0,0 +1,63 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.analytics + +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl +import io.mockative.coVerify +import io.mockative.once +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetCurrentAnalyticsTrackingIdentifierUseCaseTest { + + @Test + fun givenCurrentAnalyticsTrackingId_whenGettingTrackingId_thenCurrentTrackingIdIsReturned() = runTest { + // given + val (arrangement, useCase) = Arrangement().arrange { + withGetTrackingIdentifier(CURRENT_IDENTIFIER) + } + + // when + val result = useCase() + + // then + assertEquals(CURRENT_IDENTIFIER, result) + coVerify { + arrangement.userConfigRepository.getCurrentTrackingIdentifier() + }.wasInvoked(exactly = once) + } + + private companion object { + const val CURRENT_IDENTIFIER = "efgh-5678" + } + + private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl() { + + private val useCase: GetCurrentAnalyticsTrackingIdentifierUseCase = GetCurrentAnalyticsTrackingIdentifierUseCase( + userConfigRepository = userConfigRepository + ) + + fun arrange(block: suspend Arrangement.() -> Unit): Pair { + runBlocking { block() } + return this to useCase + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCaseTest.kt index 94ad32bd51c..52de2380446 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ScheduleNewAssetMessageUseCaseTest.kt @@ -628,16 +628,21 @@ class ScheduleNewAssetMessageUseCaseTest { // Then assertTrue(result is ScheduleNewAssetMessageResult.Failure.RestrictedFileType) - verify { - arrangement.validateAssetMimeTypeUseCase(eq("text/plain"), eq(listOf("png"))) - }.wasInvoked(exactly = once) + coVerify { + arrangement.validateAssetMimeTypeUseCase( + fileName = eq("some-asset.txt"), + mimeType = eq("text/plain"), + allowedExtension = eq(listOf("png")) + ) + } + .wasInvoked(exactly = once) } @Test fun givenAssetMimeTypeRestrictedAndFileAllowed_whenSending_thenReturnSendTheFile() = runTest(testDispatcher.default) { // Given val assetToSend = mockedLongAssetData() - val assetName = "some-asset.txt" + val assetName = "some-asset.png" val inputDataPath = fakeKaliumFileSystem.providePersistentAssetPath(assetName) val expectedAssetId = dummyUploadedAssetId val expectedAssetSha256 = SHA256Key("some-asset-sha-256".toByteArray()) @@ -668,9 +673,14 @@ class ScheduleNewAssetMessageUseCaseTest { // Then assertTrue(result is ScheduleNewAssetMessageResult.Success) - verify { - arrangement.validateAssetMimeTypeUseCase(eq("image/png"), eq(listOf("png"))) - }.wasInvoked(exactly = once) + coVerify { + arrangement.validateAssetMimeTypeUseCase( + fileName = eq("some-asset.png"), + mimeType = eq("image/png"), + allowedExtension = eq(listOf("png")) + ) + } + .wasInvoked(exactly = once) } private class Arrangement(val coroutineScope: CoroutineScope) { @@ -706,7 +716,7 @@ class ScheduleNewAssetMessageUseCaseTest { private val messageRepository: MessageRepository = mock(MessageRepository::class) @Mock - val validateAssetMimeTypeUseCase: ValidateAssetMimeTypeUseCase = mock(ValidateAssetMimeTypeUseCase::class) + val validateAssetMimeTypeUseCase: ValidateAssetFileTypeUseCase = mock(ValidateAssetFileTypeUseCase::class) @Mock val observerFileSharingStatusUseCase: ObserveFileSharingStatusUseCase = mock(ObserveFileSharingStatusUseCase::class) @@ -723,7 +733,7 @@ class ScheduleNewAssetMessageUseCaseTest { fun withValidateAsseMimeTypeResult(result: Boolean) = apply { every { - validateAssetMimeTypeUseCase.invoke(any(), any()) + validateAssetMimeTypeUseCase.invoke(any(), any(), any()) }.returns(result) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCaseTest.kt new file mode 100644 index 00000000000..9d263a8e337 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetFileTypeUseCaseTest.kt @@ -0,0 +1,103 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.asset + +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class ValidateAssetFileTypeUseCaseTest { + + @Test + fun givenRegularFileNameWithAllowedExtension_whenInvoke_thenBeApproved() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = "name.txt", mimeType = "", allowedExtension = listOf("txt", "jpg")) + + assertTrue(result) + } + + @Test + fun givenRegularFileNameWithNOTAllowedExtension_whenInvoke_thenBeRestricted() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = "name.php", mimeType = "", allowedExtension = listOf("txt", "jpg")) + + assertFalse(result) + } + + @Test + fun givenRegularFileNameWithoutExtension_whenInvoke_thenBeRestricted() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = "name", mimeType = "", allowedExtension = listOf("txt", "jpg")) + + assertFalse(result) + } + + @Test + fun givenNullFileName_whenInvoke_thenBeRestricted() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = null, mimeType = "", allowedExtension = listOf("txt", "jpg")) + + assertFalse(result) + } + + @Test + fun givenFileNameIs() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = null, mimeType = "image/jpg", allowedExtension = listOf("txt", "jpg")) + + assertFalse(result) + } + + @Test + fun givenNullFileNameAndValidMimeType_whenInvoke_thenMimeTypeIsChecked() = runTest { + val (_, validate) = arrange {} + + val result = validate(fileName = null, mimeType = "image/jpg", allowedExtension = listOf("txt", "jpg")) + + assertFalse(result) + } + + @Test + fun givenNullFileNameAndInvalidMimeType_whenInvoke_thenMimeTypeIsChecked() = runTest { + val (_, validate) = arrange {} + + val result = validate( + fileName = null, + mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + allowedExtension = listOf("txt", "jpg") + ) + + assertFalse(result) + } + + private fun arrange(block: Arrangement.() -> Unit) = Arrangement(block).arrange() + + private class Arrangement( + private val block: Arrangement.() -> Unit + ) { + fun arrange() = block().run { + this@Arrangement to ValidateAssetFileTypeUseCaseImpl() + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt index c680b29fa84..f443b8a5914 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/auth/LogoutUseCaseTest.kt @@ -51,7 +51,6 @@ import io.mockative.every import io.mockative.mock import io.mockative.once import io.mockative.time -import io.mockative.verify import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope @@ -342,6 +341,39 @@ class LogoutUseCaseTest { }.wasInvoked(exactly = calls.size.time) } + @Test + fun givenAMigrationFailedLogout_whenLoggingOut_thenExecuteAllRequiredActions() = runTest { + val reason = LogoutReason.MIGRATION_TO_CC_FAILED + val (arrangement, logoutUseCase) = Arrangement() + .withLogoutResult(Either.Right(Unit)) + .withSessionLogoutResult(Either.Right(Unit)) + .withAllValidSessionsResult(Either.Right(listOf(Arrangement.VALID_ACCOUNT_INFO))) + .withDeregisterTokenResult(DeregisterTokenUseCase.Result.Success) + .withClearCurrentClientIdResult(Either.Right(Unit)) + .withClearRetainedClientIdResult(Either.Right(Unit)) + .withUserSessionScopeGetResult(null) + .withFirebaseTokenUpdate() + .withNoOngoingCalls() + .arrange() + + logoutUseCase.invoke(reason) + arrangement.globalTestScope.advanceUntilIdle() + + coVerify { + arrangement.clearClientDataUseCase.invoke() + }.wasInvoked(exactly = once) + coVerify { + arrangement.logoutRepository.clearClientRelatedLocalMetadata() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.clientRepository.clearRetainedClientId() + }.wasInvoked(exactly = once) + coVerify { + arrangement.pushTokenRepository.setUpdateFirebaseTokenFlag(eq(true)) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val logoutRepository = mock(LogoutRepository::class) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/AnswerCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/AnswerCallUseCaseTest.kt index 9dfc156e3d6..38da64e9139 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/AnswerCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/AnswerCallUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.CallManager import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.logic.test_util.TestKaliumDispatcher import com.wire.kalium.logic.test_util.testKaliumDispatcher import io.mockative.Mock @@ -170,7 +171,7 @@ class AnswerCallUseCaseTest { isMuted = true, isCameraOn = false, isCbrEnabled = false, - callerId = "id", + callerId = TestCall.CALLER_ID, conversationName = "caller-name", conversationType = Conversation.Type.GROUP, callerName = "Name", diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt index c059b7f02a1..77cbac0d93a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallOnConversationChangeUseCaseTest.kt @@ -176,7 +176,7 @@ class EndCallOnConversationChangeUseCaseTest { endCall.invoke(eq(conversationId)) }.returns(Unit) coEvery { - endCallDialogManager.onCallEndedBecauseOfVerificationDegraded(eq(conversationId)) + endCallDialogManager.onCallEndedBecauseOfVerificationDegraded() }.returns(Unit) withEstablishedCallsFlow(listOf(call)) @@ -190,7 +190,7 @@ class EndCallOnConversationChangeUseCaseTest { private val call = Call( conversationId = conversationId, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = UserId("called-id", "domain"), isMuted = false, isCameraOn = false, isCbrEnabled = false, @@ -248,18 +248,13 @@ class EndCallOnConversationChangeUseCaseTest { private val groupConversationDetail = ConversationDetails.Group( conversation = conversation, hasOngoingCall = true, - unreadEventCount = mapOf(), - lastMessage = null, isSelfUserMember = false, - isSelfUserCreator = false, selfRole = null ) private val oneOnOneConversationDetail = ConversationDetails.OneOne( conversation = conversation, otherUser = otherUser, - unreadEventCount = mapOf(), - lastMessage = null, userType = UserType.ADMIN ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt index 3e3d10c7c28..76cd6521604 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/EndCallUseCaseTest.kt @@ -19,68 +19,57 @@ package com.wire.kalium.logic.feature.call.usecase import com.wire.kalium.logic.data.call.Call -import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.call.CallManager +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.user.ShouldAskCallFeedbackUseCase import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.logic.util.arrangement.repository.CallManagerArrangement +import com.wire.kalium.logic.util.arrangement.repository.CallManagerArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.CallRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.CallRepositoryArrangementImpl import io.mockative.Mock import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify import io.mockative.doesNothing import io.mockative.eq -import io.mockative.every import io.mockative.mock import io.mockative.once import io.mockative.verify import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest -import kotlin.test.BeforeTest import kotlin.test.Test class EndCallUseCaseTest { - @Mock - private val callManager = mock(CallManager::class) - - @Mock - private val callRepository = mock(CallRepository::class) - - private lateinit var endCall: EndCallUseCase - - @BeforeTest - fun setup() = runBlocking { - endCall = EndCallUseCaseImpl(lazy { callManager }, callRepository, TestKaliumDispatcher) - - coEvery { - callManager.endCall(eq(conversationId)) - }.returns(Unit) - - every { callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) } - .doesNothing() - } - @Test fun givenAnEstablishedCall_whenEndCallIsInvoked_thenUpdateStatusAndInvokeEndCallOnce() = runTest(TestKaliumDispatcher.main) { - coEvery { - callRepository.callsFlow() - }.returns(flowOf(listOf(call))) + + val (arrangement, endCall) = Arrangement().arrange { + withEndCall() + withCallsFlow(flowOf(listOf(call))) + withUpdateIsCameraOnById() + } endCall.invoke(conversationId) coVerify { - callManager.endCall(eq(conversationId)) + arrangement.callManager.endCall(eq(conversationId)) }.wasInvoked(once) verify { - callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + arrangement.callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) }.wasInvoked(once) coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + arrangement.callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) + + coVerify { + arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) }.wasInvoked(once) } @@ -90,23 +79,28 @@ class EndCallUseCaseTest { status = CallStatus.STILL_ONGOING, conversationType = Conversation.Type.GROUP ) - - coEvery { - callRepository.callsFlow() - }.returns(flowOf(listOf(stillOngoingCall))) + val (arrangement, endCall) = Arrangement().arrange { + withEndCall() + withCallsFlow(flowOf(listOf(stillOngoingCall))) + withUpdateIsCameraOnById() + } endCall.invoke(conversationId) coVerify { - callManager.endCall(eq(conversationId)) + arrangement.callManager.endCall(eq(conversationId)) }.wasInvoked(once) verify { - callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + arrangement.callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + }.wasInvoked(once) + + coVerify { + arrangement.callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) }.wasInvoked(once) coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) + arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) }.wasInvoked(once) } @@ -116,23 +110,28 @@ class EndCallUseCaseTest { status = CallStatus.STARTED, conversationType = Conversation.Type.GROUP ) - - coEvery { - callRepository.callsFlow() - }.returns(flowOf(listOf(stillOngoingCall))) + val (arrangement, endCall) = Arrangement().arrange { + withEndCall() + withCallsFlow(flowOf(listOf(stillOngoingCall))) + withUpdateIsCameraOnById() + } endCall.invoke(conversationId) coVerify { - callManager.endCall(eq(conversationId)) + arrangement.callManager.endCall(eq(conversationId)) }.wasInvoked(once) verify { - callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + arrangement.callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + }.wasInvoked(once) + + coVerify { + arrangement.callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) }.wasInvoked(once) coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) + arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) }.wasInvoked(once) } @@ -142,23 +141,28 @@ class EndCallUseCaseTest { status = CallStatus.INCOMING, conversationType = Conversation.Type.GROUP ) - - coEvery { - callRepository.callsFlow() - }.returns(flowOf(listOf(stillOngoingCall))) + val (arrangement, endCall) = Arrangement().arrange { + withEndCall() + withCallsFlow(flowOf(listOf(stillOngoingCall))) + withUpdateIsCameraOnById() + } endCall.invoke(conversationId) coVerify { - callManager.endCall(eq(conversationId)) + arrangement.callManager.endCall(eq(conversationId)) }.wasInvoked(once) verify { - callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + arrangement.callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + }.wasInvoked(once) + + coVerify { + arrangement.callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) }.wasInvoked(once) coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED_INTERNALLY)) + arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) }.wasInvoked(once) } @@ -167,24 +171,62 @@ class EndCallUseCaseTest { val closedCall = call.copy( status = CallStatus.CLOSED ) - - coEvery { - callRepository.callsFlow() - }.returns(flowOf(listOf(closedCall))) + val (arrangement, endCall) = Arrangement().arrange { + withEndCall() + withCallsFlow(flowOf(listOf(closedCall))) + withUpdateIsCameraOnById() + } endCall.invoke(conversationId) coVerify { - callManager.endCall(eq(conversationId)) + arrangement.callManager.endCall(eq(conversationId)) }.wasInvoked(once) verify { - callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) + arrangement.callRepository.updateIsCameraOnById(eq(conversationId), eq(false)) }.wasInvoked(once) coVerify { - callRepository.updateCallStatusById(any(), any()) + arrangement.callRepository.updateCallStatusById(any(), any()) }.wasNotInvoked() + + coVerify { + arrangement.endCallResultListener.onCallEndedAskForFeedback(eq(false)) + }.wasInvoked(once) + } + + private class Arrangement : CallRepositoryArrangement by CallRepositoryArrangementImpl(), + CallManagerArrangement by CallManagerArrangementImpl() { + + @Mock + val endCallResultListener = mock(EndCallResultListener::class) + + @Mock + val shouldAskCallFeedback = mock(ShouldAskCallFeedbackUseCase::class) + + fun arrange(block: suspend Arrangement.() -> Unit): Pair { + runBlocking { + withShouldAskCallFeedback() + withOnCallEndedAskForFeedback() + } + runBlocking { block() } + return this to EndCallUseCaseImpl( + lazy { callManager }, + callRepository, + endCallResultListener, + shouldAskCallFeedback, + TestKaliumDispatcher + ) + } + + suspend fun withShouldAskCallFeedback(should: Boolean = false) { + coEvery { shouldAskCallFeedback.invoke() }.returns(should) + } + + suspend fun withOnCallEndedAskForFeedback() { + coEvery { endCallResultListener.onCallEndedAskForFeedback(any()) }.doesNothing() + } } companion object { @@ -192,7 +234,7 @@ class EndCallUseCaseTest { private val call = Call( conversationId = conversationId, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = UserId("called-id", "domain"), isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetAllCallsWithSortedParticipantsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetAllCallsWithSortedParticipantsUseCaseTest.kt index 0756e2e9a07..e1827ca17b1 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetAllCallsWithSortedParticipantsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetAllCallsWithSortedParticipantsUseCaseTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.user.UserId import io.mockative.Mock import io.mockative.coEvery import io.mockative.mock @@ -85,26 +86,26 @@ class GetAllCallsWithSortedParticipantsUseCaseTest { private val call1 = Call( ConversationId("first", "domain"), CallStatus.STARTED, - true, - false, - false, - "caller-id", - "ONE_ON_ONE Name", - Conversation.Type.ONE_ON_ONE, - "otherUsername", - "team1" + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + callerId = UserId("called-id", "domain"), + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername", + callerTeamName = "team1" ) private val call2 = Call( ConversationId("second", "domain"), CallStatus.INCOMING, - true, - false, - false, - "caller-id", - "ONE_ON_ONE Name", - Conversation.Type.ONE_ON_ONE, - "otherUsername2", - "team2" + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + callerId = UserId("called-id", "domain"), + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername2", + callerTeamName = "team2" ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCaseTest.kt index 18c1ea18758..98387f073e2 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/GetIncomingCallsUseCaseTest.kt @@ -36,6 +36,7 @@ import io.mockative.Mock import io.mockative.any import io.mockative.coEvery import io.mockative.mock +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -74,6 +75,31 @@ class GetIncomingCallsUseCaseTest { } } + @Test + fun givenIncomingCall_whenInvokingGetIncomingCallsUseCaseAndAnotherCallAppears_thenPropagateUpdatedList() = runTest { + val incomingCallsFlow = MutableStateFlow(listOf(incomingCall(0))) + val (_, getIncomingCalls) = Arrangement() + .withSelfUserStatus(UserAvailabilityStatus.AVAILABLE) + .withConversationDetails { id -> Either.Right(conversationWithMuteStatus(id, MutedConversationStatus.AllAllowed)) } + .withIncomingCallsFlow(incomingCallsFlow) + .arrange() + + getIncomingCalls().test { + // initially there is only one incoming call + awaitItem().let { + assertEquals(1, it.size) + assertEquals(TestConversation.id(0), it[0].conversationId) + } + // then new incoming call appears + incomingCallsFlow.value = listOf(incomingCall(0), incomingCall(1)) + awaitItem().let { + assertEquals(2, it.size) + assertEquals(TestConversation.id(0), it[0].conversationId) + assertEquals(TestConversation.id(1), it[1].conversationId) + } + } + } + @Test fun givenUserWithAwayStatus_whenIncomingCallComes_thenNoCallsPropagated() = runTest { val (_, getIncomingCalls) = Arrangement() @@ -198,6 +224,12 @@ class GetIncomingCallsUseCaseTest { return this } + suspend fun withIncomingCallsFlow(callsFlow: Flow>): Arrangement = apply { + coEvery { + callRepository.incomingCallsFlow() + }.returns(callsFlow) + } + suspend fun withSelfUserStatus(status: UserAvailabilityStatus): Arrangement { coEvery { userRepository.observeSelfUser() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/IsCallRunningUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/IsCallRunningUseCaseTest.kt index cfe36df3813..afd799b4e38 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/IsCallRunningUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/IsCallRunningUseCaseTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.user.UserId import io.mockative.Mock import io.mockative.coEvery import io.mockative.mock @@ -83,26 +84,26 @@ class IsCallRunningUseCaseTest { private val call1 = Call( ConversationId("first", "domain"), CallStatus.STARTED, - true, - false, - false, - "caller-id1", - "ONE_ON_ONE Name", - Conversation.Type.ONE_ON_ONE, - "otherUsername", - "team1" + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + callerId = UserId("caller-id1", "domain"), + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername", + callerTeamName = "team1" ) private val call2 = Call( ConversationId("second", "domain"), CallStatus.CLOSED, - true, - false, - false, - "caller-id2", - "ONE_ON_ONE Name", - Conversation.Type.ONE_ON_ONE, - "otherUsername", - "team1" + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + callerId = UserId("caller-id2", "domain"), + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername", + callerTeamName = "team1" ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/MuteCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/MuteCallUseCaseTest.kt index 41f6a3bb0a3..df0bbacc84c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/MuteCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/MuteCallUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.CallManager +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.coEvery import io.mockative.coVerify @@ -100,7 +101,7 @@ class MuteCallUseCaseTest { val call = Call( conversationId = conversationId, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = TestCall.CALLER_ID, isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCaseTest.kt new file mode 100644 index 00000000000..41dafc12968 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveConferenceCallingEnabledUseCaseTest.kt @@ -0,0 +1,127 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.usecase + +import app.cash.turbine.test +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class ObserveConferenceCallingEnabledUseCaseTest { + + @Test + fun givenOnlyDefaultConferenceCallingValue_whenNewValueIsNotPresent_thenDoNotReturnAnything() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(false)) + .arrange() + + // when then + useCase().test { + awaitComplete() + } + } + + @Test + fun givenDefaultConferenceCallingValueIsTrue_whenNewValueIsAlsoTrue_thenDoNotReturnAnything() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(true, true)) + .arrange() + + // when then + useCase().test { + awaitComplete() + } + } + + @Test + fun givenDefaultConferenceCallingValueIsTrue_whenNewValueIsFalse_thenDoNotReturnAnything() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(true, false)) + .arrange() + + // when then + useCase().test { + awaitComplete() + } + } + + @Test + fun givenDefaultConferenceCallingValueIsFalse_whenNewValueIsTrue_thenReturnResult() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(false, true)) + .arrange() + + // when then + useCase().test { + awaitItem() + awaitComplete() + } + } + + @Test + fun givenDefaultConferenceCallingValueIsFalse_whenTwoNewValuesOfTrue_thenReturnOnlyOneResult() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(false, true, true)) + .arrange() + + // when then + useCase().test { + awaitItem() + awaitComplete() + } + } + + @Test + fun givenDefaultConferenceCallingValueIsFalse_whenThreeNewValuesAreTrueFalseTrue_thenReturnTwoResults() = runTest { + // given + val (_, useCase) = Arrangement() + .withDefaultValue(listOf(false, true, false, true)) + .arrange() + + // when then + useCase().test { + awaitItem() + awaitItem() + awaitComplete() + } + } + + private class Arrangement { + @Mock + val userConfigRepository = mock(UserConfigRepository::class) + + fun withDefaultValue(values: List) = apply { + every { + userConfigRepository.observeConferenceCallingEnabled() + }.returns(values.map { Either.Right(it) }.asFlow()) + } + + fun arrange(): Pair = + this to ObserveConferenceCallingEnabledUseCaseImpl(userConfigRepository) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOngoingCallsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOngoingCallsUseCaseTest.kt index 7f573fba700..0dfd7fca4f0 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOngoingCallsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOngoingCallsUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.coEvery import io.mockative.mock @@ -85,7 +86,7 @@ class ObserveOngoingCallsUseCaseTest { isMuted = false, isCameraOn = false, isCbrEnabled = false, - callerId = "callerId", + callerId = TestCall.CALLER_ID, conversationName = null, conversationType = Conversation.Type.GROUP, callerName = null, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOutgoingCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOutgoingCallUseCaseTest.kt index 539e9e2a95a..ff89652a0b4 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOutgoingCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/ObserveOutgoingCallUseCaseTest.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.coEvery import io.mockative.mock @@ -68,7 +69,7 @@ class ObserveOutgoingCallUseCaseTest { domain = "conversationDomain" ), status = CallStatus.STARTED, - callerId = "callerId@domain", + callerId = TestCall.CALLER_ID, participants = listOf(), isMuted = true, isCameraOn = false, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/SetTestRemoteVideoStatesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/SetTestRemoteVideoStatesUseCaseTest.kt index d234a77f51d..c414f295422 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/SetTestRemoteVideoStatesUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/SetTestRemoteVideoStatesUseCaseTest.kt @@ -70,7 +70,8 @@ class SetTestRemoteVideoStatesUseCaseTest { isSpeaking = false, isCameraOn = false, isSharingScreen = false, - hasEstablishedAudio = true + hasEstablishedAudio = true, + accentId = 0 ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UnMuteCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UnMuteCallUseCaseTest.kt index cd692cefe66..a5251f2be2f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UnMuteCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UnMuteCallUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.CallManager +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.coEvery import io.mockative.coVerify @@ -96,7 +97,7 @@ class UnMuteCallUseCaseTest { val call = Call( conversationId = conversationId, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = TestCall.CALLER_ID, isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UpdateConversationClientsForCurrentCallUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UpdateConversationClientsForCurrentCallUseCaseTest.kt index d5ca67a41e3..226c77120ec 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UpdateConversationClientsForCurrentCallUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/UpdateConversationClientsForCurrentCallUseCaseTest.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.coEvery import io.mockative.coVerify @@ -89,7 +90,7 @@ class UpdateConversationClientsForCurrentCallUseCaseTest { private val call = Call( conversationId = CONVERSATION_ID, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = TestCall.CALLER_ID, isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/video/UpdateVideoStateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/video/UpdateVideoStateUseCaseTest.kt index 2c203d7c738..337c15cf87b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/video/UpdateVideoStateUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/call/usecase/video/UpdateVideoStateUseCaseTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.call.Call import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock import io.mockative.eq import io.mockative.coEvery @@ -59,7 +60,7 @@ class UpdateVideoStateUseCaseTest { isMuted = true, isCameraOn = true, isCbrEnabled = false, - callerId = "caller-id", + callerId = TestCall.CALLER_ID, conversationName = "", Conversation.Type.ONE_ON_ONE, null, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ClientFingerprintUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ClientFingerprintUseCaseTest.kt index 07e226dfa8a..3750fb6f91a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ClientFingerprintUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/ClientFingerprintUseCaseTest.kt @@ -95,7 +95,7 @@ class ClientFingerprintUseCaseTest { @Test fun givenProteusException_whenGettingRemoteFingerPrint_thenErrorIsReturned() = runTest { - val error = ProteusException(null, ProteusException.Code.DECODE_ERROR) + val error = ProteusException(null, ProteusException.Code.DECODE_ERROR, 3) val userId = TestUser.USER_ID val clientId = TestClient.CLIENT_ID @@ -147,7 +147,7 @@ class ClientFingerprintUseCaseTest { .invokes { _ -> if (getSessionCalled == 0) { getSessionCalled++ - throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND) + throw ProteusException(null, ProteusException.Code.SESSION_NOT_FOUND, 2) } secondTimeResult } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCaseTest.kt new file mode 100644 index 00000000000..dfa49376049 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/IsAllowedToRegisterMLSClientUseCaseTest.kt @@ -0,0 +1,172 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.featureFlags.FeatureSupport +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class IsAllowedToRegisterMLSClientUseCaseTest { + + @Test + fun givenAllMlsConditionsAreMet_whenUseCaseInvoked_returnsTrue() = runTest { + // given + val (_, isAllowedToRegisterMLSClientUseCase) = Arrangement() + .withMlsFeatureFlag(true) + .withUserConfigMlsEnabled(true) + .withGetPublicKeysSuccessful() + .arrange() + + // when + val result = isAllowedToRegisterMLSClientUseCase() + + // then + assertEquals(true, result) + } + + @Test + fun givenMlsFeatureFlagDisabled_whenUseCaseInvoked_returnsFalse() = runTest { + // given + val (_, isAllowedToRegisterMLSClientUseCase) = Arrangement() + .withMlsFeatureFlag(false) + .withUserConfigMlsEnabled(true) + .withGetPublicKeysSuccessful() + .arrange() + + // when + val result = isAllowedToRegisterMLSClientUseCase() + + // then + assertEquals(false, result) + } + + @Test + fun givenUserConfigMlsDisabled_whenUseCaseInvoked_returnsFalse() = runTest { + // given + val (_, isAllowedToRegisterMLSClientUseCase) = Arrangement() + .withMlsFeatureFlag(true) + .withUserConfigMlsEnabled(false) + .withGetPublicKeysSuccessful() + .arrange() + + // when + val result = isAllowedToRegisterMLSClientUseCase() + + // then + assertEquals(false, result) + } + + @Test + fun givenPublicKeysFailure_whenUseCaseInvoked_returnsFalse() = runTest { + // given + val (_, isAllowedToRegisterMLSClientUseCase) = Arrangement() + .withMlsFeatureFlag(true) + .withUserConfigMlsEnabled(true) + .withGetPublicKeysFailed() + .arrange() + + // when + val result = isAllowedToRegisterMLSClientUseCase() + + // then + assertEquals(false, result) + } + + @Test + fun givenUserConfigDataNotFound_whenUseCaseInvoked_returnsFalse() = runTest { + // given + val (_, isAllowedToRegisterMLSClientUseCase) = Arrangement() + .withMlsFeatureFlag(true) + .withUserConfigDataNotFound() + .withGetPublicKeysFailed() + .arrange() + + // when + val result = isAllowedToRegisterMLSClientUseCase() + + // then + assertEquals(false, result) + } + + + private class Arrangement { + @Mock + val featureSupport = mock(FeatureSupport::class) + + @Mock + val mlsPublicKeysRepository = mock(MLSPublicKeysRepository::class) + + @Mock + val userConfigRepository = mock(UserConfigRepository::class) + + fun withMlsFeatureFlag(enabled: Boolean) = apply { + every { + featureSupport.isMLSSupported + }.returns(enabled) + } + + fun withUserConfigMlsEnabled(enabled: Boolean) = apply { + every { + userConfigRepository.isMLSEnabled() + }.returns(Either.Right(enabled)) + } + + fun withUserConfigDataNotFound() = apply { + every { + userConfigRepository.isMLSEnabled() + }.returns(Either.Left(StorageFailure.DataNotFound)) + } + + suspend fun withGetPublicKeysSuccessful() = apply { + coEvery { + mlsPublicKeysRepository.getKeys() + }.returns(Either.Right(MLS_PUBLIC_KEY)) + } + + suspend fun withGetPublicKeysFailed() = apply { + coEvery { + mlsPublicKeysRepository.getKeys() + }.returns(Either.Left(CoreFailure.Unknown(Throwable("an error")))) + } + + fun arrange() = this to IsAllowedToRegisterMLSClientUseCaseImpl( + featureSupport = featureSupport, + mlsPublicKeysRepository = mlsPublicKeysRepository, + userConfigRepository = userConfigRepository + ) + + companion object { + val MLS_PUBLIC_KEY = MLSPublicKeys( + removal = mapOf( + "ed25519" to "gRNvFYReriXbzsGu7zXiPtS8kaTvhU1gUJEV9rdFHVw=" + ) + ) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt index 7c62632d6ef..91d914628f7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/client/VerifyExistingClientUseCaseTest.kt @@ -79,6 +79,23 @@ class VerifyExistingClientUseCaseTest { assertIs(result) } + @Test + fun givenRegisteredClientIdAndMLSAllowedAndExistClientIsMLSCapable_whenRegisterMLSSucceed_thenReturnSuccessAndSkipMLSRegistration() = + runTest { + val clientId = ClientId("clientId") + val client = TestClient.CLIENT.copy(id = clientId, isMLSCapable = true) + val (arrangement, useCase) = arrange { + withSelfClientsResult(Either.Right(listOf(client))) + withIsAllowedToRegisterMLSClient(true) + } + val result = useCase.invoke(clientId) + assertIs(result) + coVerify { + arrangement.registerMLSClientUseCase(any()) + }.wasNotInvoked() + assertEquals(client, result.client) + } + @Test fun givenRegisteredClientIdAndMLSAllowed_whenRegisterMLSSucceed_thenReturnSuccess() = runTest { val clientId = ClientId("clientId") diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCaseTest.kt new file mode 100644 index 00000000000..3c0aeaf9dd6 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/GetConversationProtocolInfoUseCaseTest.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.test_util.TestKaliumDispatcher +import com.wire.kalium.logic.test_util.testKaliumDispatcher +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl +import com.wire.kalium.util.KaliumDispatcher +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GetConversationProtocolInfoUseCaseTest { + + @Test + fun givenGetConversationProtocolFails_whenInvoke_thenFailureReturned() = runTest { + val (_, useCase) = Arrangement() + .arrange { + dispatcher = this@runTest.testKaliumDispatcher + withConversationProtocolInfo(Either.Left(StorageFailure.DataNotFound)) + } + + assertTrue(useCase(ConversationId("ss", "dd")) is GetConversationProtocolInfoUseCase.Result.Failure) + } + + @Test + fun givenGetConversationProtocolSucceed_whenInvoke_thenSuccessReturned() = runTest { + val (_, useCase) = Arrangement() + .arrange { + dispatcher = this@runTest.testKaliumDispatcher + withConversationProtocolInfo(Either.Right(Conversation.ProtocolInfo.Proteus)) + } + + val result = useCase(ConversationId("ss", "dd")) + + assertTrue(result is GetConversationProtocolInfoUseCase.Result.Success) + assertEquals(Conversation.ProtocolInfo.Proteus, result.protocolInfo) + } + + private class Arrangement : ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { + + var dispatcher: KaliumDispatcher = TestKaliumDispatcher + private lateinit var getConversationProtocolInfo: GetConversationProtocolInfoUseCase + + suspend fun arrange(block: suspend Arrangement.() -> Unit): Pair { + block() + getConversationProtocolInfo = GetConversationProtocolInfoUseCase( + conversationRepository, + dispatcher + ) + + return this to getConversationProtocolInfo + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationUseCaseTest.kt index cee14f64eea..59264218319 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationUseCaseTest.kt @@ -131,7 +131,12 @@ class JoinExistingMLSConversationUseCaseTest { joinExistingMLSConversationsUseCase(Arrangement.MLS_UNESTABLISHED_GROUP_CONVERSATION.id).shouldSucceed() coVerify { - arrangement.mlsConversationRepository.establishMLSGroup(eq(Arrangement.GROUP_ID3), eq(emptyList()), any()) + arrangement.mlsConversationRepository.establishMLSGroup( + groupID = Arrangement.GROUP_ID3, + members = emptyList(), + publicKeys = null, + allowSkippingUsersWithoutKeyPackages = false + ) }.wasNotInvoked() } @@ -148,7 +153,12 @@ class JoinExistingMLSConversationUseCaseTest { joinExistingMLSConversationsUseCase(Arrangement.MLS_UNESTABLISHED_SELF_CONVERSATION.id).shouldSucceed() coVerify { - arrangement.mlsConversationRepository.establishMLSGroup(eq(Arrangement.GROUP_ID_SELF), eq(emptyList()), any()) + arrangement.mlsConversationRepository.establishMLSGroup( + groupID = Arrangement.GROUP_ID_SELF, + members = emptyList(), + publicKeys = null, + allowSkippingUsersWithoutKeyPackages = false + ) }.wasInvoked(once) } @@ -167,7 +177,12 @@ class JoinExistingMLSConversationUseCaseTest { joinExistingMLSConversationsUseCase(Arrangement.MLS_UNESTABLISHED_ONE_ONE_ONE_CONVERSATION.id).shouldSucceed() coVerify { - arrangement.mlsConversationRepository.establishMLSGroup(eq(Arrangement.GROUP_ID_ONE_ON_ONE), eq(members), any()) + arrangement.mlsConversationRepository.establishMLSGroup( + groupID = Arrangement.GROUP_ID_ONE_ON_ONE, + members = members, + publicKeys = null, + allowSkippingUsersWithoutKeyPackages = false + ) }.wasInvoked(once) } @@ -256,7 +271,7 @@ class JoinExistingMLSConversationUseCaseTest { suspend fun withEstablishMLSGroupSuccessful(additionResult: MLSAdditionResult) = apply { coEvery { - mlsConversationRepository.establishMLSGroup(any(), any(), any()) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), any()) }.returns(Either.Right(additionResult)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationsUseCaseTest.kt index a64c53ab01d..cb8c5d17438 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/JoinExistingMLSConversationsUseCaseTest.kt @@ -60,7 +60,7 @@ class JoinExistingMLSConversationsUseCaseTest { }.wasNotInvoked() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasNotInvoked() } @@ -76,7 +76,7 @@ class JoinExistingMLSConversationsUseCaseTest { }.wasNotInvoked() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasNotInvoked() } @@ -88,7 +88,7 @@ class JoinExistingMLSConversationsUseCaseTest { joinExistingMLSConversationsUseCase().shouldSucceed() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(twice) } @@ -100,7 +100,7 @@ class JoinExistingMLSConversationsUseCaseTest { joinExistingMLSConversationsUseCase().shouldSucceed() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(twice) } @@ -113,7 +113,7 @@ class JoinExistingMLSConversationsUseCaseTest { assertIs(it) } coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(twice) } @@ -125,7 +125,7 @@ class JoinExistingMLSConversationsUseCaseTest { joinExistingMLSConversationsUseCase().shouldSucceed() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(twice) } @@ -161,25 +161,25 @@ class JoinExistingMLSConversationsUseCaseTest { suspend fun withJoinExistingMLSConversationSuccessful() = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(Either.Right(Unit)) } suspend fun withJoinExistingMLSConversationNetworkFailure() = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(Either.Left(NetworkFailure.NoNetworkConnection(null))) } suspend fun withJoinExistingMLSConversationFailure() = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(Either.Left(CoreFailure.NotSupportedByProteus)) } suspend fun withNoKeyPackagesAvailable() = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(Either.Left(CoreFailure.MissingKeyPackages(setOf()))) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt index 83c62e4f838..3ff8776be81 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationDetailsUseCaseTest.kt @@ -80,20 +80,14 @@ class ObserveConversationDetailsUseCaseTest { Either.Right( ConversationDetails.Group( conversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ), Either.Right( ConversationDetails.Group( conversation.copy(name = "New Name"), - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt index bac9ed68dad..cc721631f91 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationInteractionAvailabilityUseCaseTest.kt @@ -21,6 +21,7 @@ package com.wire.kalium.logic.feature.conversation import app.cash.turbine.test import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.InteractionAvailability import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.SupportedProtocol import com.wire.kalium.logic.framework.TestConversation diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt index 9e6206c4605..a3340b10fef 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/ObserveConversationListDetailsUseCaseTest.kt @@ -64,10 +64,7 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -101,19 +98,13 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails1 = ConversationDetails.Group( groupConversation1, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) val groupConversationDetails2 = ConversationDetails.Group( groupConversation2, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -146,10 +137,7 @@ class ObserveConversationListDetailsUseCaseTest { val selfConversationDetails = ConversationDetails.Self(selfConversation) val groupConversationDetails = ConversationDetails.Group( conversation = groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -182,10 +170,7 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationUpdates = listOf( ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) ) @@ -194,15 +179,11 @@ class ObserveConversationListDetailsUseCaseTest { oneOnOneConversation, TestUser.OTHER, UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val secondOneOnOneDetails = ConversationDetails.OneOne( oneOnOneConversation, TestUser.OTHER.copy(name = "New User Name"), UserType.INTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val oneOnOneDetailsChannel = Channel(Channel.UNLIMITED) @@ -236,10 +217,7 @@ class ObserveConversationListDetailsUseCaseTest { val fetchArchivedConversations = false val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -273,10 +251,7 @@ class ObserveConversationListDetailsUseCaseTest { val fetchArchivedConversations = false val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) @@ -304,10 +279,7 @@ class ObserveConversationListDetailsUseCaseTest { val groupConversationDetails = ConversationDetails.Group( groupConversation, - lastMessage = null, isSelfUserMember = true, - isSelfUserCreator = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/RecoverMLSConversationsUseCaseTests.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/RecoverMLSConversationsUseCaseTests.kt index cacb405ccd6..3f446c83eb6 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/RecoverMLSConversationsUseCaseTests.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/RecoverMLSConversationsUseCaseTests.kt @@ -64,7 +64,7 @@ class RecoverMLSConversationsUseCaseTests { }.wasInvoked(conversations.size) coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(conversations.size) assertIs(actual) @@ -88,7 +88,7 @@ class RecoverMLSConversationsUseCaseTests { }.wasInvoked(conversations.size) coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(conversations.size) assertIs(actual) @@ -112,7 +112,7 @@ class RecoverMLSConversationsUseCaseTests { }.wasNotInvoked() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasNotInvoked() assertIs(actual) @@ -136,7 +136,7 @@ class RecoverMLSConversationsUseCaseTests { }.wasInvoked(twice) coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(once) assertIs(actual) @@ -205,7 +205,7 @@ class RecoverMLSConversationsUseCaseTests { suspend fun withJoinExistingMLSConversationUseCaseSuccessful() = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(Either.Right(Unit)) } @@ -226,10 +226,10 @@ class RecoverMLSConversationsUseCaseTests { suspend fun withJoinExistingMLSConversationUseCaseFailsFor(failedGroupId: ConversationId) = apply { coEvery { - joinExistingMLSConversationUseCase.invoke(eq(failedGroupId)) + joinExistingMLSConversationUseCase.invoke(failedGroupId, null) }.returns(Either.Left(StorageFailure.DataNotFound)) coEvery { - joinExistingMLSConversationUseCase.invoke(matches { it != failedGroupId }) + joinExistingMLSConversationUseCase.invoke(matches { it != failedGroupId }, any()) }.returns(Either.Right(Unit)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt new file mode 100644 index 00000000000..ac1fb8fb5fc --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/AddConversationToFavoritesUseCaseTest.kt @@ -0,0 +1,131 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class AddConversationToFavoritesUseCaseTest { + + @Test + fun givenValidConversation_WhenAddedToFavoritesSuccessfully_ThenReturnSuccess() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withAddConversationToFolder(Either.Right(Unit)) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder( + eq(TestConversation.ID), + eq(TestFolder.FAVORITE.id) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenFavoriteFolderNotFound_ThenReturnFailure() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenAddToFolderFails_ThenReturnFailure() = runTest { + val (arrangement, addConversationToFavoritesUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withAddConversationToFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = addConversationToFavoritesUseCase(TestConversation.ID) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.addConversationToFolder( + eq(TestConversation.ID), + eq(TestFolder.FAVORITE.id) + ) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val addConversationToFavoritesUseCase = AddConversationToFavoritesUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + suspend fun withAddConversationToFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.addConversationToFolder(any(), any()) + }.returns(either) + } + + suspend fun withSyncConversationFoldersFromLocal(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to addConversationToFavoritesUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt new file mode 100644 index 00000000000..ca6fc8e156f --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/GetFavoriteFolderUseCaseTest.kt @@ -0,0 +1,86 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.feature.conversation.folder.GetFavoriteFolderUseCase.Result +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class GetFavoriteFolderUseCaseTest { + + @Test + fun givenFavoriteFolderExists_WhenInvoked_ThenReturnSuccess() = runTest { + val (arrangement, getFavoriteFolderUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .arrange() + + val result = getFavoriteFolderUseCase() + + assertIs(result) + assertIs(result.folder) + assertEquals(TestFolder.FAVORITE.id, result.folder.id) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenFavoriteFolderDoesNotExist_WhenInvoked_ThenReturnFailure() = runTest { + val (arrangement, getFavoriteFolderUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .arrange() + + val result = getFavoriteFolderUseCase() + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val getFavoriteFolderUseCase = GetFavoriteFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to getFavoriteFolderUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt new file mode 100644 index 00000000000..74d5abef8bc --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/ObserveConversationsFromFolderUseCaseTest.kt @@ -0,0 +1,94 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.data.conversation.ConversationDetailsWithEvents +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.framework.TestConversationDetails +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ObserveConversationsFromFolderUseCaseTest { + + @Test + fun givenFolderId_WhenConversationsExist_ThenReturnFlowWithConversations() = runTest { + val testFolderId = "test-folder-id" + val testConversations = listOf( + TestConversationDetails.CONNECTION, + TestConversationDetails.CONVERSATION_ONE_ONE, + ).map { + ConversationDetailsWithEvents( + conversationDetails = it + ) + } + + val (arrangement, observeConversationsUseCase) = Arrangement() + .withConversationsFromFolder(testFolderId, testConversations) + .arrange() + + val result = observeConversationsUseCase(testFolderId).first() + + assertEquals(testConversations, result) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + } + + @Test + fun givenFolderId_WhenNoConversationsExist_ThenReturnEmptyFlow() = runTest { + val testFolderId = "test-folder-id" + + val (arrangement, observeConversationsUseCase) = Arrangement() + .withConversationsFromFolder(testFolderId, emptyList()) + .arrange() + + val result = observeConversationsUseCase(testFolderId).first() + + assertEquals(emptyList(), result) + + coVerify { + arrangement.conversationFolderRepository.observeConversationsFromFolder(testFolderId) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val observeConversationsFromFolderUseCase = ObserveConversationsFromFolderUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withConversationsFromFolder(folderId: String, conversationList: List) = apply { + coEvery { + conversationFolderRepository.observeConversationsFromFolder(folderId) + }.returns(flowOf(conversationList)) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to observeConversationsFromFolderUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt new file mode 100644 index 00000000000..bbc81934ea3 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/folder/RemoveConversationFromFavoritesUseCaseTest.kt @@ -0,0 +1,134 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.conversation.folder + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestFolder +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class RemoveConversationFromFavoritesUseCaseTest { + + @Test + fun givenValidConversation_WhenRemovedSuccessfullyFromFavorite_ThenReturnSuccess() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withRemoveConversationFromFolder(testConversationId, TestFolder.FAVORITE.id, Either.Right(Unit)) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, TestFolder.FAVORITE.id) + }.wasInvoked(exactly = once) + } + + @Test + fun givenInvalidConversation_WhenFavoriteFolderNotFound_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidConversation_WhenRemoveFromFavoritesFails_ThenReturnFailure() = runTest { + val testConversationId = TestConversation.ID + + val (arrangement, removeConversationUseCase) = Arrangement() + .withFavoriteFolder(Either.Right(TestFolder.FAVORITE)) + .withRemoveConversationFromFolder(testConversationId, TestFolder.FAVORITE.id, Either.Left(CoreFailure.Unknown(null))) + .withSyncConversationFoldersFromLocal(Either.Right(Unit)) + .arrange() + + val result = removeConversationUseCase(testConversationId) + + assertIs(result) + + coVerify { + arrangement.conversationFolderRepository.getFavoriteConversationFolder() + }.wasInvoked(exactly = once) + + coVerify { + arrangement.conversationFolderRepository.removeConversationFromFolder(testConversationId, TestFolder.FAVORITE.id) + }.wasInvoked(exactly = once) + } + + private class Arrangement { + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + + private val removeConversationUseCase = RemoveConversationFromFavoritesUseCaseImpl( + conversationFolderRepository + ) + + suspend fun withFavoriteFolder(either: Either) = apply { + coEvery { + conversationFolderRepository.getFavoriteConversationFolder() + }.returns(either) + } + + suspend fun withRemoveConversationFromFolder( + conversationId: ConversationId, + folderId: String, + either: Either + ) = apply { + coEvery { + conversationFolderRepository.removeConversationFromFolder(conversationId, folderId) + }.returns(either) + } + + suspend fun withSyncConversationFoldersFromLocal(either: Either) = apply { + coEvery { + conversationFolderRepository.syncConversationFoldersFromLocal() + }.returns(either) + } + + fun arrange(block: Arrangement.() -> Unit = { }) = apply(block).let { this to removeConversationUseCase } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolverTest.kt index 460a3ead111..28162df0d20 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/MLSOneOnOneConversationResolverTest.kt @@ -72,7 +72,7 @@ class MLSOneOnOneConversationResolverTest { }.wasNotInvoked() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasNotInvoked() } @@ -127,7 +127,7 @@ class MLSOneOnOneConversationResolverTest { } coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(any()) + arrangement.joinExistingMLSConversationUseCase.invoke(any(), any()) }.wasInvoked(exactly = once) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt index e5207ef75c4..d7f199fa13d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/conversation/mls/OneOnOneMigratorTest.kt @@ -132,6 +132,10 @@ class OneOnOneMigratorTest { coVerify { arrangement.messageRepository.moveMessagesToAnotherConversation(any(), any()) }.wasNotInvoked() + + coVerify { + arrangement.systemMessageInserter.insertProtocolChangedSystemMessage(any(), any(), any()) + }.wasNotInvoked() } @Test @@ -141,7 +145,7 @@ class OneOnOneMigratorTest { ) val failure = CoreFailure.MissingClientRegistration - val (_, oneOnOneMigrator) = arrange { + val (arrangement, oneOnOneMigrator) = arrange { withResolveConversationReturning(Either.Left(failure)) } @@ -171,6 +175,10 @@ class OneOnOneMigratorTest { coVerify { arrangement.userRepository.updateActiveOneOnOneConversation(any(), any()) }.wasNotInvoked() + + coVerify { + arrangement.systemMessageInserter.insertProtocolChangedSystemMessage(any(), any(), any()) + }.wasNotInvoked() } @Test @@ -212,6 +220,10 @@ class OneOnOneMigratorTest { coVerify { arrangement.messageRepository.moveMessagesToAnotherConversation(eq(originalConversationId), eq(resolvedConversationId)) }.wasInvoked(exactly = once) + + coVerify { + arrangement.systemMessageInserter.insertProtocolChangedSystemMessage(any(), any(), any()) + }.wasInvoked(exactly = once) } @Test @@ -234,6 +246,10 @@ class OneOnOneMigratorTest { coVerify { arrangement.userRepository.updateActiveOneOnOneConversation(eq(user.id), eq(resolvedConversationId)) }.wasInvoked(exactly = once) + + coVerify { + arrangement.systemMessageInserter.insertProtocolChangedSystemMessage(any(), any(), any()) + }.wasInvoked(exactly = once) } private class Arrangement(private val block: suspend Arrangement.() -> Unit) : @@ -249,7 +265,8 @@ class OneOnOneMigratorTest { conversationGroupRepository = conversationGroupRepository, conversationRepository = conversationRepository, messageRepository = messageRepository, - userRepository = userRepository + userRepository = userRepository, + systemMessageInserter = systemMessageInserter ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorkerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorkerTest.kt index d4c45635c53..4d1e70e4cb2 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorkerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/CertificateRevocationListCheckWorkerTest.kt @@ -48,7 +48,7 @@ class CertificateRevocationListCheckWorkerTest { .withCheckRevocationListResult() .arrange() - checkCrlWorker.execute() + checkCrlWorker() coVerify { arrangement.certificateRevocationListRepository.getCRLs() @@ -75,7 +75,7 @@ class CertificateRevocationListCheckWorkerTest { @Mock val checkRevocationList = mock(RevocationListChecker::class) - fun arrange() = this to CertificateRevocationListCheckWorkerImpl( + fun arrange() = this to SyncCertificateRevocationListUseCase( certificateRevocationListRepository, incrementalSyncRepository, checkRevocationList, kaliumLogger ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetMembersE2EICertificateStatusesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetMembersE2EICertificateStatusesUseCaseTest.kt index aa76385da59..49682c539f2 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetMembersE2EICertificateStatusesUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetMembersE2EICertificateStatusesUseCaseTest.kt @@ -22,6 +22,7 @@ import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.cryptography.CryptoQualifiedClientId import com.wire.kalium.cryptography.WireIdentity import com.wire.kalium.logic.MLSFailure +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.user.UserId @@ -29,7 +30,8 @@ import com.wire.kalium.logic.feature.e2ei.usecase.GetMembersE2EICertificateStatu import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArrangement import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArrangementImpl -import io.mockative.matchers.EqualsMatcher +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangementImpl import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant @@ -54,6 +56,7 @@ class GetMembersE2EICertificateStatusesUseCaseTest { fun givenEmptyWireIdentityMap_whenRequestMembersStatuses_thenNotActivatedResult() = runTest { val (_, getMembersE2EICertificateStatuses) = arrange { withMembersIdentities(Either.Right(mapOf())) + withMembersNameAndHandle(Either.Right(mapOf())) } val result = getMembersE2EICertificateStatuses(CONVERSATION_ID, listOf()) @@ -65,6 +68,7 @@ class GetMembersE2EICertificateStatusesUseCaseTest { fun givenOneWireIdentityExpiredForSomeUser_whenRequestMembersStatuses_thenResultUsersStatusIsExpired() = runTest { val (_, getMembersE2EICertificateStatuses) = arrange { + withMembersNameAndHandle(Either.Right(mapOf(USER_ID to NAME_AND_HANDLE))) withMembersIdentities( Either.Right( mapOf( @@ -87,6 +91,7 @@ class GetMembersE2EICertificateStatusesUseCaseTest { runTest { val userId2 = USER_ID.copy(value = "value_2") val (_, getMembersE2EICertificateStatuses) = arrange { + withMembersNameAndHandle(Either.Right(mapOf(userId2 to NAME_AND_HANDLE))) withMembersIdentities( Either.Right( mapOf( @@ -108,12 +113,14 @@ class GetMembersE2EICertificateStatusesUseCaseTest { } private class Arrangement(private val block: suspend Arrangement.() -> Unit) : - MLSConversationRepositoryArrangement by MLSConversationRepositoryArrangementImpl() { + MLSConversationRepositoryArrangement by MLSConversationRepositoryArrangementImpl(), + ConversationRepositoryArrangement by ConversationRepositoryArrangementImpl() { fun arrange() = run { runBlocking { block() } this@Arrangement to GetMembersE2EICertificateStatusesUseCaseImpl( - mlsConversationRepository = mlsConversationRepository + mlsConversationRepository = mlsConversationRepository, + conversationRepository = conversationRepository ) } } @@ -145,5 +152,7 @@ class GetMembersE2EICertificateStatusesUseCaseTest { notAfter = Instant.DISTANT_FUTURE.epochSeconds ) ) + + private val NAME_AND_HANDLE = NameAndHandle(name = "user displayName", handle = "userHandle") } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetUserE2eiCertificateStatusUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetUserE2eiCertificateStatusUseCaseTest.kt index 451d60e8224..2a0dc5dab3f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetUserE2eiCertificateStatusUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/GetUserE2eiCertificateStatusUseCaseTest.kt @@ -22,6 +22,8 @@ import com.wire.kalium.cryptography.CryptoCertificateStatus import com.wire.kalium.cryptography.CryptoQualifiedClientId import com.wire.kalium.cryptography.WireIdentity import com.wire.kalium.logic.MLSFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.toCrypto import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.e2ei.usecase.IsOtherUserE2EIVerifiedUseCaseImpl @@ -30,13 +32,14 @@ import com.wire.kalium.logic.util.arrangement.mls.IsE2EIEnabledUseCaseArrangemen import com.wire.kalium.logic.util.arrangement.mls.IsE2EIEnabledUseCaseArrangementImpl import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArrangement import com.wire.kalium.logic.util.arrangement.mls.MLSConversationRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserRepositoryArrangementImpl import io.mockative.any import io.mockative.coVerify import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant import kotlin.test.Test -import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -48,6 +51,7 @@ class GetUserE2eiCertificateStatusUseCaseTest { val (_, getUserE2eiCertificateStatus) = arrange { withE2EIEnabledAndMLSEnabled(true) withUserIdentity(Either.Left(MLSFailure.WrongEpoch)) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) } val result = getUserE2eiCertificateStatus(USER_ID) @@ -61,6 +65,7 @@ class GetUserE2eiCertificateStatusUseCaseTest { val (_, getUserE2eiCertificateStatus) = arrange { withE2EIEnabledAndMLSEnabled(true) withUserIdentity(Either.Right(listOf())) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) } val result = getUserE2eiCertificateStatus(USER_ID) @@ -73,6 +78,7 @@ class GetUserE2eiCertificateStatusUseCaseTest { runTest { val (_, getUserE2eiCertificateStatus) = arrange { withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) withUserIdentity( Either.Right( listOf( @@ -93,6 +99,7 @@ class GetUserE2eiCertificateStatusUseCaseTest { runTest { val (_, getUserE2eiCertificateStatus) = arrange { withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) withUserIdentity( Either.Right( listOf( @@ -113,6 +120,7 @@ class GetUserE2eiCertificateStatusUseCaseTest { runTest { val (_, getUserE2eiCertificateStatus) = arrange { withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) withUserIdentity( Either.Right( listOf( @@ -147,15 +155,73 @@ class GetUserE2eiCertificateStatusUseCaseTest { }.wasNotInvoked() } + @Test + fun givenOneWireIdentityIsOk_whenUserNameDiffFromIdentityName_thenResultIsExpired() = + runTest { + val (_, getUserE2eiCertificateStatus) = arrange { + withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("another user displayName", "userHandle"))) + withUserIdentity(Either.Right(listOf(WIRE_IDENTITY))) + } + + val result = getUserE2eiCertificateStatus(USER_ID) + + assertFalse(result) + } + + @Test + fun givenOneWireIdentityIsOk_whenUserHandleDiffFromIdentityName_thenResultIsExpired() = + runTest { + val (_, getUserE2eiCertificateStatus) = arrange { + withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "anotherUserHandle"))) + withUserIdentity(Either.Right(listOf(WIRE_IDENTITY))) + } + + val result = getUserE2eiCertificateStatus(USER_ID) + + assertFalse(result) + } + + @Test + fun givenOneWireIdentityIsOk_whenUserNameAndHandleAbsent_thenResultIsExpired() = + runTest { + val (_, getUserE2eiCertificateStatus) = arrange { + withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Left(StorageFailure.DataNotFound)) + withUserIdentity(Either.Right(listOf(WIRE_IDENTITY))) + } + + val result = getUserE2eiCertificateStatus(USER_ID) + + assertFalse(result) + } + + @Test + fun givenOneWireIdentityIsOk_whenUserNameAndHandleSameAsInIdentity_thenResultIsTrue() = + runTest { + val (_, getUserE2eiCertificateStatus) = arrange { + withE2EIEnabledAndMLSEnabled(true) + withNameAndHandle(Either.Right(NameAndHandle("user displayName", "userHandle"))) + withUserIdentity(Either.Right(listOf(WIRE_IDENTITY))) + } + + val result = getUserE2eiCertificateStatus(USER_ID) + + assertTrue(result) + } + private class Arrangement(private val block: suspend Arrangement.() -> Unit) : MLSConversationRepositoryArrangement by MLSConversationRepositoryArrangementImpl(), - IsE2EIEnabledUseCaseArrangement by IsE2EIEnabledUseCaseArrangementImpl() { + IsE2EIEnabledUseCaseArrangement by IsE2EIEnabledUseCaseArrangementImpl(), + UserRepositoryArrangement by UserRepositoryArrangementImpl() { fun arrange() = run { runBlocking { block() } this@Arrangement to IsOtherUserE2EIVerifiedUseCaseImpl( mlsConversationRepository = mlsConversationRepository, - isE2EIEnabledUseCase = isE2EIEnabledUseCase + isE2EIEnabledUseCase = isE2EIEnabledUseCase, + userRepository = userRepository ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagSyncWorkerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagSyncWorkerTest.kt index 3779d6980d2..be248bfff14 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagSyncWorkerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/featureConfig/FeatureFlagSyncWorkerTest.kt @@ -60,19 +60,15 @@ class FeatureFlagSyncWorkerTest { @Test fun givenSyncIsLiveTwiceInAShortInterval_thenShouldCallFeatureConfigsUseCaseOnlyOnce() = runTest { - val minimumInterval = 5.minutes val stateChannel = Channel(capacity = Channel.UNLIMITED) val (arrangement, featureFlagSyncWorker) = arrange { - minimumIntervalBetweenPulls = minimumInterval withIncrementalSyncState(stateChannel.consumeAsFlow()) } val job = launch { featureFlagSyncWorker.execute() } - stateChannel.send(IncrementalSyncStatus.Live) stateChannel.send(IncrementalSyncStatus.Pending) - advanceUntilIdle() stateChannel.send(IncrementalSyncStatus.Live) advanceUntilIdle() // Not enough to run twice coVerify { @@ -82,48 +78,6 @@ class FeatureFlagSyncWorkerTest { job.cancel() } - @Test - fun givenSyncIsLiveAgainAfterMinInterval_thenShouldCallFeatureConfigsUseCaseTwice() = runTest { - val minInterval = 5.minutes - val now = Clock.System.now() - val stateTimes = mapOf( - now to IncrementalSyncStatus.Live, - now + minInterval + 1.milliseconds to IncrementalSyncStatus.Pending, - now + minInterval + 2.milliseconds to IncrementalSyncStatus.Live - ) - val fakeClock = object : Clock { - var callCount = 0 - override fun now(): Instant { - return stateTimes.keys.toList()[callCount].also { callCount++ } - } - } - val stateChannel = Channel(capacity = Channel.UNLIMITED) - val (arrangement, featureFlagSyncWorker) = arrange { - minimumIntervalBetweenPulls = minInterval - withIncrementalSyncState(stateChannel.consumeAsFlow()) - clock = fakeClock - } - stateChannel.send(stateTimes.values.toList()[0]) - val job = launch { - featureFlagSyncWorker.execute() - } - advanceUntilIdle() - - coVerify { - arrangement.syncFeatureConfigsUseCase.invoke() - }.wasInvoked(exactly = once) - stateChannel.send(stateTimes.values.toList()[1]) - advanceUntilIdle() - - stateChannel.send(stateTimes.values.toList()[2]) - advanceUntilIdle() - - coVerify { - arrangement.syncFeatureConfigsUseCase.invoke() - }.wasInvoked(exactly = once) - job.cancel() - } - private class Arrangement( private val configure: Arrangement.() -> Unit ) : IncrementalSyncRepositoryArrangement by IncrementalSyncRepositoryArrangementImpl() { @@ -131,10 +85,6 @@ class FeatureFlagSyncWorkerTest { @Mock val syncFeatureConfigsUseCase: SyncFeatureConfigsUseCase = mock(SyncFeatureConfigsUseCase::class) - var minimumIntervalBetweenPulls: Duration = 1.minutes - - var clock: Clock = Clock.System - suspend fun arrange(): Pair = run { coEvery { syncFeatureConfigsUseCase.invoke() @@ -143,8 +93,6 @@ class FeatureFlagSyncWorkerTest { this@Arrangement to FeatureFlagSyncWorkerImpl( incrementalSyncRepository = incrementalSyncRepository, syncFeatureConfigs = syncFeatureConfigsUseCase, - minIntervalBetweenPulls = minimumIntervalBetweenPulls, - clock = clock, kaliumLogger = kaliumLogger ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNotificationsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNotificationsUseCaseTest.kt index 934cb1aa7c7..37bf75c7e9d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNotificationsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/GetNotificationsUseCaseTest.kt @@ -64,6 +64,7 @@ import kotlinx.datetime.Instant import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals +import kotlin.time.Duration import kotlin.time.Duration.Companion.days class GetNotificationsUseCaseTest { @@ -192,7 +193,7 @@ class GetNotificationsUseCaseTest { arrange.notificationEventsManager.observeEphemeralNotifications() }.wasInvoked(exactly = once) - syncStatusFlow.emit(IncrementalSyncStatus.Failed(CoreFailure.Unknown(null))) + syncStatusFlow.emit(IncrementalSyncStatus.Failed(CoreFailure.Unknown(null), Duration.ZERO)) coVerify { arrange.messageRepository.getNotificationMessage(any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageEnvelopeCreatorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageEnvelopeCreatorTest.kt index ef7a1c9a643..6f515c2f488 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageEnvelopeCreatorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/MessageEnvelopeCreatorTest.kt @@ -338,7 +338,7 @@ class MessageEnvelopeCreatorTest { @Test fun givenProteusThrowsDuringEncryption_whenCreatingEnvelope_thenTheFailureShouldBePropagated() = runTest { - val exception = ProteusException("OOPS", ProteusException.Code.PANIC) + val exception = ProteusException("OOPS", ProteusException.Code.PANIC, 15) coEvery { proteusClient.encryptBatched(any(), any()) }.throws(exception) @@ -368,7 +368,7 @@ class MessageEnvelopeCreatorTest { fun givenProteusThrowsDuringEncryption_whenCreatingEnvelope_thenNoMoreEncryptionsShouldBeAttempted() = runTest { coEvery { proteusClient.encryptBatched(any(), any()) - }.throws(ProteusException("OOPS", ProteusException.Code.PANIC)) + }.throws(ProteusException("OOPS", ProteusException.Code.PANIC, 15)) every { @@ -590,7 +590,7 @@ class MessageEnvelopeCreatorTest { @Test fun givenProteusThrowsDuringEncryption_whenCreatingBroadcastEnvelope_thenTheFailureShouldBePropagated() = runTest { - val exception = ProteusException("OOPS", ProteusException.Code.PANIC) + val exception = ProteusException("OOPS", ProteusException.Code.PANIC, 15) coEvery { proteusClient.encryptBatched(any(), any()) }.throws(exception) @@ -610,7 +610,7 @@ class MessageEnvelopeCreatorTest { fun givenProteusThrowsDuringEncryption_whenCreatingBroadcastEnvelope_thenNoMoreEncryptionsShouldBeAttempted() = runTest { coEvery { proteusClient.encryptBatched(any(), any()) - }.throws(ProteusException("OOPS", ProteusException.Code.PANIC)) + }.throws(ProteusException("OOPS", ProteusException.Code.PANIC, 15)) every { protoContentMapper.encodeToProtobuf(any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReactionsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReactionsUseCaseTest.kt index 26349a9e121..c3faa94f32d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReactionsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReactionsUseCaseTest.kt @@ -92,7 +92,8 @@ class ObserveMessageReactionsUseCaseTest { userType = UserType.INTERNAL, isUserDeleted = false, connectionStatus = ConnectionState.ACCEPTED, - availabilityStatus = UserAvailabilityStatus.NONE + availabilityStatus = UserAvailabilityStatus.NONE, + accentId = 0 ) ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReceiptsUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReceiptsUseCaseTest.kt index 37ef8917ea7..cb6e591edcf 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReceiptsUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/ObserveMessageReceiptsUseCaseTest.kt @@ -114,7 +114,8 @@ class ObserveMessageReceiptsUseCaseTest { userType = UserType.INTERNAL, isUserDeleted = false, connectionStatus = ConnectionState.ACCEPTED, - availabilityStatus = UserAvailabilityStatus.NONE + availabilityStatus = UserAvailabilityStatus.NONE, + accentId = 0 ) ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PendingProposalSchedulerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PendingProposalSchedulerTest.kt index b8607eaee40..05215c911db 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PendingProposalSchedulerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PendingProposalSchedulerTest.kt @@ -95,22 +95,6 @@ class PendingProposalSchedulerTest { }.wasInvoked(once) } - @Test - fun givenMLSSupportIsDisabled_whenSyncIsLive_thenPendingProposalIsNotCommitted() = runTest(TestKaliumDispatcher.default) { - val (arrangement, _) = Arrangement() - .withScheduledProposalTimers(listOf(ProposalTimer(TestConversation.GROUP_ID, Arrangement.INSTANT_PAST))) - .withCommitPendingProposalsSuccessful() - .arrange() - - arrangement.kaliumConfigs.isMLSSupportEnabled = false - arrangement.incrementalSyncRepository.updateIncrementalSyncState(IncrementalSyncStatus.Live) - yield() - - coVerify { - arrangement.mlsConversationRepository.commitPendingProposals(eq(TestConversation.GROUP_ID)) - }.wasNotInvoked() - } - @Test fun givenNonExpiredProposalTimer_whenSyncFinishes_thenPendingProposalIsNotCommitted() = runTest(TestKaliumDispatcher.default) { val (arrangement, _) = Arrangement() @@ -182,8 +166,6 @@ class PendingProposalSchedulerTest { private class Arrangement { - val kaliumConfigs = KaliumConfigs() - val incrementalSyncRepository = InMemoryIncrementalSyncRepository() @Mock @@ -193,7 +175,6 @@ class PendingProposalSchedulerTest { val subconversationRepository = mock(SubconversationRepository::class) val pendingProposalScheduler = PendingProposalSchedulerImpl( - kaliumConfigs, incrementalSyncRepository, lazy { mlsConversationRepository }, lazy { subconversationRepository }, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCaseTest.kt index 69f9bc700ca..24cb2ef2385 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/PersistMigratedMessagesUseCaseTest.kt @@ -58,7 +58,10 @@ class PersistMigratedMessagesUseCaseTest { @Mock val migrationDAO: MigrationDAO = mock(MigrationDAO::class) - val genericMessage = GenericMessage("uuid", GenericMessage.Content.Text(Text("some_text"))) + val genericMessage = GenericMessage( + messageId = "uuid", + content = GenericMessage.Content.Text(Text("some_text")) + ) fun fakeMigratedMessage() = MigratedMessage( conversationId = TestConversation.ID, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCaseTest.kt index 998ca1eaa0a..885b586b299 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/RetryFailedMessageUseCaseTest.kt @@ -159,6 +159,38 @@ class RetryFailedMessageUseCaseTest { }.wasInvoked(exactly = once) } + @Test + fun givenAValidFailedAndNotUploadedAssetMessage_whenSuccessfullyUploadedAsset_thenAssetTransferShouldBeChangedToUploaded() = + runTest(testDispatcher.default) { + // given + val name = "some_asset.txt" + val content = MessageContent.Asset(ASSET_CONTENT.value.copy(name = name)) + val path = fakeKaliumFileSystem.providePersistentAssetPath(name) + val message = assetMessage().copy(content = content, status = Message.Status.Failed) + val uploadedAssetId = UploadedAssetId("remote_key", "remote_domain", "remote_token") + val uploadedAssetSha = SHA256Key(byteArrayOf()) + val (arrangement, useCase) = Arrangement() + .withGetMessageById(Either.Right(message)) + .withUpdateMessageStatus(Either.Right(Unit)) + .withUpdateAssetMessageTransferStatus(UpdateTransferStatusResult.Success) + .withFetchPrivateDecodedAsset(Either.Right(path)) + .withStoredData(mockedLongAssetData(), path) + .withGetAssetMessageTransferStatus(AssetTransferStatus.FAILED_UPLOAD) + .withUploadAndPersistPrivateAsset(Either.Right(uploadedAssetId to uploadedAssetSha)) + .withPersistMessage(Either.Right(Unit)) + .withSendMessage(Either.Right(Unit)) + .arrange() + + // when + useCase.invoke(message.id, message.conversationId) + advanceUntilIdle() + + // then + coVerify { + arrangement.updateAssetMessageTransferStatus.invoke(AssetTransferStatus.UPLOADED, message.conversationId, message.id) + }.wasInvoked(exactly = once) + } + @Test fun givenAValidFailedAndNotUploadedAssetMessage_whenRetryingFailedMessage_thenUploadAssetAndSendAMessageWithProperAssetRemoteData() = runTest(testDispatcher.default) { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SessionEstablisherTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SessionEstablisherTest.kt index f88bf4bb836..3b8c40b565b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SessionEstablisherTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SessionEstablisherTest.kt @@ -66,7 +66,7 @@ class SessionEstablisherTest { @Test fun givenProteusClientThrowsWhenCheckingSession_whenPreparingSessions_thenItShouldFail() = runTest { - val exception = ProteusException("PANIC!!!11!eleven!", ProteusException.Code.PANIC) + val exception = ProteusException("PANIC!!!11!eleven!", ProteusException.Code.PANIC, 15) val (_, sessionEstablisher) = Arrangement() .withDoesSessionExistThrows(exception) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt index 0c7d12aed83..b1bc573e67e 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/StaleEpochVerifierTest.kt @@ -79,7 +79,7 @@ class StaleEpochVerifierTest { staleEpochHandler.verifyEpoch(CONVERSATION_ID).shouldSucceed() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(eq(CONVERSATION_ID)) + arrangement.joinExistingMLSConversationUseCase.invoke(CONVERSATION_ID, null) }.wasNotInvoked() } @@ -96,7 +96,7 @@ class StaleEpochVerifierTest { staleEpochHandler.verifyEpoch(CONVERSATION_ID).shouldSucceed() coVerify { - arrangement.joinExistingMLSConversationUseCase.invoke(eq(CONVERSATION_ID)) + arrangement.joinExistingMLSConversationUseCase.invoke(CONVERSATION_ID, null) }.wasInvoked(once) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandlerTest.kt index 880c5f904a5..d418400fc7b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/ConfirmationDeliveryHandlerTest.kt @@ -17,18 +17,15 @@ */ package com.wire.kalium.logic.feature.message.confirmation +import co.touchlab.stately.collections.ConcurrentMutableMap import com.benasher44.uuid.uuid4 import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.id.CurrentClientIdProvider -import com.wire.kalium.logic.feature.message.MessageSender -import com.wire.kalium.logic.framework.TestClient import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestMessage -import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.kaliumLogger @@ -58,7 +55,6 @@ class ConfirmationDeliveryHandlerTest { @Test fun givenANewMessage_whenEnqueuing_thenShouldBeAddedSuccessfullyToTheConversationKey() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .arrange() sut.enqueueConfirmationDelivery(TestConversation.ID, TestMessage.TEST_MESSAGE_ID) @@ -70,7 +66,6 @@ class ConfirmationDeliveryHandlerTest { @Test fun givenANewMessage_whenEnqueuingDuplicated_thenShouldNotBeAddedToTheConversationKey() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .arrange() sut.enqueueConfirmationDelivery(TestConversation.ID, TestMessage.TEST_MESSAGE_ID) @@ -84,9 +79,8 @@ class ConfirmationDeliveryHandlerTest { @Test fun givenMessagesEnqueued_whenCollectingThem_thenShouldSendOnlyForOneToOneConversations() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult() + .withSendDeliverSignalResult() .arrange() val job = launch { sut.sendPendingConfirmations() } @@ -97,7 +91,7 @@ class ConfirmationDeliveryHandlerTest { job.cancel() coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasInvoked() - coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked() + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasInvoked() assertTrue(arrangement.pendingConfirmationMessages.isEmpty()) } @@ -105,9 +99,8 @@ class ConfirmationDeliveryHandlerTest { @Test fun givenMessagesEnqueued_whenCollectingThemAndNoSession_thenShouldStopCollecting() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult() + .withSendDeliverSignalResult() .arrange() val job = launch { sut.sendPendingConfirmations() } @@ -119,16 +112,15 @@ class ConfirmationDeliveryHandlerTest { advanceUntilIdle() coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasNotInvoked() - coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasNotInvoked() + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasNotInvoked() } @OptIn(ExperimentalCoroutinesApi::class) @Test - fun givenMessagesEnqueued_whenSendingConfirmationsAndError_thenShouldNotFail() = runTest { + fun givenMessagesEnqueued_whenSendingConfirmationsAndError_thenMessagesShouldPersist() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult(Either.Left(CoreFailure.Unknown(RuntimeException("Something went wrong")))) + .withSendDeliverSignalResult(Either.Left(CoreFailure.Unknown(RuntimeException("Something went wrong")))) .arrange() val job = launch { sut.sendPendingConfirmations() } @@ -140,17 +132,16 @@ class ConfirmationDeliveryHandlerTest { job.cancel() coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasInvoked() - coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked() - assertTrue(arrangement.pendingConfirmationMessages.isEmpty()) + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasInvoked() + assertTrue(arrangement.pendingConfirmationMessages.isNotEmpty()) } @OptIn(ExperimentalCoroutinesApi::class) @Test fun givenABigLoadOfMessagesEnqueued_whenSendingConfirmations_thenShouldAddAndRemoveSecurely() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult() + .withSendDeliverSignalResult() .arrange() val job = launch { sut.sendPendingConfirmations() } @@ -170,7 +161,7 @@ class ConfirmationDeliveryHandlerTest { job.cancel() coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasInvoked() - coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked(once) + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasInvoked(once) assertTrue(arrangement.pendingConfirmationMessages.isEmpty()) } @@ -179,9 +170,8 @@ class ConfirmationDeliveryHandlerTest { @Test fun givenMultipleEnqueues_whenSendingConfirmations_thenShouldOnlySendOnce() = runTest { val (arrangement, sut) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult() + .withSendDeliverSignalResult() .arrange() val job = launch { sut.sendPendingConfirmations() } @@ -194,16 +184,15 @@ class ConfirmationDeliveryHandlerTest { job.cancel() coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasInvoked() - coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked(once) + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasInvoked(once) assertTrue(arrangement.pendingConfirmationMessages.isEmpty()) } @Test fun givenSyncIsOngoing_whenItTakesLongTimeToExecute_thenShouldReturnAnyway() = runTest { val (arrangement, handler) = Arrangement() - .withCurrentClientIdProvider() .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) - .withMessageSenderResult() + .withSendDeliverSignalResult() .arrange() coEvery { arrangement.syncManager.waitUntilLive() }.invokes { -> @@ -227,43 +216,52 @@ class ConfirmationDeliveryHandlerTest { sendJob.cancel() } - private class Arrangement { + @Test + fun givenMessagesSent_whenCleared_thenShouldRemoveMessagesFromPendingConfirmation() = runTest { + val (arrangement, handler) = Arrangement() + .withConversationDetailsResult(flowOf(TestConversation.CONVERSATION.right())) + .withSendDeliverSignalResult() + .arrange() - @Mock - private val currentClientIdProvider = mock(CurrentClientIdProvider::class) + val job = launch { handler.sendPendingConfirmations() } + advanceUntilIdle() + + handler.enqueueConfirmationDelivery(TestConversation.ID, TestMessage.TEST_MESSAGE_ID) + advanceUntilIdle() + job.cancel() + + coVerify { arrangement.conversationRepository.observeConversationById(any()) }.wasInvoked() + coVerify { arrangement.sendDeliverSignal(any(), any()) }.wasInvoked() + assertTrue(arrangement.pendingConfirmationMessages[TestConversation.ID]?.isEmpty() ?: true) + } + + private class Arrangement { @Mock val syncManager: SyncManager = mock(SyncManager::class) @Mock - val messageSender = mock(MessageSender::class) + val sendDeliverSignal: SendDeliverSignalUseCase = mock(SendDeliverSignalUseCase::class) @Mock val conversationRepository = mock(ConversationRepository::class) - val pendingConfirmationMessages: MutableMap> = mutableMapOf() - - suspend fun withCurrentClientIdProvider() = apply { - coEvery { currentClientIdProvider.invoke() }.returns(Either.Right(TestClient.CLIENT_ID)) - } + val pendingConfirmationMessages: ConcurrentMutableMap> = ConcurrentMutableMap() suspend fun withConversationDetailsResult(result: Flow>) = apply { coEvery { conversationRepository.observeConversationById(any()) }.returns(result) } - suspend fun withMessageSenderResult(result: Either = Unit.right()) = apply { - coEvery { messageSender.sendMessage(any(), any()) }.returns(result) + suspend fun withSendDeliverSignalResult(result: Either = Unit.right()) = apply { + coEvery { sendDeliverSignal(any(), any()) }.returns(result) } fun arrange() = this to ConfirmationDeliveryHandlerImpl( syncManager = syncManager, - selfUserId = TestUser.SELF.id, - currentClientIdProvider = currentClientIdProvider, conversationRepository = conversationRepository, - messageSender = messageSender, + sendDeliverSignalUseCase = sendDeliverSignal, kaliumLogger = kaliumLogger, pendingConfirmationMessages = pendingConfirmationMessages ) } - } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCaseTest.kt new file mode 100644 index 00000000000..c07b94af458 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/confirmation/SendDeliverSignalUseCaseTest.kt @@ -0,0 +1,120 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.message.confirmation + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.framework.TestClient +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestMessage +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.right +import com.wire.kalium.logic.kaliumLogger +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class SendDeliverSignalUseCaseTest { + + @Test + fun givenValidClientIdAndMessage_whenInvoking_thenShouldSendMessageSuccessfully() = runTest { + val (arrangement, usecase) = Arrangement() + .withCurrentClientIdProvider() + .withMessageSenderResult() + .arrange() + + val conversation = TestConversation.CONVERSATION + val messageIdList = listOf(TestMessage.TEST_MESSAGE_ID) + + val result = usecase.invoke(conversation, messageIdList) + + assertTrue(result is Either.Right) + coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked() + } + + @Test + fun givenMessageSendingFailure_whenInvoking_thenShouldLogError() = runTest { + val (arrangement, usecase) = Arrangement() + .withCurrentClientIdProvider() + .withMessageSenderResult(Either.Left(CoreFailure.Unknown(RuntimeException("Sending failed")))) + .arrange() + + val conversation = TestConversation.CONVERSATION + val messageIdList = listOf(TestMessage.TEST_MESSAGE_ID) + + val result = usecase.invoke(conversation, messageIdList) + + assertTrue(result is Either.Left) + coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasInvoked() + } + + @Test + fun givenClientIdProviderFailure_whenInvoking_thenShouldReturnFailure() = runTest { + val (arrangement, usecase) = Arrangement() + .withCurrentClientIdProviderError() + .arrange() + + val conversation = TestConversation.CONVERSATION + val messageIdList = listOf(TestMessage.TEST_MESSAGE_ID) + + val result = usecase.invoke(conversation, messageIdList) + + assertTrue(result is Either.Left) + coVerify { arrangement.messageSender.sendMessage(any(), any()) }.wasNotInvoked() + } + + private class Arrangement { + + @Mock + private val currentClientIdProvider = mock(CurrentClientIdProvider::class) + + @Mock + val messageSender = mock(MessageSender::class) + + suspend fun withCurrentClientIdProvider() = apply { + coEvery { currentClientIdProvider.invoke() }.returns(Either.Right(TestClient.CLIENT_ID)) + } + + suspend fun withCurrentClientIdProviderError() = apply { + coEvery { currentClientIdProvider.invoke() }.returns( + Either.Left( + CoreFailure.Unknown( + RuntimeException("Client ID not available") + ) + ) + ) + } + + suspend fun withMessageSenderResult(result: Either = Unit.right()) = apply { + coEvery { messageSender.sendMessage(any(), any()) }.returns(result) + } + + fun arrange() = this to SendDeliverSignalUseCaseImpl( + selfUserId = TestClient.USER_ID, + currentClientIdProvider = currentClientIdProvider, + messageSender = messageSender, + kaliumLogger = kaliumLogger, + ) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigratorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigratorTest.kt index a60f24f0ced..46ef572af21 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigratorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigratorTest.kt @@ -27,6 +27,7 @@ import com.wire.kalium.logic.data.conversation.MLSConversationRepository import com.wire.kalium.logic.data.conversation.mls.MLSAdditionResult import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.mls.CipherSuite import com.wire.kalium.logic.data.user.UserId @@ -39,7 +40,7 @@ import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.left import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.test_util.TestNetworkResponseError -import com.wire.kalium.logic.util.arrangement.CallRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.CallRepositoryArrangementImpl import com.wire.kalium.logic.util.shouldSucceed import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException @@ -78,18 +79,23 @@ class MLSMigratorTest { migrator.migrateProteusConversations() coVerify { - arrangement.conversationRepository.updateProtocolRemotely(eq(conversation.id), eq(Conversation.Protocol.MIXED)) + arrangement.conversationRepository.updateProtocolRemotely(conversation.id, Conversation.Protocol.MIXED) }.wasInvoked(once) coVerify { - arrangement.mlsConversationRepository.establishMLSGroup(eq(Arrangement.MIXED_PROTOCOL_INFO.groupId), eq(emptyList()), any()) + arrangement.mlsConversationRepository.establishMLSGroup( + groupID = Arrangement.MIXED_PROTOCOL_INFO.groupId, + members = emptyList(), + publicKeys = null, + false + ) } coVerify { arrangement.mlsConversationRepository.addMemberToMLSGroup( - eq(Arrangement.MIXED_PROTOCOL_INFO.groupId), - eq(Arrangement.MEMBERS), - eq(CIPHER_SUITE) + Arrangement.MIXED_PROTOCOL_INFO.groupId, + Arrangement.MEMBERS, + CIPHER_SUITE ) } } @@ -119,7 +125,12 @@ class MLSMigratorTest { }.wasInvoked(once) coVerify { - arrangement.mlsConversationRepository.establishMLSGroup(eq(Arrangement.MIXED_PROTOCOL_INFO.groupId), eq(emptyList()), any()) + arrangement.mlsConversationRepository.establishMLSGroup( + groupID = Arrangement.MIXED_PROTOCOL_INFO.groupId, + members = emptyList(), + publicKeys = null, + allowSkippingUsersWithoutKeyPackages = false + ) } coVerify { @@ -232,7 +243,11 @@ class MLSMigratorTest { suspend fun withGetProteusTeamConversationsReturning(conversationsIds: List) = apply { coEvery { - conversationRepository.getConversationIds(eq(Conversation.Type.GROUP), eq(Conversation.Protocol.PROTEUS), any()) + conversationRepository.getConversationIds( + Conversation.Type.GROUP, + Conversation.Protocol.PROTEUS, + TeamId(value = "Some-team") + ) }.returns(Either.Right(conversationsIds)) } @@ -268,13 +283,13 @@ class MLSMigratorTest { suspend fun withEstablishGroupSucceeds(additionResult: MLSAdditionResult) = apply { coEvery { - mlsConversationRepository.establishMLSGroup(any(), any(), any()) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), any()) }.returns(Either.Right(additionResult)) } suspend fun withEstablishGroupFails() = apply { coEvery { - mlsConversationRepository.establishMLSGroup(any(), any(), any()) + mlsConversationRepository.establishMLSGroup(any(), any(), any(), any()) }.returns(Either.Left(NetworkFailure.ServerMiscommunication(MLS_STALE_MESSAGE_ERROR))) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdaterTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdaterTest.kt index eb7b15f107d..08e9d595adf 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdaterTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/PushTokenUpdaterTest.kt @@ -59,7 +59,7 @@ class PushTokenUpdaterTest { }.wasNotInvoked() coVerify { - arrangement.clientRepository.registerToken(any()) + arrangement.clientRepository.registerToken(any(), any(), any(), any()) }.wasNotInvoked() coVerify { @@ -81,7 +81,7 @@ class PushTokenUpdaterTest { }.wasNotInvoked() coVerify { - arrangement.clientRepository.registerToken(any()) + arrangement.clientRepository.registerToken(any(), any(), any(), any()) }.wasNotInvoked() coVerify { @@ -94,14 +94,27 @@ class PushTokenUpdaterTest { val (arrangement, pushTokenUpdater) = Arrangement() .withUpdateFirebaseTokenFlag(true) .withCurrentClientId(ClientId(MOCK_CLIENT_ID)) - .withNotificationToken(Either.Right(NotificationToken(MOCK_TOKEN, MOCK_TRANSPORT, MOCK_APP_ID))) + .withNotificationToken( + Either.Right( + NotificationToken( + MOCK_TOKEN, + MOCK_TRANSPORT, + MOCK_APP_ID + ) + ) + ) .withRegisterTokenResult(Either.Right(Unit)) .arrange() pushTokenUpdater.monitorTokenChanges() coVerify { - arrangement.clientRepository.registerToken(eq(pushTokenRequestBody)) + arrangement.clientRepository.registerToken( + eq(pushTokenRequestBody.senderId), + eq(pushTokenRequestBody.client), + eq(pushTokenRequestBody.token), + eq(pushTokenRequestBody.transport), + ) }.wasInvoked(once) coVerify { @@ -129,7 +142,8 @@ class PushTokenUpdaterTest { val clientRepository: ClientRepository = mock(ClientRepository::class) @Mock - val notificationTokenRepository: NotificationTokenRepository = mock(NotificationTokenRepository::class) + val notificationTokenRepository: NotificationTokenRepository = + mock(NotificationTokenRepository::class) @Mock val pushTokenRepository: PushTokenRepository = mock(PushTokenRepository::class) @@ -148,7 +162,7 @@ class PushTokenUpdaterTest { suspend fun withRegisterTokenResult(result: Either) = apply { coEvery { - clientRepository.registerToken(any()) + clientRepository.registerToken(any(), any(), any(), any()) }.returns(result) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenToAPIUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenToAPIUseCaseTest.kt new file mode 100644 index 00000000000..4b5c4637d88 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/notificationToken/SendFCMTokenToAPIUseCaseTest.kt @@ -0,0 +1,161 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.notificationToken + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.notification.NotificationToken +import com.wire.kalium.logic.configuration.notification.NotificationTokenRepository +import com.wire.kalium.logic.data.client.ClientRepository +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.fold +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class SendFCMTokenToAPIUseCaseTest { + @Test + fun whenInvokedAndSuccessfulResultInTokenRegistered() = runTest { + + val useCase = + Arrangement() + .withClientId() + .withNotificationToken() + .withClientRepositoryRegisterToken() + .arrange() + + val result = useCase.invoke() + assertEquals(Either.Right(Unit), result) + } + + @Test + fun whenInvokedAndFailureOnClientId() = runTest { + + val useCase = Arrangement() + .withClientIdFailure() + .withNotificationToken() + .arrange() + + val failReason = useCase.invoke().fold( + { it.status }, + { fail("Expected failure, but got success") } + ) + assertEquals(SendFCMTokenError.Reason.CANT_GET_CLIENT_ID, failReason) + + } + + @Test + fun whenInvokedAndFailureOnNotificationToken() = runTest { + + val useCase = Arrangement() + .withClientId() + .withNotificationTokenFailure() + .arrange() + + val failReason = useCase.invoke().fold( + { it.status }, + { fail("Expected failure, but got success") } + ) + assertEquals(SendFCMTokenError.Reason.CANT_GET_NOTIFICATION_TOKEN, failReason) + } + + @Test + fun whenInvokedAndFailureOnClientRepositoryRegisterToken() = runTest { + + val useCase = Arrangement() + .withClientId() + .withNotificationToken() + .withClientRepositoryRegisterTokenFailure() + .arrange() + + val failReason = useCase.invoke().fold( + { it.status }, + { fail("Expected failure, but got success") } + ) + assertEquals(SendFCMTokenError.Reason.CANT_REGISTER_TOKEN, failReason) + } + + + private class Arrangement { + + @Mock + private val currentClientIdProvider: CurrentClientIdProvider = + mock(CurrentClientIdProvider::class) + + @Mock + private val clientRepository: ClientRepository = mock(ClientRepository::class) + + @Mock + private val notificationTokenRepository: NotificationTokenRepository = + mock(NotificationTokenRepository::class) + + + fun arrange(): SendFCMTokenToAPIUseCaseImpl { + return SendFCMTokenToAPIUseCaseImpl( + currentClientIdProvider, clientRepository, notificationTokenRepository + ) + } + + suspend fun withClientId() = apply { + coEvery { + currentClientIdProvider.invoke() + }.returns(Either.Right(ClientId("clientId"))) + } + + suspend fun withClientIdFailure() = apply { + coEvery { + currentClientIdProvider.invoke() + }.returns(Either.Left(CoreFailure.MissingClientRegistration)) + } + + fun withNotificationToken() = apply { + every { + notificationTokenRepository.getNotificationToken() + }.returns(Either.Right(NotificationToken("applicationId", "token", "transport"))) + } + + fun withNotificationTokenFailure() = apply { + every { + notificationTokenRepository.getNotificationToken() + }.returns(Either.Left(StorageFailure.DataNotFound)) + } + + suspend fun withClientRepositoryRegisterToken() = apply { + coEvery { + clientRepository.registerToken(any(), any(), any(), any()) + }.returns(Either.Right(Unit)) + } + + suspend fun withClientRepositoryRegisterTokenFailure() = apply { + coEvery { + clientRepository.registerToken(any(), any(), any(), any()) + }.returns(Either.Left(NetworkFailure.FeatureNotSupported)) + } + + } + +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt new file mode 100644 index 00000000000..9a806ec5816 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/personaltoteamaccount/CanMigrateFromPersonalToTeamUseCaseTest.kt @@ -0,0 +1,149 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.personaltoteamaccount + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.configuration.server.ServerConfigRepository +import com.wire.kalium.logic.data.id.SelfTeamIdProvider +import com.wire.kalium.logic.data.id.TeamId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO +import com.wire.kalium.network.session.SessionManager +import com.wire.kalium.network.utils.TestRequestHandler.Companion.TEST_BACKEND_CONFIG +import io.mockative.Mock +import io.mockative.coEvery +import io.mockative.every +import io.mockative.mock +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CanMigrateFromPersonalToTeamUseCaseTest { + + @Test + fun givenAPIVersionBelowMinimumAndUserNotInATeam_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Right(null)) + .withServerConfig(6) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + @Test + fun givenAPIVersionEqualToMinimumAndUserNotInATeam_whenInvoking_thenReturnsTrue() = + runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withServerConfig(7) + .withTeamId(Either.Right(null)) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertTrue(result) + } + + @Test + fun givenAPIVersionAboveMinimumAndUserInATeam_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Right(TeamId("teamId"))) + .withServerConfig(9) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + + @Test + fun givenSelfTeamIdProviderFailure_whenInvoking_thenReturnsFalse() = runTest { + // Given + val (_, useCase) = Arrangement() + .withRepositoryReturningMinimumApiVersion() + .withTeamId(Either.Left(CoreFailure.MissingClientRegistration)) + .withServerConfig(9) + .arrange() + + // When + val result = useCase.invoke() + + // Then + assertFalse(result) + } + + private class Arrangement { + + @Mock + val serverConfigRepository = mock(ServerConfigRepository::class) + + @Mock + val sessionManager = mock(SessionManager::class) + + @Mock + val selfTeamIdProvider = mock(SelfTeamIdProvider::class) + + suspend fun withTeamId(result: Either) = apply { + coEvery { + selfTeamIdProvider() + }.returns(result) + } + + fun withRepositoryReturningMinimumApiVersion() = apply { + every { + serverConfigRepository.minimumApiVersionForPersonalToTeamAccountMigration + }.returns(MIN_API_VERSION) + } + + fun withServerConfig(apiVersion: Int) = apply { + val backendConfig = TEST_BACKEND_CONFIG.copy( + metaData = TEST_BACKEND_CONFIG.metaData.copy( + commonApiVersion = ApiVersionDTO.Valid(apiVersion) + ) + ) + every { + sessionManager.serverConfig() + }.returns(backendConfig) + } + + fun arrange() = this to CanMigrateFromPersonalToTeamUseCaseImpl( + sessionManager = sessionManager, + serverConfigRepository = serverConfigRepository, + selfTeamIdProvider = selfTeamIdProvider + ) + } + + companion object { + private const val MIN_API_VERSION = 7 + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParserTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParserTest.kt index 214d05d56df..e7219e68253 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParserTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/FederatedSearchParserTest.kt @@ -41,7 +41,7 @@ class FederatedSearchParserTest { } val searchQuery = "searchQuery" - val result = federatedSearchParser(searchQuery) + val result = federatedSearchParser(searchQuery, true) assertEquals(searchQuery, result.searchTerm) assertEquals(selfUserId.domain, result.domain) @@ -58,7 +58,7 @@ class FederatedSearchParserTest { } val searchQuery = "search Query" - val result = federatedSearchParser(searchQuery) + val result = federatedSearchParser(searchQuery, true) assertEquals(searchQuery, result.searchTerm) assertEquals(selfUserId.domain, result.domain) @@ -75,7 +75,7 @@ class FederatedSearchParserTest { } val searchQuery = " search Query @domain.co" - val result = federatedSearchParser(searchQuery) + val result = federatedSearchParser(searchQuery, true) assertEquals(" search Query ", result.searchTerm) assertEquals("domain.co", result.domain) @@ -92,9 +92,26 @@ class FederatedSearchParserTest { } val searchQuery = " search Query @domain.co" - federatedSearchParser(searchQuery) - federatedSearchParser(searchQuery) - federatedSearchParser(searchQuery) + federatedSearchParser(searchQuery, true) + federatedSearchParser(searchQuery, true) + federatedSearchParser(searchQuery, true) + + coVerify { + arrangement.sessionRepository.isFederated(eq(selfUserId)) + }.wasInvoked(exactly = once) + } + + @Test + fun givenUserIsNotFederated_whenSearchQueryIncludeDomainButRemoteDomainForbidden_thenSearchQueryIsNotModified() = runTest { + val (arrangement, federatedSearchParser) = Arrangement().arrange { + withIsFederated(result = true.right(), userId = AnyMatcher(valueOf())) + } + + val searchQuery = " search Query @domain.co" + val result = federatedSearchParser(searchQuery, false) + + assertEquals(" search Query @domain.co", result.searchTerm) + assertEquals(selfUserId.domain, result.domain) coVerify { arrangement.sessionRepository.isFederated(eq(selfUserId)) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt new file mode 100644 index 00000000000..0ac583443fd --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/search/IsFederationSearchAllowedUseCaseTest.kt @@ -0,0 +1,166 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.search + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.mls.MLSPublicKeys +import com.wire.kalium.logic.data.mlspublickeys.MLSPublicKeysRepository +import com.wire.kalium.logic.data.user.SupportedProtocol +import com.wire.kalium.logic.feature.conversation.GetConversationProtocolInfoUseCase +import com.wire.kalium.logic.feature.user.GetDefaultProtocolUseCase +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestConversation.PROTEUS_PROTOCOL_INFO +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcherImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class IsFederationSearchAllowedUseCaseTest { + + @Test + fun givenMLSIsNotConfigured_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = false) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasNotInvoked() + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeamWithEmptyKeys_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withEmptyMlsKeys() + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasNotInvoked() + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeam_whenInvokingIsFederationSearchAllowed_thenReturnTrue() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = null) + + assertEquals(true, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasNotInvoked() + } + + @Test + fun givenMLSIsConfiguredAndAMLSTeamAndProteusProtocol_whenInvokingIsFederationSearchAllowed_thenReturnFalse() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.MLS) + .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(PROTEUS_PROTOCOL_INFO)) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = TestConversation.ID) + + assertEquals(false, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasInvoked(once) + } + + @Test + fun givenMLSIsConfiguredAndAProteusTeamAndProteusProtocol_whenInvokingIsFederationSearchAllowed_thenReturnFalse() = runTest { + val (arrangement, isFederationSearchAllowedUseCase) = Arrangement() + .withMLSConfiguredForBackend(isConfigured = true) + .withDefaultProtocol(SupportedProtocol.PROTEUS) + .withConversationProtocolInfo(GetConversationProtocolInfoUseCase.Result.Success(PROTEUS_PROTOCOL_INFO)) + .arrange() + + val isAllowed = isFederationSearchAllowedUseCase(conversationId = TestConversation.ID) + + assertEquals(false, isAllowed) + coVerify { arrangement.mlsPublicKeysRepository.getKeys() }.wasInvoked(once) + coVerify { arrangement.getDefaultProtocol.invoke() }.wasInvoked(once) + coVerify { arrangement.getConversationProtocolInfo.invoke(any()) }.wasInvoked(once) + } + + private class Arrangement { + + @Mock + val mlsPublicKeysRepository = mock(MLSPublicKeysRepository::class) + + @Mock + val getDefaultProtocol = mock(GetDefaultProtocolUseCase::class) + + @Mock + val getConversationProtocolInfo = mock(GetConversationProtocolInfoUseCase::class) + + private val MLS_PUBLIC_KEY = MLSPublicKeys( + removal = mapOf( + "ed25519" to "gRNvFYReriXbzsGu7zXiPtS8kaTvhU1gUJEV9rdFHVw=" + ) + ) + + fun withDefaultProtocol(protocol: SupportedProtocol) = apply { + every { getDefaultProtocol.invoke() }.returns(protocol) + } + + suspend fun withConversationProtocolInfo(protocolInfo: GetConversationProtocolInfoUseCase.Result) = apply { + coEvery { getConversationProtocolInfo(any()) }.returns(protocolInfo) + } + + suspend fun withMLSConfiguredForBackend(isConfigured: Boolean = true) = apply { + coEvery { mlsPublicKeysRepository.getKeys() }.returns( + if (isConfigured) { + Either.Right(MLS_PUBLIC_KEY) + } else { + Either.Left(CoreFailure.Unknown(RuntimeException("MLS is not configured"))) + } + ) + } + + suspend fun withEmptyMlsKeys() = apply { + coEvery { mlsPublicKeysRepository.getKeys() }.returns(Either.Right(MLSPublicKeys(emptyMap()))) + } + + fun arrange() = this to IsFederationSearchAllowedUseCase( + mlsPublicKeysRepository = mlsPublicKeysRepository, + getDefaultProtocol = getDefaultProtocol, + getConversationProtocolInfo = getConversationProtocolInfo, + dispatcher = KaliumDispatcherImpl + ) + } +} + + diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt new file mode 100644 index 00000000000..a52b866fd48 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/ShouldAskCallFeedbackUseCaseTest.kt @@ -0,0 +1,89 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.functional.left +import com.wire.kalium.logic.functional.right +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl +import com.wire.kalium.util.DateTimeUtil +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.days + +class ShouldAskCallFeedbackUseCaseTest { + + @Test + fun givenNoNextTimeForCallFeedbackSaved_whenInvoked_thenTrueIsReturned() = runTest { + val (_, useCase) = Arrangement().arrange { + withGetNextTimeForCallFeedback(StorageFailure.DataNotFound.left()) + } + + val result = useCase() + + assertTrue(result) + } + + @Test + fun givenNextTimeForCallFeedbackInPast_whenInvoked_thenTrueIsReturned() = runTest { + val nextTimeToAsk = DateTimeUtil.currentInstant().minus(1.days).toEpochMilliseconds() + val (_, useCase) = Arrangement().arrange { + withGetNextTimeForCallFeedback(nextTimeToAsk.right()) + } + + val result = useCase() + + assertTrue(result) + } + + @Test + fun givenNextTimeForCallFeedbackInFuture_whenInvoked_thenFalseIsReturned() = runTest { + val nextTimeToAsk = DateTimeUtil.currentInstant().plus(1.days).toEpochMilliseconds() + val (_, useCase) = Arrangement().arrange { + withGetNextTimeForCallFeedback(nextTimeToAsk.right()) + } + + val result = useCase() + + assertFalse(result) + } + + @Test + fun givenNextTimeForCallFeedbackIsNegative_whenInvoked_thenFalseIsReturned() = runTest { + val nextTimeToAsk = -1L + val (_, useCase) = Arrangement().arrange { + withGetNextTimeForCallFeedback(nextTimeToAsk.right()) + } + + val result = useCase() + + assertFalse(result) + } + + private class Arrangement : UserConfigRepositoryArrangement by UserConfigRepositoryArrangementImpl() { + + fun arrange(block: suspend Arrangement.() -> Unit): Pair { + runBlocking { block() } + return this to ShouldAskCallFeedbackUseCase(userConfigRepository) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCaseTest.kt new file mode 100644 index 00000000000..d1333725108 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UpdateNextTimeCallFeedbackUseCaseTest.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.user + +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.UserConfigRepositoryArrangementImpl +import com.wire.kalium.util.DateTimeUtil +import io.mockative.coVerify +import io.mockative.eq +import io.mockative.matches +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.minutes + +class UpdateNextTimeCallFeedbackUseCaseTest { + + @Test + fun givenNeverAskAgainIsTrue_whenInvoked_thenNextTimeSetToNegative() = runTest { + val (arrangement, useCase) = Arrangement().arrange { + withUpdateNextTimeForCallFeedback() + } + + useCase(true) + + coVerify { arrangement.userConfigRepository.updateNextTimeForCallFeedback(eq(-1L)) } + .wasInvoked() + } + + @Test + fun givenNeverAskAgainIsFalse_whenInvoked_thenNextTimeSetToNegative() = runTest { + val (arrangement, useCase) = Arrangement().arrange { + withUpdateNextTimeForCallFeedback() + } + val expectedMin = DateTimeUtil.currentInstant().plus(3.days).toEpochMilliseconds() + val expectedMax = DateTimeUtil.currentInstant().plus(3.days).plus(10.minutes).toEpochMilliseconds() + + useCase(false) + + coVerify { arrangement.userConfigRepository.updateNextTimeForCallFeedback(matches { it in expectedMin.. Unit): Pair { + runBlocking { block() } + return this to UpdateNextTimeCallFeedbackUseCase(userConfigRepository) + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt index 943cac79327..02108d9c82b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/UploadUserAvatarUseCaseTest.kt @@ -21,15 +21,9 @@ package com.wire.kalium.logic.feature.user import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.asset.AssetRepository import com.wire.kalium.logic.data.asset.UploadedAssetId -import com.wire.kalium.logic.data.user.ConnectionState -import com.wire.kalium.logic.data.user.SelfUser -import com.wire.kalium.logic.data.user.SupportedProtocol -import com.wire.kalium.logic.data.user.UserAssetId -import com.wire.kalium.logic.data.user.UserAvailabilityStatus -import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.user.type.UserType import com.wire.kalium.logic.functional.Either +import com.wire.kalium.util.KaliumDispatcherImpl import io.mockative.Mock import io.mockative.any import io.mockative.coEvery @@ -106,29 +100,15 @@ class UploadUserAvatarUseCaseTest { @Mock val userRepository = mock(UserRepository::class) - private val uploadUserAvatarUseCase: UploadUserAvatarUseCase = UploadUserAvatarUseCaseImpl(userRepository, assetRepository) + val dispatcher = KaliumDispatcherImpl + + private val uploadUserAvatarUseCase: UploadUserAvatarUseCase = + UploadUserAvatarUseCaseImpl(userRepository, assetRepository, dispatcher) var userHomePath = "/Users/me".toPath() val fakeFileSystem = FakeFileSystem().also { it.createDirectories(userHomePath) } - private val dummySelfUser = SelfUser( - id = UserId("some_id", "some_domain"), - name = "some_name", - handle = "some_handle", - email = "some_email", - phone = null, - accentId = 1, - teamId = null, - connectionStatus = ConnectionState.ACCEPTED, - previewPicture = UserAssetId("value1", "domain"), - completePicture = UserAssetId("value2", "domain"), - userType = UserType.INTERNAL, - availabilityStatus = UserAvailabilityStatus.NONE, - expiresAt = null, - supportedProtocols = setOf(SupportedProtocol.PROTEUS) - ) - fun withStoredData(data: ByteArray, dataNamePath: Path): Arrangement { val fullDataPath = "$userHomePath/$dataNamePath".toPath() fakeFileSystem.write(fullDataPath) { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt new file mode 100644 index 00000000000..23f8ce4db9f --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/user/migration/MigrateFromPersonalToTeamUseCaseTest.kt @@ -0,0 +1,170 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.logic.feature.user.migration + +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.user.CreateUserTeam +import com.wire.kalium.logic.data.user.UserRepository +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.network.api.model.ErrorResponse +import com.wire.kalium.network.exceptions.KaliumException +import io.ktor.http.HttpStatusCode +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class MigrateFromPersonalToTeamUseCaseTest { + + @Test + fun givenRepositorySucceeds_whenMigratingUserToTeam_thenShouldPropagateSuccess() = runTest { + val (arrangement, useCase) = Arrangement() + .withUpdateTeamIdReturning(Either.Right(Unit)) + .withMigrationSuccess() + .arrange() + + val result = useCase(teamName = "teamName") + + coVerify { + arrangement.userRepository.updateTeamId(any(), any()) + }.wasInvoked(exactly = once) + assertTrue(arrangement.isCachedTeamIdInvalidated) + assertIs(result) + } + + @Test + fun givenRepositoryFailsWithNoNetworkConnection_whenMigratingUserToTeam_thenShouldPropagateNoNetworkFailure() = + runTest { + val (_, useCase) = Arrangement() + .withMigrationNoNetworkFailure() + .arrange() + + val result = useCase(teamName = "teamName") + + assertIs(result) + assertIs(result.failure) + } + + @Test + fun givenRepositoryFailsWithUserAlreadyInTeam_whenMigratingUserToTeam_thenShouldPropagateUserAlreadyInTeamFailure() = + runTest { + val (_, useCase) = Arrangement() + .withUserAlreadyInTeamFailure() + .arrange() + + val result = useCase(teamName = "teamName") + + assertIs(result) + assertIs(result.failure) + } + + @Test + fun givenRepositoryFailsWithUnknownError_whenMigratingUserToTeam_thenShouldPropagateUnknownFailure() = + runTest { + val (_, useCase) = Arrangement() + .withMigrationUserNotFoundFailure() + .arrange() + + val result = useCase(teamName = "teamName") + + assertIs(result) + assertIs(result.failure) + val coreFailure = + (result.failure as MigrateFromPersonalToTeamFailure.UnknownError).coreFailure + val serverMiscommunication = coreFailure as NetworkFailure.ServerMiscommunication + val invalidRequestError = + serverMiscommunication.kaliumException as KaliumException.InvalidRequestError + val errorLabel = invalidRequestError.errorResponse.label + + assertEquals("not-found", errorLabel) + } + + + private class Arrangement { + @Mock + val userRepository: UserRepository = mock(UserRepository::class) + + var isCachedTeamIdInvalidated = false + + suspend fun withMigrationSuccess() = apply { + coEvery { userRepository.migrateUserToTeam(any()) }.returns( + Either.Right( + CreateUserTeam("teamId", "teamName") + ) + ) + } + + suspend fun withUserAlreadyInTeamFailure() = withMigrationReturning( + Either.Left( + NetworkFailure.ServerMiscommunication( + KaliumException.InvalidRequestError( + ErrorResponse( + HttpStatusCode.Forbidden.value, + message = "Switching teams is not allowed", + label = "user-already-in-a-team", + ) + ) + ) + ) + ) + + + suspend fun withMigrationUserNotFoundFailure() = withMigrationReturning( + Either.Left( + NetworkFailure.ServerMiscommunication( + KaliumException.InvalidRequestError( + ErrorResponse( + HttpStatusCode.NotFound.value, + message = "User not found", + label = "not-found", + ) + ) + ) + ) + ) + + suspend fun withMigrationNoNetworkFailure() = withMigrationReturning( + Either.Left(NetworkFailure.NoNetworkConnection(null)) + ) + + suspend fun withMigrationReturning(result: Either) = apply { + coEvery { userRepository.migrateUserToTeam(any()) }.returns(result) + } + + suspend fun withUpdateTeamIdReturning(result: Either) = apply { + coEvery { userRepository.updateTeamId(any(), any()) }.returns(result) + } + + fun arrange() = this to MigrateFromPersonalToTeamUseCaseImpl(selfUserId = TestUser.SELF.id, + userRepository = userRepository, + invalidateTeamId = { + isCachedTeamIdInvalidated = true + }) + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt index 6bb4fc0532f..7904879e12f 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestCall.kt @@ -91,12 +91,14 @@ object TestCall { ) ), activeSpeakers = mapOf(QualifiedID("participantId", "participantDomain") to listOf("abcd")), + callerId = CALLER_ID, users = listOf( OtherUserMinimized( id = QualifiedID("participantId", "participantDomain"), name = "User Name", completePicture = null, - userType = UserType.ADMIN + userType = UserType.ADMIN, + accentId = 0 ) ) ) @@ -107,7 +109,7 @@ object TestCall { isMuted = true, isCameraOn = false, isCbrEnabled = false, - callerId = CALLER_ID.toString(), + callerId = CALLER_ID, conversationName = CONVERSATION_NAME, conversationType = Conversation.Type.ONE_ON_ONE, callerName = CALLER_NAME, @@ -124,7 +126,8 @@ object TestCall { name = "User Name", avatarAssetId = null, userType = UserType.ADMIN, - isSpeaking = false + isSpeaking = false, + accentId = 0 ) ), maxParticipants = 0 @@ -137,11 +140,11 @@ object TestCall { false, false, false, - "client1", + UserId("client1", "domain"), "ONE_ON_ONE Name ${convId.value}", Conversation.Type.ONE_ON_ONE, null, - null + null, ) fun groupIncomingCall(convId: ConversationId) = @@ -151,7 +154,7 @@ object TestCall { false, false, false, - "client1", + UserId("client1", "domain"), "ONE_ON_ONE Name ${convId.value}", Conversation.Type.GROUP, null, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt index 70dc26c8616..6f36299004d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversation.kt @@ -149,7 +149,6 @@ object TestConversation { userDeleted = false, connectionStatus = null, otherUserId = null, - isCreator = 0L, lastNotificationDate = null, protocolInfo = protocolInfo, creatorId = "someValue", @@ -176,6 +175,8 @@ object TestConversation { userSupportedProtocols = null, userActiveOneOnOneConversationId = null, legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, + accentId = null, + isFavorite = false ) fun one_on_one(convId: ConversationId) = Conversation( @@ -313,7 +314,6 @@ object TestConversation { userDeleted = false, connectionStatus = null, otherUserId = null, - isCreator = 0L, lastNotificationDate = null, protocolInfo = ConversationEntity.ProtocolInfo.Proteus, creatorId = "someValue", @@ -339,7 +339,9 @@ object TestConversation { proteusVerificationStatus = ConversationEntity.VerificationStatus.NOT_VERIFIED, userSupportedProtocols = null, userActiveOneOnOneConversationId = null, - legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED + legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, + accentId = null, + isFavorite = false ) val MLS_PROTOCOL_INFO = ProtocolInfo.MLS( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt index 8a6a2eb9fce..1dce7148793 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestConversationDetails.kt @@ -41,16 +41,11 @@ object TestConversationDetails { TestConversation.ONE_ON_ONE(), TestUser.OTHER, UserType.EXTERNAL, - lastMessage = null, - unreadEventCount = emptyMap() ) val CONVERSATION_GROUP = ConversationDetails.Group( conversation = TestConversation.GROUP(), - lastMessage = null, - isSelfUserCreator = true, isSelfUserMember = true, - unreadEventCount = emptyMap(), selfRole = Conversation.Member.Role.Member ) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt index 2ffcb8c3a99..5536aa2bcae 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestEvent.kt @@ -22,6 +22,8 @@ import com.wire.kalium.cryptography.utils.EncryptedData import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.Conversation.Member +import com.wire.kalium.logic.data.conversation.FolderType +import com.wire.kalium.logic.data.conversation.FolderWithConversations import com.wire.kalium.logic.data.conversation.MutedConversationStatus import com.wire.kalium.logic.data.event.Event import com.wire.kalium.logic.data.event.EventDeliveryInfo @@ -33,6 +35,8 @@ import com.wire.kalium.logic.data.user.Connection import com.wire.kalium.logic.data.user.ConnectionState import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.sync.incremental.EventSource +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity import com.wire.kalium.util.time.UNIX_FIRST_DATE import io.ktor.util.encodeBase64 import kotlinx.datetime.Instant @@ -139,6 +143,14 @@ object TestEvent { senderUserId = TestUser.USER_ID ) + fun accessUpdate(eventId: String = "eventId") = Event.Conversation.AccessUpdate( + id = eventId, + conversationId = TestConversation.ID, + access = setOf(Conversation.Access.PRIVATE), + accessRole = setOf(Conversation.AccessRole.TEAM_MEMBER, Conversation.AccessRole.SERVICE), + qualifiedFrom = TestUser.USER_ID + ) + fun teamMemberLeave(eventId: String = "eventId") = Event.Team.MemberLeave( eventId, teamId = "teamId", @@ -159,6 +171,18 @@ object TestEvent { value = true ) + fun foldersUpdate(eventId: String = "eventId") = Event.UserProperty.FoldersUpdate( + id = eventId, + folders = listOf( + FolderWithConversations( + id = "folder1", + name = "Favorites", + type = FolderType.FAVORITE, + conversationIdList = listOf(TestConversation.ID) + ) + ) + ) + fun newMessageEvent( encryptedContent: String, senderUserId: UserId = TestUser.USER_ID, @@ -200,13 +224,6 @@ object TestEvent { timestampIso = "2022-03-30T15:36:00.000Z" ) - fun newAccessUpdateEvent() = Event.Conversation.AccessUpdate( - id = "eventId", - conversationId = TestConversation.ID, - data = TestConversation.CONVERSATION_RESPONSE, - qualifiedFrom = TestUser.USER_ID, - ) - fun codeUpdated() = Event.Conversation.CodeUpdated( id = "eventId", conversationId = TestConversation.ID, @@ -248,7 +265,10 @@ object TestEvent { id = "eventId", ) - fun Event.wrapInEnvelope(isTransient: Boolean = false, source: EventSource = EventSource.LIVE): EventEnvelope { + fun Event.wrapInEnvelope( + isTransient: Boolean = false, + source: EventSource = EventSource.LIVE + ): EventEnvelope { return EventEnvelope(this, EventDeliveryInfo(isTransient, source)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt new file mode 100644 index 00000000000..e194be3b22e --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestFolder.kt @@ -0,0 +1,36 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.framework + +import com.wire.kalium.logic.data.conversation.ConversationFolder +import com.wire.kalium.logic.data.conversation.FolderType + +object TestFolder { + + val USER = ConversationFolder( + id = "folderId", + name = "friends", + type = FolderType.USER + ) + + val FAVORITE = ConversationFolder( + id = "favoriteFolderId", + name = "", + type = FolderType.FAVORITE + ) +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt index d3ac5e3c9a9..c147498c3fc 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/framework/TestUser.kt @@ -189,5 +189,6 @@ object TestUser { name = "otherUsername", completePicture = UserAssetId("value2", "domain"), userType = UserType.EXTERNAL, + accentId = 0 ) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/AvsSyncStateReporterTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/AvsSyncStateReporterTest.kt index 078a81f8cba..1fd271a0a6b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/AvsSyncStateReporterTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/AvsSyncStateReporterTest.kt @@ -30,6 +30,7 @@ import io.mockative.once import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.time.Duration class AvsSyncStateReporterTest { @@ -124,7 +125,7 @@ class AvsSyncStateReporterTest { fun withFailedIncrementalSyncState() = apply { every { incrementalSyncRepository.incrementalSyncState - }.returns(flowOf(IncrementalSyncStatus.Failed(CoreFailure.SyncEventOrClientNotFound))) + }.returns(flowOf(IncrementalSyncStatus.Failed(CoreFailure.SyncEventOrClientNotFound, Duration.ZERO))) } } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCaseTest.kt index 79373e0057c..2b2117d8222 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/ObserveSyncStateUseCaseTest.kt @@ -28,7 +28,6 @@ import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.test_util.TestKaliumDispatcher import io.mockative.Mock -import io.mockative.coEvery import io.mockative.every import io.mockative.mock import kotlinx.coroutines.flow.MutableStateFlow @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.time.Duration class ObserveSyncStateUseCaseTest { @@ -49,7 +49,7 @@ class ObserveSyncStateUseCaseTest { useCase().test { val item = awaitItem() - assertEquals(SyncState.Failed(coreFailure), item) + assertEquals(SyncState.Failed(coreFailure, Duration.ZERO), item) } } @@ -102,7 +102,7 @@ class ObserveSyncStateUseCaseTest { useCase().test { val item = awaitItem() - assertEquals(SyncState.Failed(coreFailure), item) + assertEquals(SyncState.Failed(coreFailure, Duration.ZERO), item) } } @@ -198,7 +198,7 @@ class ObserveSyncStateUseCaseTest { companion object { val coreFailure = CoreFailure.Unknown(null) - val slowSyncFailureFlow = MutableStateFlow(SlowSyncStatus.Failed(coreFailure)).asStateFlow() - val incrementalSyncFailureFlow = flowOf(IncrementalSyncStatus.Failed(coreFailure)) + val slowSyncFailureFlow = MutableStateFlow(SlowSyncStatus.Failed(coreFailure, Duration.ZERO)).asStateFlow() + val incrementalSyncFailureFlow = flowOf(IncrementalSyncStatus.Failed(coreFailure, Duration.ZERO)) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/SyncManagerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/SyncManagerTest.kt index 4daf2eb5087..96d494d8bdf 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/SyncManagerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/SyncManagerTest.kt @@ -21,10 +21,10 @@ package com.wire.kalium.logic.sync import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.session.SessionRepository import com.wire.kalium.logic.data.sync.InMemoryIncrementalSyncRepository -import com.wire.kalium.logic.data.sync.SlowSyncRepositoryImpl import com.wire.kalium.logic.data.sync.IncrementalSyncRepository import com.wire.kalium.logic.data.sync.IncrementalSyncStatus import com.wire.kalium.logic.data.sync.SlowSyncRepository +import com.wire.kalium.logic.data.sync.SlowSyncRepositoryImpl import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.util.shouldFail @@ -40,6 +40,7 @@ import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.time.Duration @OptIn(ExperimentalCoroutinesApi::class) class SyncManagerTest { @@ -47,7 +48,7 @@ class SyncManagerTest { @Test fun givenSlowSyncFailed_whenWaitingUntilLiveOrFailure_thenShouldReturnFailure() = runTest { val (arrangement, syncManager) = Arrangement().arrange() - arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration)) + arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO)) arrangement.incrementalSyncRepository.updateIncrementalSyncState(IncrementalSyncStatus.Pending) val result = syncManager.waitUntilLiveOrFailure() @@ -59,7 +60,7 @@ class SyncManagerTest { fun givenIncrementalSyncFailedAndSlowSyncIsComplete_whenWaitingUntilLiveOrFailure_thenShouldReturnFailure() = runTest { val (arrangement, syncManager) = Arrangement().arrange() arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Complete) - val failedState = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration) + val failedState = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO) arrangement.incrementalSyncRepository.updateIncrementalSyncState(failedState) val result = syncManager.waitUntilLiveOrFailure() @@ -79,7 +80,7 @@ class SyncManagerTest { advanceUntilIdle() assertTrue { result.isActive } - arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration)) + arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO)) advanceUntilIdle() result.await().shouldFail() } @@ -97,7 +98,7 @@ class SyncManagerTest { assertTrue { result.isActive } arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Complete) - val failure = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration) + val failure = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO) arrangement.incrementalSyncRepository.updateIncrementalSyncState(failure) advanceUntilIdle() result.await().shouldFail() @@ -144,7 +145,7 @@ class SyncManagerTest { advanceUntilIdle() assertTrue { result.isActive } - val failure = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration) + val failure = IncrementalSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO) arrangement.incrementalSyncRepository.updateIncrementalSyncState(failure) advanceUntilIdle() assertTrue { result.isCompleted } @@ -155,7 +156,7 @@ class SyncManagerTest { @Test fun givenSlowSyncFailed_whenWaitingUntilStartedOrFailure_thenShouldReturnFailure() = runTest { val (arrangement, syncManager) = Arrangement().arrange() - arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration)) + arrangement.slowSyncRepository.updateSlowSyncStatus(SlowSyncStatus.Failed(CoreFailure.MissingClientRegistration, Duration.ZERO)) val result = syncManager.waitUntilStartedOrFailure() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventGathererTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventGathererTest.kt index dc0b1451608..4ae56bb468a 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventGathererTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventGathererTest.kt @@ -30,10 +30,12 @@ import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.framework.TestEvent.wrapInEnvelope import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.sync.KaliumSyncException +import com.wire.kalium.logic.util.ServerTimeHandler import com.wire.kalium.network.api.base.authenticated.notification.WebSocketEvent import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.exceptions.KaliumException import io.mockative.Mock +import io.mockative.any import io.mockative.coEvery import io.mockative.coVerify import io.mockative.every @@ -66,6 +68,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning("2022-03-30T15:36:00.000Z") .arrange() eventGatherer.gatherEvents().test { @@ -82,6 +85,10 @@ class EventGathererTest { arrangement.eventRepository.pendingEvents() }.wasInvoked(exactly = once) + coVerify { + arrangement.serverTimeHandler.computeTimeOffset(any()) + }.wasInvoked(exactly = once) + cancelAndIgnoreRemainingEvents() } } @@ -95,6 +102,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withConnectionPolicyReturning(MutableStateFlow(ConnectionPolicy.DISCONNECT_AFTER_PENDING_EVENTS)) .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -125,6 +133,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Right(pendingEvent))) .withConnectionPolicyReturning(MutableStateFlow(ConnectionPolicy.DISCONNECT_AFTER_PENDING_EVENTS)) .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -157,6 +166,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withConnectionPolicyReturning(MutableStateFlow(ConnectionPolicy.KEEP_ALIVE)) .withLiveEventsReturning(Either.Right(liveEventsChannel)) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -171,11 +181,12 @@ class EventGathererTest { fun givenWebsocketEventAndDisconnectPolicy_whenGathering_thenShouldCompleteFlow() = runTest { val liveEventsChannel = Channel>(capacity = Channel.UNLIMITED) - val (arrangement, eventGatherer) = Arrangement() + val (_, eventGatherer) = Arrangement() .withLastEventIdReturning(Either.Right("lastEventId")) .withPendingEventsReturning(emptyFlow()) .withConnectionPolicyReturning(MutableStateFlow(ConnectionPolicy.DISCONNECT_AFTER_PENDING_EVENTS)) .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() // Open Websocket should trigger fetching pending events @@ -198,6 +209,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -222,6 +234,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withConnectionPolicyReturning(MutableStateFlow(ConnectionPolicy.DISCONNECT_AFTER_PENDING_EVENTS)) .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -248,6 +261,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Left(failureCause))) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -272,6 +286,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Left(failureCause))) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.receiveAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -293,6 +308,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(emptyFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -315,6 +331,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Right(event))) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() // Open Websocket should trigger fetching pending events @@ -337,6 +354,7 @@ class EventGathererTest { .withPendingEventsReturning(emptyFlow()) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() // Open Websocket should trigger fetching pending events @@ -362,6 +380,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Right(event))) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() // Open Websocket should trigger fetching pending events @@ -397,6 +416,7 @@ class EventGathererTest { .withPendingEventsReturning(flowOf(Either.Left(failureCause))) .withKeepAliveConnectionPolicy() .withLiveEventsReturning(Either.Right(liveEventsChannel.consumeAsFlow())) + .withFetchServerTimeReturning(null) .arrange() eventGatherer.gatherEvents().test { @@ -419,7 +439,10 @@ class EventGathererTest { @Mock val incrementalSyncRepository = mock(IncrementalSyncRepository::class) - val eventGatherer: EventGatherer = EventGathererImpl(eventRepository, incrementalSyncRepository) + @Mock + val serverTimeHandler = mock(ServerTimeHandler::class) + + val eventGatherer: EventGatherer = EventGathererImpl(eventRepository, incrementalSyncRepository, serverTimeHandler) suspend fun withLiveEventsReturning(either: Either>>) = apply { coEvery { @@ -427,6 +450,12 @@ class EventGathererTest { }.returns(either) } + suspend fun withFetchServerTimeReturning(time: String?) = apply { + coEvery { + eventRepository.fetchServerTime() + }.returns(time) + } + suspend fun withPendingEventsReturning(either: Flow>) = apply { coEvery { eventRepository.pendingEvents() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessorTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessorTest.kt index 20fb72d2e8e..a35491fafa7 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessorTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/incremental/EventProcessorTest.kt @@ -34,15 +34,26 @@ import com.wire.kalium.logic.util.arrangement.eventHandler.FeatureConfigEventRec import com.wire.kalium.logic.util.shouldFail import io.mockative.Mock import io.mockative.any -import io.mockative.eq import io.mockative.coEvery import io.mockative.coVerify +import io.mockative.eq import io.mockative.mock import io.mockative.once +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest +import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +@OptIn(ExperimentalCoroutinesApi::class) class EventProcessorTest { @Test @@ -50,7 +61,7 @@ class EventProcessorTest { // Given val event = TestEvent.memberJoin() - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -68,7 +79,7 @@ class EventProcessorTest { // Given val event = TestEvent.memberJoin() - val (arrangement, eventProcessor) = Arrangement() + val (arrangement, eventProcessor) = Arrangement(this) .withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) .arrange() @@ -87,7 +98,7 @@ class EventProcessorTest { val event = TestEvent.memberJoin() val failure = CoreFailure.MissingClientRegistration - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withConversationEventReceiverFailingWith(failure) withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -107,7 +118,7 @@ class EventProcessorTest { // Given val event = TestEvent.newConnection() - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -126,7 +137,7 @@ class EventProcessorTest { val event = TestEvent.newConnection() val failure = CoreFailure.MissingClientRegistration - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUserEventReceiverFailingWith(failure) withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -146,7 +157,7 @@ class EventProcessorTest { // Given val envelope = TestEvent.newConnection().wrapInEnvelope(isTransient = false) - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUpdateLastProcessedEventId(envelope.event.id, Either.Right(Unit)) } @@ -164,7 +175,7 @@ class EventProcessorTest { // Given val event = TestEvent.newConnection().wrapInEnvelope(isTransient = true) - val (arrangement, eventProcessor) = Arrangement().arrange() + val (arrangement, eventProcessor) = Arrangement(this).arrange() // When eventProcessor.processEvent(event) @@ -180,7 +191,7 @@ class EventProcessorTest { // Given val event = TestEvent.userPropertyReadReceiptMode() - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -197,7 +208,7 @@ class EventProcessorTest { val event = TestEvent.userPropertyReadReceiptMode() val failure = CoreFailure.MissingClientRegistration - val (arrangement, eventProcessor) = Arrangement().arrange { + val (arrangement, eventProcessor) = Arrangement(this).arrange { withUserPropertiesEventReceiverFailingWith(failure) withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) } @@ -212,7 +223,88 @@ class EventProcessorTest { }.wasNotInvoked() } - private class Arrangement : FeatureConfigEventReceiverArrangement by FeatureConfigEventReceiverArrangementImpl() { + @Test + fun givenEvent_whenCallerIsCancelled_thenShouldStillProcessNormally() = runTest { + val event = TestEvent.userPropertyReadReceiptMode() + + val callerScope = CoroutineScope(Job()) + + val (arrangement, eventProcessor) = Arrangement(this).arrange { + withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) + withUserPropertiesEventReceiverInvoking { + callerScope.cancel() // Cancel during event processing + Either.Right(Unit) + } + } + + callerScope.launch { + eventProcessor.processEvent(event.wrapInEnvelope()) + }.join() + advanceUntilIdle() + assertFalse(callerScope.isActive) + // Then + coVerify { + arrangement.userPropertiesEventReceiver.onEvent(any(), any()) + }.wasInvoked(exactly = once) + coVerify { + arrangement.eventRepository.updateLastProcessedEventId(any()) + }.wasInvoked(exactly = once) + } + + @Test + fun givenEvent_whenProcessingScopeIsCancelledMidwayThrough_thenShouldProceedAnywayAndCancellationIsPropagated() = runTest { + val event = TestEvent.userPropertyReadReceiptMode() + + val processingScope = CoroutineScope(Job()) + + val (arrangement, eventProcessor) = Arrangement(processingScope).arrange { + withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) + withUserPropertiesEventReceiverInvoking { + processingScope.cancel() // Cancel during event processing + Either.Right(Unit) + } + } + + assertFailsWith(CancellationException::class) { + eventProcessor.processEvent(event.wrapInEnvelope()) + advanceUntilIdle() + } + // Then + coVerify { + arrangement.userPropertiesEventReceiver.onEvent(any(), any()) + }.wasInvoked(exactly = once) + coVerify { + arrangement.eventRepository.updateLastProcessedEventId(any()) + }.wasInvoked(exactly = once) + } + + @Test + fun givenEvent_whenProcessingScopeIsAlreadyCancelled_thenShouldNotProcessAndPropagateCancellation() = runTest { + val event = TestEvent.userPropertyReadReceiptMode() + + val processingScope = CoroutineScope(Job()) + processingScope.cancel() + + val (arrangement, eventProcessor) = Arrangement(processingScope).arrange { + withUpdateLastProcessedEventId(event.id, Either.Right(Unit)) + } + + assertFailsWith(CancellationException::class) { + eventProcessor.processEvent(event.wrapInEnvelope()) + advanceUntilIdle() + } + // Then + coVerify { + arrangement.userPropertiesEventReceiver.onEvent(any(), any()) + }.wasNotInvoked() + coVerify { + arrangement.eventRepository.updateLastProcessedEventId(any()) + }.wasNotInvoked() + } + + private class Arrangement( + val processingScope: CoroutineScope + ) : FeatureConfigEventReceiverArrangement by FeatureConfigEventReceiverArrangementImpl() { @Mock val eventRepository = mock(EventRepository::class) @@ -276,6 +368,12 @@ class EventProcessorTest { }.returns(result) } + suspend fun withUserPropertiesEventReceiverInvoking(invocation: () -> Either) = apply { + coEvery { + userPropertiesEventReceiver.onEvent(any(), any()) + }.invokes(invocation) + } + suspend fun withUserPropertiesEventReceiverSucceeding() = withUserPropertiesEventReceiverReturning(Either.Right(Unit)) suspend fun withUserPropertiesEventReceiverFailingWith(failure: CoreFailure) = withUserPropertiesEventReceiverReturning( @@ -295,7 +393,8 @@ class EventProcessorTest { teamEventReceiver, featureConfigEventReceiver, userPropertiesEventReceiver, - federationEventReceiver + federationEventReceiver, + processingScope ) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiverTest.kt index ed9df56c936..f106495aa46 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ConversationEventReceiverTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.sync.receiver.conversation.AccessUpdateEventHandler import com.wire.kalium.logic.sync.receiver.conversation.ConversationMessageTimerEventHandler import com.wire.kalium.logic.sync.receiver.conversation.DeletedConversationEventHandler import com.wire.kalium.logic.sync.receiver.conversation.MLSWelcomeEventHandler @@ -208,17 +209,6 @@ class ConversationEventReceiverTest { result.shouldSucceed() } - @Test - fun givenAccessUpdateEvent_whenOnEventInvoked_thenReturnSuccess() = runTest { - val accessUpdateEvent = TestEvent.newAccessUpdateEvent() - - val (_, featureConfigEventReceiver) = Arrangement().arrange() - - val result = featureConfigEventReceiver.onEvent(accessUpdateEvent, TestEvent.liveDeliveryInfo) - - result.shouldSucceed() - } - @Test fun givenConversationMessageTimerEvent_whenOnEventInvoked_thenPropagateConversationMessageTimerEventHandlerResult() = runTest { @@ -339,6 +329,48 @@ class ConversationEventReceiverTest { result.shouldFail() } + @Test + fun givenAccessUpdateEventAndHandlingSucceeds_whenOnEventInvoked_thenSuccessHandlerResult() = runTest { + // given + val accessUpdateEvent = TestEvent.accessUpdate() + val (arrangement, handler) = Arrangement() + .withConversationAccessUpdateEventSucceeded(Either.Right(Unit)) + .arrange() + + // when + val result = handler.onEvent( + event = accessUpdateEvent, + deliveryInfo = TestEvent.liveDeliveryInfo + ) + + // then + result.shouldSucceed() + coVerify { + arrangement.accessUpdateEventHandler.handle(eq(accessUpdateEvent)) + }.wasInvoked(once) + } + + @Test + fun givenAccessUpdateEventAndHandlingFails_whenOnEventInvoked_thenHandlerPropagateFails() = runTest { + // given + val accessUpdateEvent = TestEvent.accessUpdate() + val (arrangement, handler) = Arrangement() + .withConversationAccessUpdateEventSucceeded(Either.Left(StorageFailure.Generic(RuntimeException("some error")))) + .arrange() + + // when + val result = handler.onEvent( + event = accessUpdateEvent, + deliveryInfo = TestEvent.liveDeliveryInfo + ) + + // then + result.shouldFail() + coVerify { + arrangement.accessUpdateEventHandler.handle(eq(accessUpdateEvent)) + }.wasInvoked(once) + } + private class Arrangement : CodeUpdatedHandlerArrangement by CodeUpdatedHandlerArrangementImpl(), CodeDeletedHandlerArrangement by CodeDeletedHandlerArrangementImpl() { @@ -379,6 +411,9 @@ class ConversationEventReceiverTest { @Mock val protocolUpdateEventHandler = mock(ProtocolUpdateEventHandler::class) + @Mock + val accessUpdateEventHandler = mock(AccessUpdateEventHandler::class) + private val conversationEventReceiver: ConversationEventReceiver = ConversationEventReceiverImpl( newMessageHandler = newMessageEventHandler, newConversationHandler = newConversationEventHandler, @@ -393,7 +428,8 @@ class ConversationEventReceiverTest { codeUpdatedHandler = codeUpdatedHandler, codeDeletedHandler = codeDeletedHandler, typingIndicatorHandler = typingIndicatorHandler, - protocolUpdateEventHandler = protocolUpdateEventHandler + protocolUpdateEventHandler = protocolUpdateEventHandler, + accessUpdateEventHandler = accessUpdateEventHandler ) fun arrange(block: suspend Arrangement.() -> Unit = {}) = run { @@ -425,6 +461,12 @@ class ConversationEventReceiverTest { }.returns(result) } + suspend fun withConversationAccessUpdateEventSucceeded(result: Either) = apply { + coEvery { + accessUpdateEventHandler.handle(any()) + }.returns(result) + } + suspend fun withMLSWelcomeEventSucceeded() = apply { coEvery { mlsWelcomeEventHandler.handle(any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/MessageTextEditHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/MessageTextEditHandlerTest.kt index c3d68d3499e..7479678b7a1 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/MessageTextEditHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/MessageTextEditHandlerTest.kt @@ -118,7 +118,7 @@ class MessageTextEditHandlerTest { @Test fun givenEditIsOlderThanLocalPendingStoredEdit_whenHandling_thenShouldUpdateOnlyMessageIdAndDate() = runTest { val originalContent = TestMessage.TEXT_CONTENT - val originalEditStatus = Message.EditStatus.Edited(Instant.UNIX_FIRST_DATE) + val originalEditStatus = Message.EditStatus.Edited(Instant.DISTANT_FUTURE) // original message date is newer than edit date val originalMessage = ORIGINAL_MESSAGE.copy( editStatus = originalEditStatus, content = originalContent, @@ -127,7 +127,7 @@ class MessageTextEditHandlerTest { ) val editContent = EDIT_CONTENT val editMessage = EDIT_MESSAGE.copy( - date = EDIT_MESSAGE.date - 10.minutes, + date = originalEditStatus.lastEditInstant - 10.minutes, // edit date is older than original message date content = editContent ) val expectedContent = MessageContent.TextEdited( @@ -151,6 +151,41 @@ class MessageTextEditHandlerTest { } } + @Test + fun givenAnAlreadyEditedMessage_whenNewEditIsInTheFuture_thenMessageContentIsUpdatd() = runTest { + val originalContent = TestMessage.TEXT_CONTENT + val originalEditStatus = Message.EditStatus.Edited(Instant.UNIX_FIRST_DATE) + val originalMessage = ORIGINAL_MESSAGE.copy( + editStatus = originalEditStatus, + content = originalContent, + status = Message.Status.Sent + ) + val editContent = EDIT_CONTENT + val editMessage = EDIT_MESSAGE.copy( + date = Instant.UNIX_FIRST_DATE + 10.minutes, + content = editContent + ) + val expectedContent = MessageContent.TextEdited( + editMessageId = editContent.editMessageId, + newContent = editContent.newContent, + newMentions = editContent.newMentions + ) + val (arrangement, messageTextEditHandler) = arrange { + withGetMessageById(Either.Right(originalMessage)) + } + + messageTextEditHandler.handle(editMessage, editContent) + + with(arrangement) { + coVerify { + messageRepository.updateTextMessage(any(), eq(expectedContent), eq(editMessage.id), eq(editMessage.date)) + }.wasInvoked(exactly = once) + coVerify { + messageRepository.updateMessageStatus(any(), any(), any()) + }.wasInvoked(exactly = once) + } + } + private suspend fun arrange(block: suspend Arrangement.() -> Unit) = Arrangement(block).arrange() private class Arrangement( diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ProtocolUpdateEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ProtocolUpdateEventHandlerTest.kt index 6ceeaaafe16..3fcee9b1637 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ProtocolUpdateEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/ProtocolUpdateEventHandlerTest.kt @@ -22,8 +22,8 @@ import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.sync.receiver.conversation.ProtocolUpdateEventHandler import com.wire.kalium.logic.sync.receiver.conversation.ProtocolUpdateEventHandlerImpl -import com.wire.kalium.logic.util.arrangement.CallRepositoryArrangement -import com.wire.kalium.logic.util.arrangement.CallRepositoryArrangementImpl +import com.wire.kalium.logic.util.arrangement.repository.CallRepositoryArrangement +import com.wire.kalium.logic.util.arrangement.repository.CallRepositoryArrangementImpl import com.wire.kalium.logic.util.arrangement.SystemMessageInserterArrangement import com.wire.kalium.logic.util.arrangement.SystemMessageInserterArrangementImpl import com.wire.kalium.logic.util.arrangement.repository.ConversationRepositoryArrangement diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt index 6ba11ddfe57..af93f739d10 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/UserPropertiesEventReceiverTest.kt @@ -19,10 +19,13 @@ package com.wire.kalium.logic.sync.receiver import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.conversation.folders.ConversationFolderRepository import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify import io.mockative.every import io.mockative.mock import io.mockative.once @@ -46,13 +49,31 @@ class UserPropertiesEventReceiverTest { }.wasInvoked(exactly = once) } + @Test + fun givenFoldersUpdateEvent_repositoryIsInvoked() = runTest { + val event = TestEvent.foldersUpdate() + val (arrangement, eventReceiver) = Arrangement() + .withUpdateConversationFolders() + .arrange() + + eventReceiver.onEvent(event, TestEvent.liveDeliveryInfo) + + coVerify { + arrangement.conversationFolderRepository.updateConversationFolders(any()) + }.wasInvoked(exactly = once) + } + private class Arrangement { @Mock val userConfigRepository = mock(UserConfigRepository::class) + @Mock + val conversationFolderRepository = mock(ConversationFolderRepository::class) + private val userPropertiesEventReceiver: UserPropertiesEventReceiver = UserPropertiesEventReceiverImpl( - userConfigRepository = userConfigRepository + userConfigRepository = userConfigRepository, + conversationFolderRepository = conversationFolderRepository ) fun withUpdateReadReceiptsSuccess() = apply { @@ -61,6 +82,12 @@ class UserPropertiesEventReceiverTest { }.returns(Either.Right(Unit)) } + suspend fun withUpdateConversationFolders() = apply { + coEvery { + conversationFolderRepository.updateConversationFolders(any()) + }.returns(Either.Right(Unit)) + } + fun arrange() = this to userPropertiesEventReceiver } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt new file mode 100644 index 00000000000..2b99f10d69b --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/AccessUpdateHandlerTest.kt @@ -0,0 +1,118 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.sync.receiver.conversation + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation.Access +import com.wire.kalium.logic.data.conversation.Conversation.AccessRole +import com.wire.kalium.logic.data.conversation.ConversationMapper +import com.wire.kalium.logic.data.id.PersistenceQualifiedId +import com.wire.kalium.logic.data.id.toDao +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestEvent +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.matches +import io.mockative.mock +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class AccessUpdateHandlerTest { + + @Test + fun givenConversationAccessUpdateEvent_whenHandlingIt_thenShouldCallUpdateDatabase() = runTest { + // given + val event = TestEvent.accessUpdate() + + val (arrangement, eventHandler) = Arrangement() + .withMappingModelToDAOAccess( + event.access, + listOf(ConversationEntity.Access.PRIVATE) + ) + .withMappingModelToDAOAccessRole( + event.accessRole, + listOf(ConversationEntity.AccessRole.TEAM_MEMBER, ConversationEntity.AccessRole.SERVICE) + ) + .arrange() + + // when + eventHandler.handle(event) + + // then + coVerify { + arrangement.conversationDAO.updateAccess( + matches { + it == PersistenceQualifiedId( + value = TestConversation.ID.value, + domain = TestConversation.ID.domain + ) + }, + matches { + it.contains(ConversationEntity.Access.PRIVATE) + }, + matches { + it.contains(ConversationEntity.AccessRole.TEAM_MEMBER) && + it.contains(ConversationEntity.AccessRole.SERVICE) + } + ) + } + } + + private class Arrangement { + + @Mock + val conversationDAO = mock(ConversationDAO::class) + + @Mock + val conversationMapper = mock(ConversationMapper::class) + + init { + runBlocking { + coEvery { conversationDAO.updateAccess(any(), any(), any()) }.returns(Unit) + } + } + + private val accessUpdateEventHandler: AccessUpdateEventHandler = AccessUpdateEventHandler( + selfUserId = TestUser.USER_ID, + conversationDAO = conversationDAO, + conversationMapper = conversationMapper + ) + + fun withMappingModelToDAOAccess(param: Set, result: List) = apply { + every { + conversationMapper.fromModelToDAOAccess(param) + }.returns(result) + } + + fun withMappingModelToDAOAccessRole(param: Set, result: List) = apply { + every { + conversationMapper.fromModelToDAOAccessRole(param) + }.returns(result) + } + + fun arrange() = this to accessUpdateEventHandler + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt index 50009a3f18c..cccfce2047d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/NewMessageEventHandlerTest.kt @@ -35,9 +35,9 @@ import com.wire.kalium.logic.feature.message.ephemeral.EphemeralMessageDeletionH import com.wire.kalium.logic.framework.TestEvent import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.functional.isRight import com.wire.kalium.logic.sync.receiver.handler.legalhold.LegalHoldHandler import com.wire.kalium.util.DateTimeUtil -import com.wire.kalium.util.DateTimeUtil.toIsoDateTimeString import io.mockative.Mock import io.mockative.any import io.mockative.coEvery @@ -48,7 +48,6 @@ import io.mockative.once import io.mockative.verify import kotlinx.coroutines.test.runTest import kotlinx.datetime.Instant -import kotlinx.datetime.toInstant import kotlin.test.Test class NewMessageEventHandlerTest { @@ -56,7 +55,7 @@ class NewMessageEventHandlerTest { @Test fun givenProteusEvent_whenHandling_shouldAskProteusUnpackerToDecrypt() = runTest { val (arrangement, newMessageEventHandler) = Arrangement() - .withProteusUnpackerReturning(Either.Right(MessageUnpackResult.HandshakeMessage)) + .withProteusUnpackerReturning(Either.Left(CoreFailure.InvalidEventSenderID)) .arrange() val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") @@ -64,7 +63,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) } @@ -76,7 +75,8 @@ class NewMessageEventHandlerTest { ProteusFailure( ProteusException( message = null, - code = ProteusException.Code.DUPLICATE_MESSAGE + code = ProteusException.Code.DUPLICATE_MESSAGE, + intCode = 7 ) ) ) @@ -88,7 +88,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -104,7 +104,8 @@ class NewMessageEventHandlerTest { ProteusFailure( ProteusException( message = null, - code = ProteusException.Code.INVALID_SIGNATURE + code = ProteusException.Code.INVALID_SIGNATURE, + intCode = 5 ) ) ) @@ -116,7 +117,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -210,7 +211,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -234,7 +235,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -255,7 +256,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasNotInvoked() } @@ -270,7 +271,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasNotInvoked() } @@ -284,7 +285,7 @@ class NewMessageEventHandlerTest { val newMessageEvent = TestEvent.newMessageEvent("encryptedContent") newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) - coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) }.wasInvoked(exactly = once) + coVerify { arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { arrangement.applicationMessageHandler.handleDecryptionError(any(), any(), any(), any(), any(), any()) }.wasNotInvoked() coVerify { arrangement.confirmationDeliveryHandler.enqueueConfirmationDelivery(any(), any()) }.wasInvoked(exactly = once) } @@ -323,7 +324,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -343,7 +344,7 @@ class NewMessageEventHandlerTest { newMessageEventHandler.handleNewProteusMessage(newMessageEvent, TestEvent.liveDeliveryInfo) coVerify { - arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent)) + arrangement.proteusMessageUnpacker.unpackProteusMessage(eq(newMessageEvent), any()) }.wasInvoked(exactly = once) coVerify { @@ -433,10 +434,17 @@ class NewMessageEventHandlerTest { staleEpochVerifier ) - suspend fun withProteusUnpackerReturning(result: Either) = apply { + suspend fun withProteusUnpackerReturning(result: Either) = apply { coEvery { - proteusMessageUnpacker.unpackProteusMessage(any()) - }.returns(result) + proteusMessageUnpacker.unpackProteusMessage(any(), any()) + }.invokes { args -> + if (result is Either.Right) { + val lambda = args[1] as suspend (MessageUnpackResult.ApplicationMessage) -> MessageUnpackResult.ApplicationMessage + Either.Right(lambda(result.value)) + } else { + result + } + } } suspend fun withHandleLegalHoldSuccess() = apply { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt index a00d99ac133..f5e3df2aa25 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ProteusMessageUnpackerTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.cryptography.ProteusClient import com.wire.kalium.cryptography.utils.PlainData import com.wire.kalium.cryptography.utils.encryptDataWithAES256 import com.wire.kalium.cryptography.utils.generateRandomAES256Key +import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.client.ProteusClientProvider import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.message.MessageContent @@ -76,7 +77,7 @@ class ProteusMessageUnpackerTest { val encodedEncryptedContent = Base64.encodeToBase64("Hello".encodeToByteArray()) val messageEvent = TestEvent.newMessageEvent(encodedEncryptedContent.decodeToString()) - proteusUnpacker.unpackProteusMessage(messageEvent) + proteusUnpacker.unpackProteusMessage(messageEvent) { } val cryptoSessionId = CryptoSessionId( CryptoUserID(messageEvent.senderUserId.value, messageEvent.senderUserId.domain), @@ -85,7 +86,7 @@ class ProteusMessageUnpackerTest { val decodedByteArray = Base64.decodeFromBase64(messageEvent.content.toByteArray()) coVerify { - arrangement.proteusClient.decrypt(matches { it.contentEquals(decodedByteArray) }, eq(cryptoSessionId)) + arrangement.proteusClient.decrypt(matches { it.contentEquals(decodedByteArray) }, eq(cryptoSessionId), any()) }.wasInvoked(exactly = once) } @@ -130,7 +131,7 @@ class ProteusMessageUnpackerTest { encryptedExternalContent = encryptedProtobufExternalContent ) - val result = proteusUnpacker.unpackProteusMessage(messageEvent) + val result = proteusUnpacker.unpackProteusMessage(messageEvent) { it } result.shouldSucceed { assertIs(it) @@ -152,8 +153,11 @@ class ProteusMessageUnpackerTest { suspend fun withProteusClientDecryptingByteArray(decryptedData: ByteArray) = apply { coEvery { - proteusClient.decrypt(any(), any()) - }.returns(decryptedData) + proteusClient.decrypt>(any(), any(), any()) + }.invokes { args -> + val lambda = args[2] as suspend (ByteArray) -> Either<*, *> + lambda.invoke(decryptedData) + } } fun withProtoContentMapperReturning(plainBlobMatcher: Matcher, protoContent: ProtoContent) = apply { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt index f9dbe4f7ee5..facf5ddb70b 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/sync/slow/SlowSyncWorkerTest.kt @@ -24,6 +24,7 @@ import com.wire.kalium.logic.data.sync.SlowSyncStep import com.wire.kalium.logic.data.user.LegalHoldStatus import com.wire.kalium.logic.feature.connection.SyncConnectionsUseCase import com.wire.kalium.logic.feature.conversation.SyncConversationsUseCase +import com.wire.kalium.logic.feature.conversation.folder.SyncConversationFoldersUseCase import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolver import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.legalhold.FetchLegalHoldForSelfUserFromRemoteUseCase @@ -71,6 +72,7 @@ class SlowSyncWorkerTest { .withJoinMLSConversationsSuccess() .withResolveOneOnOneConversationsSuccess() .withFetchLegalHoldStatusSuccess() + .withSyncFoldersSuccess() .arrange() worker.slowSyncStepsFlow(successfullyMigration).collect() @@ -408,6 +410,7 @@ class SlowSyncWorkerTest { .withJoinMLSConversationsSuccess() .withResolveOneOnOneConversationsSuccess() .withFetchLegalHoldStatusSuccess() + .withSyncFoldersSuccess() .arrange() slowSyncWorker.slowSyncStepsFlow(successfullyMigration).collect() @@ -511,6 +514,9 @@ class SlowSyncWorkerTest { @Mock val fetchLegalHoldForSelfUserFromRemoteUseCase = mock(FetchLegalHoldForSelfUserFromRemoteUseCase::class) + @Mock + val syncConversationFoldersUseCase = mock(SyncConversationFoldersUseCase::class) + init { runBlocking { withLastProcessedEventIdReturning(Either.Right("lastProcessedEventId")) @@ -529,6 +535,7 @@ class SlowSyncWorkerTest { updateSupportedProtocols = updateSupportedProtocols, fetchLegalHoldForSelfUserFromRemoteUseCase = fetchLegalHoldForSelfUserFromRemoteUseCase, oneOnOneResolver = oneOnOneResolver, + syncConversationFolders = syncConversationFoldersUseCase ) suspend fun withSyncSelfUserFailure() = apply { @@ -644,6 +651,12 @@ class SlowSyncWorkerTest { oneOnOneResolver.resolveAllOneOnOneConversations(any()) }.returns(success) } + + suspend fun withSyncFoldersSuccess() = apply { + coEvery { + syncConversationFoldersUseCase.invoke() + }.returns(success) + } } private companion object { diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/CallRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/CallRepositoryArrangement.kt deleted file mode 100644 index a16ca78289d..00000000000 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/CallRepositoryArrangement.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ -package com.wire.kalium.logic.util.arrangement - -import com.wire.kalium.logic.data.call.Call -import com.wire.kalium.logic.data.call.CallRepository -import com.wire.kalium.logic.data.call.CallStatus -import com.wire.kalium.logic.data.conversation.Conversation -import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.data.user.UserId -import io.mockative.Mock -import io.mockative.coEvery -import io.mockative.mock -import kotlinx.coroutines.flow.flowOf - -interface CallRepositoryArrangement { - val callRepository: CallRepository - suspend fun withEstablishedCall() - suspend fun withoutAnyEstablishedCall() -} - -internal class CallRepositoryArrangementImpl : CallRepositoryArrangement { - - @Mock - override val callRepository = mock(CallRepository::class) - - override suspend fun withEstablishedCall() { - coEvery { - callRepository.establishedCallsFlow() - }.returns(flowOf(listOf(call))) - } - - override suspend fun withoutAnyEstablishedCall() { - coEvery { - callRepository.establishedCallsFlow() - }.returns(flowOf(listOf())) - } - - companion object { - val call = Call( - conversationId = ConversationId("conversationId", "domain"), - status = CallStatus.ESTABLISHED, - callerId = UserId("caller", "domain").toString(), - participants = listOf(), - isMuted = true, - isCameraOn = false, - isCbrEnabled = false, - maxParticipants = 0, - conversationName = "ONE_ON_ONE Name", - conversationType = Conversation.Type.ONE_ON_ONE, - callerName = "otherUsername", - callerTeamName = "team_1" - ) - } -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionRequestsJson.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallManagerArrangement.kt similarity index 51% rename from mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionRequestsJson.kt rename to logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallManagerArrangement.kt index 03049d8c486..8c2e10386a2 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionRequestsJson.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallManagerArrangement.kt @@ -15,35 +15,26 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ +package com.wire.kalium.logic.util.arrangement.repository -package com.wire.kalium.mocks.responses.connection +import com.wire.kalium.logic.feature.call.CallManager +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.mock -import com.wire.kalium.mocks.responses.ValidJsonProvider +interface CallManagerArrangement { -object ConnectionRequestsJson { + val callManager: CallManager - val validEmptyBody = ValidJsonProvider(String) { - """ - { - "size":500 - } - """.trimIndent() - } + suspend fun withEndCall() +} - val validPagingState = ValidJsonProvider("PAGING_STATE_1234") { - """ - { - "paging_state": "$it", - "size":500 - } - """.trimIndent() - } +internal class CallManagerArrangementImpl : CallManagerArrangement { + @Mock + override val callManager = mock(CallManager::class) - val validConnectionStatusUpdate = ValidJsonProvider("accepted") { - """ - { - "status": "$it" - } - """.trimIndent() + override suspend fun withEndCall() { + coEvery { callManager.endCall(any()) }.returns(Unit) } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallRepositoryArrangement.kt index a57e0d95533..5827fdd6153 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/CallRepositoryArrangement.kt @@ -17,17 +17,30 @@ */ package com.wire.kalium.logic.util.arrangement.repository -import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestCall import io.mockative.Mock +import io.mockative.any import io.mockative.coEvery +import io.mockative.doesNothing +import io.mockative.every import io.mockative.mock +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf internal interface CallRepositoryArrangement { val callRepository: CallRepository suspend fun withEstablishedCallsFlow(calls: List) + suspend fun withEstablishedCall() + suspend fun withoutAnyEstablishedCall() + suspend fun withCallsFlow(flow: Flow>) + suspend fun withUpdateIsCameraOnById(conversationId: ConversationId = any(), isCameraOn: Boolean = any()) } internal open class CallRepositoryArrangementImpl : CallRepositoryArrangement { @@ -40,4 +53,41 @@ internal open class CallRepositoryArrangementImpl : CallRepositoryArrangement { callRepository.establishedCallsFlow() }.returns(flowOf(calls)) } + + override suspend fun withEstablishedCall() { + coEvery { + callRepository.establishedCallsFlow() + }.returns(flowOf(listOf(CallRepositoryArrangementImpl.call))) + } + + override suspend fun withoutAnyEstablishedCall() { + coEvery { + callRepository.establishedCallsFlow() + }.returns(flowOf(listOf())) + } + + override suspend fun withCallsFlow(flow: Flow>) { + coEvery { callRepository.callsFlow() }.returns(flow) + } + + override suspend fun withUpdateIsCameraOnById(conversationId: ConversationId, isCameraOn: Boolean) { + every { callRepository.updateIsCameraOnById(conversationId, isCameraOn) }.doesNothing() + } + + companion object { + val call = Call( + conversationId = ConversationId("conversationId", "domain"), + status = CallStatus.ESTABLISHED, + callerId = TestCall.CALLER_ID, + participants = listOf(), + isMuted = true, + isCameraOn = false, + isCbrEnabled = false, + maxParticipants = 0, + conversationName = "ONE_ON_ONE Name", + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = "otherUsername", + callerTeamName = "team_1" + ) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt index ece6d0c10c9..1a223b651d9 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/ConversationRepositoryArrangement.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationDetails import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.mls.EpochChangesData +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.UserId @@ -109,6 +110,7 @@ internal interface ConversationRepositoryArrangement { suspend fun withSelectGroupStatusMembersNamesAndHandles(result: Either) suspend fun withConversationDetailsByIdReturning(result: Either) suspend fun withPersistMembers(result: Either) + suspend fun withMembersNameAndHandle(result: Either>) } internal open class ConversationRepositoryArrangementImpl : ConversationRepositoryArrangement { @@ -266,4 +268,8 @@ internal open class ConversationRepositoryArrangementImpl : ConversationReposito override suspend fun withPersistMembers(result: Either) { coEvery { conversationRepository.persistMembers(any(), any()) }.returns(result) } + + override suspend fun withMembersNameAndHandle(result: Either>) { + coEvery { conversationRepository.selectMembersNameAndHandle(any()) }.returns(result) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/MessageRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/MessageRepositoryArrangement.kt index aa947062e5b..5266ec213ac 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/MessageRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/MessageRepositoryArrangement.kt @@ -22,6 +22,7 @@ import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.MessageRepository +import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.notification.LocalNotification import com.wire.kalium.logic.functional.Either import io.mockative.Mock @@ -37,6 +38,9 @@ internal interface MessageRepositoryArrangement { @Mock val messageRepository: MessageRepository + @Mock + val systemMessageInserter: SystemMessageInserter + suspend fun withGetMessageById( result: Either, messageID: Matcher = AnyMatcher(valueOf()), @@ -68,6 +72,8 @@ internal open class MessageRepositoryArrangementImpl : MessageRepositoryArrangem @Mock override val messageRepository: MessageRepository = mock(MessageRepository::class) + override val systemMessageInserter = mock(SystemMessageInserter::class) + override suspend fun withGetMessageById( result: Either, messageID: Matcher, diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt index 3a5258a0adb..501f9521a5c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserConfigRepositoryArrangement.kt @@ -48,6 +48,8 @@ internal interface UserConfigRepositoryArrangement { suspend fun withGetPreviousTrackingIdentifier(result: String?) suspend fun withObserveTrackingIdentifier(result: Either) suspend fun withDeletePreviousTrackingIdentifier() + suspend fun withUpdateNextTimeForCallFeedback() + suspend fun withGetNextTimeForCallFeedback(result: Either) } internal class UserConfigRepositoryArrangementImpl : UserConfigRepositoryArrangement { @@ -125,4 +127,12 @@ internal class UserConfigRepositoryArrangementImpl : UserConfigRepositoryArrange override suspend fun withDeletePreviousTrackingIdentifier() { coEvery { userConfigRepository.deletePreviousTrackingIdentifier() }.returns(Unit) } + + override suspend fun withGetNextTimeForCallFeedback(result: Either) { + coEvery { userConfigRepository.getNextTimeForCallFeedback() }.returns(result) + } + + override suspend fun withUpdateNextTimeForCallFeedback() { + coEvery { userConfigRepository.updateNextTimeForCallFeedback(any()) }.returns(Unit) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt index e0bbb8c0511..25fecc6da7c 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/repository/UserRepositoryArrangement.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.util.arrangement.repository import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.mls.NameAndHandle import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.QualifiedID import com.wire.kalium.logic.data.user.OtherUser @@ -31,7 +32,6 @@ import com.wire.kalium.logic.functional.Either import io.mockative.Mock import io.mockative.any import io.mockative.coEvery -import io.mockative.eq import io.mockative.fake.valueOf import io.mockative.matchers.AnyMatcher import io.mockative.matchers.Matcher @@ -93,6 +93,8 @@ internal interface UserRepositoryArrangement { userId: Matcher = AnyMatcher(valueOf()), conversationId: Matcher = AnyMatcher(valueOf()) ) + + suspend fun withNameAndHandle(result: Either, userId: Matcher = AnyMatcher(valueOf())) } @Suppress("INAPPLICABLE_JVM_NAME") @@ -227,4 +229,8 @@ internal open class UserRepositoryArrangementImpl : UserRepositoryArrangement { } .returns(result) } + + override suspend fun withNameAndHandle(result: Either, userId: Matcher) { + coEvery { userRepository.getNameAndHandle(matches { userId.matches(it) }) }.returns(result) + } } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/JoinExistingMLSConversationUseCaseArrangement.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/JoinExistingMLSConversationUseCaseArrangement.kt index 0010718bf6f..95ced3aa0e6 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/JoinExistingMLSConversationUseCaseArrangement.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/util/arrangement/usecase/JoinExistingMLSConversationUseCaseArrangement.kt @@ -39,7 +39,7 @@ internal class JoinExistingMLSConversationUseCaseArrangementImpl : JoinExistingM override suspend fun withJoinExistingMLSConversationUseCaseReturning(result: Either) { coEvery { - joinExistingMLSConversationUseCase.invoke(any()) + joinExistingMLSConversationUseCase.invoke(any(), any()) }.returns(result) } } diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt new file mode 100644 index 00000000000..8d4c8caa2f5 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/data/client/ProteusClientProviderTest.kt @@ -0,0 +1,73 @@ +package com.wire.kalium.logic.data.client + +import com.wire.kalium.cryptography.exceptions.ProteusStorageMigrationException +import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.persistence.dbPassphrase.PassphraseStorage +import com.wire.kalium.util.FileUtil +import com.wire.kalium.util.KaliumDispatcherImpl +import io.mockative.Mock +import io.mockative.any +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.nio.file.Paths +import kotlin.io.path.createDirectory +import kotlin.io.path.createFile +import kotlin.io.path.exists + +class ProteusClientProviderTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusClientProvider) = Arrangement() + .withCorruptedProteusStorage() + .arrange() + + // when - then + try { + proteusClientProvider.getOrCreate() + } catch (e: ProteusStorageMigrationException) { + coVerify { arrangement.proteusMigrationRecoveryHandler.clearClientData(any()) }.wasInvoked(once) + } + } + + private class Arrangement { + + @Mock + val passphraseStorage = mock(PassphraseStorage::class) + + @Mock + val proteusMigrationRecoveryHandler = mock(ProteusMigrationRecoveryHandler::class) + + init { + every { passphraseStorage.getPassphrase(any()) }.returns("passphrase") + } + + /** + * Corrupted because it's just an empty file called "prekeys". + * But nothing to migrate, this is just to test that we are calling recovery. + */ + fun withCorruptedProteusStorage() = apply { + val rootProteusPath = Paths.get("/tmp/rootProteusPath") + if (rootProteusPath.exists()) { + FileUtil.deleteDirectory(rootProteusPath.toString()) + } + rootProteusPath.createDirectory() + rootProteusPath.resolve("prekeys").createFile() + } + + fun arrange() = this to ProteusClientProviderImpl( + rootProteusPath = "/tmp/rootProteusPath", + userId = TestUser.USER_ID, + passphraseStorage = passphraseStorage, + kaliumConfigs = KaliumConfigs(encryptProteusStorage = true), + dispatcher = KaliumDispatcherImpl, + proteusMigrationRecoveryHandler = proteusMigrationRecoveryHandler + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt index 9bff5d9d1f9..56fb0aec4e2 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnCloseCallTest.kt @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature.call.scenario import com.wire.kalium.calling.CallClosedReason import com.wire.kalium.calling.types.Uint32_t +import com.wire.kalium.logic.data.call.CallHelper import com.wire.kalium.logic.data.call.CallMetadata import com.wire.kalium.logic.data.call.CallMetadataProfile import com.wire.kalium.logic.data.call.CallRepository @@ -28,7 +29,7 @@ import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.GroupID import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl import com.wire.kalium.logic.data.mls.CipherSuite -import com.wire.kalium.logic.feature.call.scenario.OnCloseCall +import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.logic.framework.TestUser import com.wire.kalium.network.NetworkState import com.wire.kalium.network.NetworkStateObserver @@ -81,197 +82,261 @@ class OnCloseCallTest { } @Test - fun givenCloseReasonIsCanceled_whenOnCloseCallBackHappens_thenPersistMissedCallAndUpdateStatus() = testScope.runTest { - val reason = CallClosedReason.CANCELLED.avsValue - - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + fun givenCloseReasonIsCanceled_whenOnCloseCallBackHappens_thenPersistMissedCallAndUpdateStatus() = + testScope.runTest { + val reason = CallClosedReason.CANCELLED.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasInvoked(once) + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasInvoked(once) - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.MISSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.MISSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenCloseReasonIsRejected_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = testScope.runTest { - val reason = CallClosedReason.REJECTED.avsValue - - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + fun givenCloseReasonIsRejected_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = + testScope.runTest { + val reason = CallClosedReason.REJECTED.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasNotInvoked() + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasNotInvoked() - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.REJECTED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.REJECTED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenCloseReasonIsEndedNormally_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = testScope.runTest { - - val reason = CallClosedReason.NORMAL.avsValue - - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + fun givenCloseReasonIsEndedNormally_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = + testScope.runTest { + + val reason = CallClosedReason.NORMAL.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasNotInvoked() + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasNotInvoked() - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenAnIncomingGroupCall_whenOnCloseCallBackHappens_thenPersistMissedCallAndUpdateStatus() = testScope.runTest { - val incomingCall = callMetadata.copy( - callStatus = CallStatus.INCOMING, - conversationType = Conversation.Type.GROUP - ) + fun givenAnIncomingGroupCall_whenOnCloseCallBackHappens_thenPersistMissedCallAndUpdateStatus() = + testScope.runTest { + val incomingCall = callMetadata.copy( + callStatus = CallStatus.INCOMING, + conversationType = Conversation.Type.GROUP + ) - every { - callRepository.getCallMetadataProfile() - }.returns(CallMetadataProfile(mapOf(conversationId to incomingCall))) + every { + callRepository.getCallMetadataProfile() + }.returns(CallMetadataProfile(mapOf(conversationId to incomingCall))) - val reason = CallClosedReason.NORMAL.avsValue + val reason = CallClosedReason.NORMAL.avsValue - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasInvoked(once) + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasInvoked(once) - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenCurrentCallClosedInternally_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = testScope.runTest { - val closedInternallyCall = callMetadata.copy( - callStatus = CallStatus.CLOSED_INTERNALLY, - conversationType = Conversation.Type.GROUP - ) + fun givenCurrentCallClosedInternally_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = + testScope.runTest { + val closedInternallyCall = callMetadata.copy( + callStatus = CallStatus.CLOSED_INTERNALLY, + conversationType = Conversation.Type.GROUP + ) - every { - callRepository.getCallMetadataProfile() - }.returns(CallMetadataProfile(mapOf(conversationId to closedInternallyCall))) + every { + callRepository.getCallMetadataProfile() + }.returns(CallMetadataProfile(mapOf(conversationId to closedInternallyCall))) - val reason = CallClosedReason.NORMAL.avsValue + val reason = CallClosedReason.NORMAL.avsValue - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasNotInvoked() + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasNotInvoked() - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenCurrentCallIsEstablished_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = testScope.runTest { - val establishedCall = callMetadata.copy( - callStatus = CallStatus.ESTABLISHED, - establishedTime = "time", - conversationType = Conversation.Type.GROUP - ) - - every { - callRepository.getCallMetadataProfile() - }.returns(CallMetadataProfile(mapOf(conversationId to establishedCall))) - val reason = CallClosedReason.NORMAL.avsValue + fun givenCurrentCallIsEstablished_whenOnCloseCallBackHappens_thenDoNotPersistMissedCallAndUpdateStatus() = + testScope.runTest { + val establishedCall = callMetadata.copy( + callStatus = CallStatus.ESTABLISHED, + establishedTime = "time", + conversationType = Conversation.Type.GROUP + ) - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + every { + callRepository.getCallMetadataProfile() + }.returns(CallMetadataProfile(mapOf(conversationId to establishedCall))) + val reason = CallClosedReason.NORMAL.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(eq(conversationId)) - }.wasNotInvoked() + coVerify { + callRepository.persistMissedCall(eq(conversationId)) + }.wasNotInvoked() - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasNotInvoked() - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasNotInvoked() + } @Test - fun givenMLSCallEndedNormally_whenOnCloseCallBackHappens_thenLeaveMlsConference() = testScope.runTest { - val mlsCall = callMetadata.copy( - protocol = Conversation.ProtocolInfo.MLS( - groupId = GroupID(""), - groupState = Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, - epoch = ULong.MAX_VALUE, - keyingMaterialLastUpdate = Instant.DISTANT_FUTURE, - cipherSuite = CipherSuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + fun givenMLSCallEndedNormally_whenOnCloseCallBackHappens_thenLeaveMlsConference() = + testScope.runTest { + val mlsCall = callMetadata.copy( + protocol = Conversation.ProtocolInfo.MLS( + groupId = GroupID(""), + groupState = Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + epoch = ULong.MAX_VALUE, + keyingMaterialLastUpdate = Instant.DISTANT_FUTURE, + cipherSuite = CipherSuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256 + ) ) - ) - every { - callRepository.getCallMetadataProfile() - }.returns(CallMetadataProfile(mapOf(conversationId to mlsCall))) - val reason = CallClosedReason.NORMAL.avsValue - - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + every { + callRepository.getCallMetadataProfile() + }.returns(CallMetadataProfile(mapOf(conversationId to mlsCall))) + val reason = CallClosedReason.NORMAL.avsValue + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) - }.wasInvoked(once) + coVerify { + callRepository.updateCallStatusById(eq(conversationId), eq(CallStatus.CLOSED)) + }.wasInvoked(once) - coVerify { - callRepository.leaveMlsConference(eq(conversationId)) - }.wasInvoked(once) - } + coVerify { + callRepository.leaveMlsConference(eq(conversationId)) + }.wasInvoked(once) + } @Test - fun givenDeviceOffline_whenOnCloseCallBackHappens_thenDoNotPersistMissedCall() = testScope.runTest { - val reason = CallClosedReason.CANCELLED.avsValue - - every { - networkStateObserver.observeNetworkState() - }.returns(MutableStateFlow(NetworkState.NotConnected)) - - onCloseCall.onClosedCall(reason, conversationIdString, time, userIdString, clientId, null) - yield() + fun givenDeviceOffline_whenOnCloseCallBackHappens_thenDoNotPersistMissedCall() = + testScope.runTest { + val reason = CallClosedReason.CANCELLED.avsValue + + every { + networkStateObserver.observeNetworkState() + }.returns(MutableStateFlow(NetworkState.NotConnected)) + + onCloseCall.onClosedCall( + reason, + conversationIdString, + time, + userIdString, + clientId, + null + ) + yield() - coVerify { - callRepository.persistMissedCall(conversationId) + coVerify { + callRepository.persistMissedCall(conversationId) + }.wasNotInvoked() } - } companion object { private val conversationId = ConversationId("conversationId", "wire.com") @@ -280,6 +345,7 @@ class OnCloseCallTest { private const val clientId = "clientId" private val callMetadata = CallMetadata( + callerId = TestCall.CALLER_ID, isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnMuteStateForSelfUserChangedTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnMuteStateForSelfUserChangedTest.kt index fd5d87984f8..98f4c92b9fb 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnMuteStateForSelfUserChangedTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnMuteStateForSelfUserChangedTest.kt @@ -23,6 +23,7 @@ import com.wire.kalium.logic.data.call.CallStatus import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.feature.call.scenario.OnMuteStateForSelfUserChanged +import com.wire.kalium.logic.framework.TestCall import com.wire.kalium.logic.test_util.TestKaliumDispatcher import io.mockative.Mock import io.mockative.any @@ -98,7 +99,7 @@ class OnMuteStateForSelfUserChangedTest { private val call = Call( conversationId = conversationId, status = CallStatus.ESTABLISHED, - callerId = "called-id", + callerId = TestCall.CALLER_ID, isMuted = false, isCameraOn = false, isCbrEnabled = false, diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChangedTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChangedTest.kt new file mode 100644 index 00000000000..f0750a73493 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/call/scenario/OnParticipantListChangedTest.kt @@ -0,0 +1,246 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.logic.feature.call.scenario + +import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.configuration.UserConfigRepository +import com.wire.kalium.logic.data.call.Call +import com.wire.kalium.logic.data.call.CallRepository +import com.wire.kalium.logic.data.call.CallStatus +import com.wire.kalium.logic.data.call.CallHelper +import com.wire.kalium.logic.data.call.ParticipantMinimized +import com.wire.kalium.logic.data.call.mapper.ParticipantMapper +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.GroupID +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.id.QualifiedIdMapperImpl +import com.wire.kalium.logic.data.mls.CipherSuite +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import io.mockative.Mock +import io.mockative.any +import io.mockative.coEvery +import io.mockative.coVerify +import io.mockative.every +import io.mockative.mock +import io.mockative.once +import io.mockative.twice +import io.mockative.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.yield +import kotlinx.datetime.Clock +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +class OnParticipantListChangedTest { + + @BeforeTest + fun setup() { + testScope = TestScope() + } + + @Test + fun givenCallRepository_whenParticipantListChangedCallBackHappens_thenUpdateCallParticipantsOnce() = + testScope.runTest { + val (arrangement, onParticipantListChanged) = Arrangement() + .withParticipantMapper() + .withUserConfigRepositoryReturning(Either.Left(StorageFailure.DataNotFound)) + .arrange() + + onParticipantListChanged.onParticipantChanged( + REMOTE_CONVERSATION_ID, data, null + ) + yield() + + verify { + arrangement.participantMapper.fromCallMemberToParticipantMinimized(any()) + }.wasInvoked(exactly = twice) + + coVerify { + arrangement.callRepository.updateCallParticipants(any(), any()) + }.wasInvoked(exactly = once) + } + + @Test + fun givenMlsCallHelperReturnsTrue_whenParticipantListChangedCallBackHappens_thenEndCall() = + testScope.runTest { + val (arrangement, onParticipantListChanged) = Arrangement() + .withParticipantMapper() + .withUserConfigRepositoryReturning(Either.Right(true)) + .withProtocol() + .withEstablishedCall() + .withShouldEndSFTOneOnOneCall(true) + .arrange() + + onParticipantListChanged.onParticipantChanged( + REMOTE_CONVERSATION_ID, data, null + ) + advanceUntilIdle() + yield() + + assertTrue { arrangement.isEndCallInvoked } + } + + + @Test + fun givenMlsCallHelperReturnsFalse_whenParticipantListChangedCallBackHappens_thenDoNotEndCall() = + testScope.runTest { + val (arrangement, onParticipantListChanged) = Arrangement() + .withParticipantMapper() + .withUserConfigRepositoryReturning(Either.Right(true)) + .withProtocol() + .withEstablishedCall() + .withShouldEndSFTOneOnOneCall(false) + .arrange() + + onParticipantListChanged.onParticipantChanged( + REMOTE_CONVERSATION_ID, data, null + ) + yield() + + assertFalse { arrangement.isEndCallInvoked } + } + + + internal class Arrangement { + + @Mock + val callRepository = mock(CallRepository::class) + + @Mock + val participantMapper = mock(ParticipantMapper::class) + + @Mock + val userConfigRepository = mock(UserConfigRepository::class) + + @Mock + val callHelper = mock(CallHelper::class) + + var isEndCallInvoked = false + + private val qualifiedIdMapper = QualifiedIdMapperImpl(TestUser.SELF.id) + + fun arrange() = this to OnParticipantListChanged( + callRepository = callRepository, + participantMapper = participantMapper, + userConfigRepository = userConfigRepository, + callHelper = callHelper, + qualifiedIdMapper = qualifiedIdMapper, + endCall = { + isEndCallInvoked = true + }, + callingScope = testScope, + ) + + fun withUserConfigRepositoryReturning(result: Either) = apply { + every { + userConfigRepository.shouldUseSFTForOneOnOneCalls() + }.returns(result) + } + + fun withParticipantMapper() = apply { + every { + participantMapper.fromCallMemberToParticipantMinimized(any()) + }.returns(participant) + } + + fun withProtocol() = apply { + every { + callRepository.currentCallProtocol(any()) + }.returns(mlsProtocolInfo) + } + + suspend fun withEstablishedCall() = apply { + coEvery { + callRepository.establishedCallsFlow() + }.returns(flowOf(listOf(call))) + } + + fun withShouldEndSFTOneOnOneCall(result: Boolean) = apply { + every { + callHelper.shouldEndSFTOneOnOneCall(any(), any(), any(), any(), any()) + }.returns(result) + } + } + + companion object { + lateinit var testScope: TestScope + private const val REMOTE_CONVERSATION_ID = "c9mGRDNE7YRVRbk6jokwXNXPgU1n37iS" + private val data = """ + { + "convid": "c9mGRDNE7YRVRbk6jokwXNXPgU1n37iS", + "members": [ + { + "userid": "userid1", + "clientid": "clientid1", + "aestab": "1", + "vrecv": "1", + "muted": "0" + }, + { + "userid": "userid2", + "clientid": "clientid2", + "aestab": "1", + "vrecv": "1", + "muted": "0" + } + ] + } + """ + val participant = ParticipantMinimized( + id = QualifiedID("userid1", "domain"), + userId = QualifiedID("participantId", "participantDomain"), + clientId = "abcd", + isMuted = true, + isCameraOn = false, + isSharingScreen = false, + hasEstablishedAudio = true + ) + val mlsProtocolInfo = Conversation.ProtocolInfo.MLS( + GroupID("groupid"), + Conversation.ProtocolInfo.MLSCapable.GroupState.ESTABLISHED, + 1UL, + Clock.System.now(), + CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + private val conversationId = ConversationId("conversationId", "domainId") + + private val call = Call( + conversationId = conversationId, + status = CallStatus.ESTABLISHED, + callerId = UserId("called-id", "domain"), + isMuted = false, + isCameraOn = false, + isCbrEnabled = false, + conversationName = null, + conversationType = Conversation.Type.ONE_ON_ONE, + callerName = null, + callerTeamName = null, + establishedTime = null + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt new file mode 100644 index 00000000000..7a52607adb6 --- /dev/null +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/feature/client/ProteusMigrationRecoveryHandlerTest.kt @@ -0,0 +1,36 @@ +package com.wire.kalium.logic.feature.client + +import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.feature.auth.LogoutUseCase +import io.mockative.Mock +import io.mockative.coVerify +import io.mockative.mock +import io.mockative.once +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ProteusMigrationRecoveryHandlerTest { + + @Test + fun givenGettingOrCreatingAProteusClient_whenMigrationPerformedAndFails_thenCatchErrorAndStartRecovery() = runTest { + // given + val (arrangement, proteusMigrationRecoveryHandler) = Arrangement().arrange() + + // when + val clearLocalFiles: suspend () -> Unit = { } + proteusMigrationRecoveryHandler.clearClientData(clearLocalFiles) + + // then + coVerify { arrangement.logoutUseCase(LogoutReason.MIGRATION_TO_CC_FAILED, true) }.wasInvoked(once) + } + + private class Arrangement { + + @Mock + val logoutUseCase = mock(LogoutUseCase::class) + + fun arrange() = this to ProteusMigrationRecoveryHandlerImpl( + lazy { logoutUseCase } + ) + } +} diff --git a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandlerTest.kt b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandlerTest.kt index e68281d90de..dbe4a760f23 100644 --- a/logic/src/jvmTest/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandlerTest.kt +++ b/logic/src/jvmTest/kotlin/com/wire/kalium/logic/sync/receiver/asset/AssetMessageHandlerTest.kt @@ -31,7 +31,7 @@ import com.wire.kalium.logic.data.message.PersistMessageUseCase import com.wire.kalium.logic.data.message.hasValidData import com.wire.kalium.logic.data.message.hasValidRemoteData import com.wire.kalium.logic.data.user.UserId -import com.wire.kalium.logic.feature.asset.ValidateAssetMimeTypeUseCase +import com.wire.kalium.logic.feature.asset.ValidateAssetFileTypeUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.util.time.UNIX_FIRST_DATE import io.mockative.Mock @@ -243,6 +243,224 @@ class AssetMessageHandlerTest { }.wasInvoked(exactly = once) } + @Test + fun givenValidPreviewAssetMessageStoredAndExtensionIsAllowed_whenHandlingTheUpdate_itIsCorrectlyProcessedAndVisible() = runTest { + // Given + val previewAssetMessage = PREVIEW_ASSET_MESSAGE.copy(visibility = Message.Visibility.HIDDEN) + val updateAssetMessage = COMPLETE_ASSET_MESSAGE + val isFileSharingEnabled = FileSharingStatus.Value.EnabledSome(listOf("txt", "png", "zip")) + val (arrangement, assetMessageHandler) = Arrangement() + .withSuccessfulFileSharingFlag(isFileSharingEnabled) + .withValidateAssetFileType(true) + .withSuccessfulStoredMessage(previewAssetMessage) + .withSuccessfulPersistMessageUseCase(updateAssetMessage) + .arrange() + + // When + assetMessageHandler.handle(updateAssetMessage) + + // Then + assertFalse((previewAssetMessage.content as MessageContent.Asset).value.hasValidRemoteData()) + assertTrue((updateAssetMessage.content as MessageContent.Asset).value.remoteData.hasValidData()) + coVerify { + arrangement.persistMessage( + matches { + it.id == updateAssetMessage.id + && it.conversationId.toString() == updateAssetMessage.conversationId.toString() + && it.visibility == Message.Visibility.VISIBLE + }) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.messageRepository.getMessageById( + eq(previewAssetMessage.conversationId), + eq(previewAssetMessage.id) + ) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.validateAssetFileTypeUseCase( + fileName = eq(COMPLETE_ASSET_CONTENT.value.name), + mimeType = eq("application/zip"), + allowedExtension = eq(isFileSharingEnabled.allowedType) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidPreviewAssetMessageStoredAndExtensionIsNotAllowed_whenHandlingTheUpdate_itIsProcessedButNoVisible() = runTest { + // Given + val previewAssetMessage = PREVIEW_ASSET_MESSAGE.copy(visibility = Message.Visibility.HIDDEN) + val updateAssetMessage = COMPLETE_ASSET_MESSAGE + val isFileSharingEnabled = FileSharingStatus.Value.EnabledSome(listOf("txt", "png")) + val (arrangement, assetMessageHandler) = Arrangement() + .withSuccessfulFileSharingFlag(isFileSharingEnabled) + .withValidateAssetFileType(true) + .withSuccessfulStoredMessage(previewAssetMessage) + .withSuccessfulPersistMessageUseCase(updateAssetMessage) + .arrange() + + // When + assetMessageHandler.handle(updateAssetMessage) + + // Then + assertFalse((previewAssetMessage.content as MessageContent.Asset).value.hasValidRemoteData()) + assertTrue((updateAssetMessage.content as MessageContent.Asset).value.remoteData.hasValidData()) + coVerify { + arrangement.persistMessage(matches { + it.id == updateAssetMessage.id + && it.conversationId.toString() == updateAssetMessage.conversationId.toString() + && it.visibility == updateAssetMessage.visibility + }) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.messageRepository.getMessageById( + conversationId = eq(previewAssetMessage.conversationId), + messageUuid = eq(previewAssetMessage.id) + ) + }.wasInvoked(exactly = once) + + coVerify { + arrangement.validateAssetFileTypeUseCase( + fileName = eq(COMPLETE_ASSET_CONTENT.value.name), + mimeType = eq("application/zip"), + allowedExtension = eq(isFileSharingEnabled.allowedType) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenValidPreviewAssetMessageStoredButFileSharingRestricted_whenHandlingTheUpdate_itIsProcessedButNoVisible() = runTest { + // Given + val previewAssetMessage = PREVIEW_ASSET_MESSAGE.copy(visibility = Message.Visibility.HIDDEN) + val updateAssetMessage = COMPLETE_ASSET_MESSAGE + val isFileSharingEnabled = FileSharingStatus.Value.Disabled + val (arrangement, assetMessageHandler) = Arrangement() + .withSuccessfulFileSharingFlag(isFileSharingEnabled) + .withValidateAssetFileType(true) + .withSuccessfulStoredMessage(previewAssetMessage) + .withSuccessfulPersistMessageUseCase(updateAssetMessage) + .arrange() + + // When + assetMessageHandler.handle(updateAssetMessage) + + // Then + assertFalse((previewAssetMessage.content as MessageContent.Asset).value.hasValidRemoteData()) + assertTrue((updateAssetMessage.content as MessageContent.Asset).value.remoteData.hasValidData()) + coVerify { + arrangement.persistMessage(matches { + it.id == updateAssetMessage.id + && it.conversationId.toString() == updateAssetMessage.conversationId.toString() + && it.visibility == updateAssetMessage.visibility + }) + }.wasInvoked(exactly = once) + + coVerify { arrangement.messageRepository.getMessageById(eq(previewAssetMessage.conversationId), eq(previewAssetMessage.id)) } + .wasNotInvoked() + + coVerify { + arrangement.validateAssetFileTypeUseCase( + fileName = any(), + mimeType = any(), + allowedExtension = any>() + ) + } + coVerify { arrangement.validateAssetFileTypeUseCase(any(), any(), any>()) } + .wasNotInvoked() + } + + @Test + fun givenFileWithNullNameAndCompleteData_whenProcessingCheckAPreviousAssetWithTheSameIDIsRestricted_thenDoNotStore() = runTest { + // Given + val messageCOntant = MessageContent.Asset( + AssetContent( + sizeInBytes = 100, + name = null, + mimeType = "", + metadata = null, + remoteData = AssetContent.RemoteData( + otrKey = "otrKey".toByteArray(), + sha256 = "sha256".toByteArray(), + assetId = "some-asset-id", + assetDomain = "some-asset-domain", + assetToken = "some-asset-token", + encryptionAlgorithm = MessageEncryptionAlgorithm.AES_GCM + ), + ) + + ) + val assetMessage = COMPLETE_ASSET_MESSAGE.copy(content = messageCOntant) + + val previewAssetMessage = PREVIEW_ASSET_MESSAGE.copy( + visibility = Message.Visibility.HIDDEN, + content = MessageContent.RestrictedAsset("application/zip", 500, "some-asset-name.zip.") + ) + + val isFileSharingEnabled = FileSharingStatus.Value.EnabledSome(listOf("txt", "png", "zip")) + val (arrangement, assetMessageHandler) = Arrangement() + .withSuccessfulFileSharingFlag(isFileSharingEnabled) + .withSuccessfulStoredMessage(previewAssetMessage) + .withValidateAssetFileType(true) + .arrange() + + // When + assetMessageHandler.handle(assetMessage) + + // Then + coVerify { arrangement.persistMessage(any()) } + .wasNotInvoked() + + coVerify { + arrangement.messageRepository.getMessageById( + eq(assetMessage.conversationId), eq(assetMessage.id) + ) + }.wasInvoked(exactly = once) + } + + @Test + fun givenFileWithNullNameAndCompleteData_whenProcessingCheckAPreviousAssetWithTheSameIDIsMissing_thenStoreAsRestricted() = runTest { + // Given + val messageCOntant = MessageContent.Asset( + AssetContent( + sizeInBytes = 100, + name = null, + mimeType = "", + metadata = null, + remoteData = AssetContent.RemoteData( + otrKey = "otrKey".toByteArray(), + sha256 = "sha256".toByteArray(), + assetId = "some-asset-id", + assetDomain = "some-asset-domain", + assetToken = "some-asset-token", + encryptionAlgorithm = MessageEncryptionAlgorithm.AES_GCM + ), + ) + + ) + val assetMessage = COMPLETE_ASSET_MESSAGE.copy(content = messageCOntant) + + val storedMessage = assetMessage.copy(content = MessageContent.RestrictedAsset(mimeType = "", sizeInBytes = 100, name = "")) + val isFileSharingEnabled = FileSharingStatus.Value.EnabledSome(listOf("txt", "png", "zip")) + val (arrangement, assetMessageHandler) = Arrangement() + .withSuccessfulFileSharingFlag(isFileSharingEnabled) + .withSuccessfulStoredMessage(null) + .withSuccessfulPersistMessageUseCase(storedMessage) + .withValidateAssetFileType(true) + .arrange() + + // When + assetMessageHandler.handle(assetMessage) + + // Then + coVerify { arrangement.persistMessage(any()) } + .wasInvoked(exactly = once) + + coVerify { arrangement.messageRepository.getMessageById(eq(assetMessage.conversationId), eq(assetMessage.id)) } + .wasInvoked(exactly = once) + } + private class Arrangement { @Mock @@ -255,14 +473,14 @@ class AssetMessageHandlerTest { val userConfigRepository = mock(UserConfigRepository::class) @Mock - val validateAssetMimeType = mock(ValidateAssetMimeTypeUseCase::class) + val validateAssetFileTypeUseCase = mock(ValidateAssetFileTypeUseCase::class) private val assetMessageHandlerImpl = - AssetMessageHandlerImpl(messageRepository, persistMessage, userConfigRepository, validateAssetMimeType) + AssetMessageHandlerImpl(messageRepository, persistMessage, userConfigRepository, validateAssetFileTypeUseCase) - fun withValidateAssetMime(result: Boolean) = apply { + fun withValidateAssetFileType(result: Boolean) = apply { every { - validateAssetMimeType.invoke(any(), any()) + validateAssetFileTypeUseCase.invoke(any(), any(), any()) }.returns(result) } diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetDownloadResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/asset/AssetMocks.kt similarity index 57% rename from mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetDownloadResponseJson.kt rename to mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/asset/AssetMocks.kt index 28ebcbb7675..1edb4bc8b25 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetDownloadResponseJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/asset/AssetMocks.kt @@ -15,25 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ +package com.wire.kalium.mocks.mocks.asset -package com.wire.kalium.mocks.responses.asset - -import com.wire.kalium.mocks.responses.ValidJsonProvider +import com.wire.kalium.network.api.authenticated.asset.AssetResponse import com.wire.kalium.network.api.model.ErrorResponse -object AssetDownloadResponseJson { - private val invalidJsonProvider = { serializable: ErrorResponse -> - """ - |{ - | "code": "${serializable.code}", - | "message": "${serializable.message}", - | "label": "${serializable.label}" - |} - """.trimMargin() - } +object AssetMocks { + + val invalid = ErrorResponse(code = 401, message = "Invalid Asset Token", label = "invalid_asset_token") - val invalid = ValidJsonProvider( - ErrorResponse(code = 401, message = "Invalid Asset Token", label = "invalid_asset_token"), - invalidJsonProvider + val asset = AssetResponse( + key = "3-1-e7788668-1b22-488a-b63c-acede42f771f", + expires = "expiration_date", + token = "asset_token", + domain = "staging.wire.link" ) + } diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/client/TokenMocks.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/client/TokenMocks.kt new file mode 100644 index 00000000000..70b43fd946f --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/client/TokenMocks.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.mocks.client + +import com.wire.kalium.network.api.model.AccessTokenDTO + +object TokenMocks { + + val accessToken = AccessTokenDTO( + userId = "user_id", // TODO check structure of userId to connect with User Mocks + value = "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939." + + "t=a.l=.u=75ebeb16-a860-4be4-84a7-157654b492cf.c=18401233206926541098", + expiresIn = 900, + tokenType = "Bearer" + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/connection/ConnectionMocks.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/connection/ConnectionMocks.kt new file mode 100644 index 00000000000..aa4085db774 --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/connection/ConnectionMocks.kt @@ -0,0 +1,111 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.mocks.connection + +import com.wire.kalium.network.api.authenticated.connection.ConnectionDTO +import com.wire.kalium.network.api.authenticated.connection.ConnectionResponse +import com.wire.kalium.network.api.authenticated.connection.ConnectionStateDTO +import com.wire.kalium.network.api.authenticated.connection.UpdateConnectionRequest +import com.wire.kalium.network.api.model.ConversationId +import com.wire.kalium.network.api.model.PaginationRequest +import com.wire.kalium.network.api.model.QualifiedID +import kotlinx.datetime.Instant + +object ConnectionMocks { + val connectionList = listOf( + ConnectionDTO( + conversationId = "addb6fbf-2bc3-4b59-b428-6fa4c594fb05", + from = "36ef84a9-837a-4f75-af81-5a2e70e06836", + lastUpdate = Instant.parse("2022-04-04T16:11:28.388Z"), + qualifiedConversationId = ConversationId( + domain = "staging.zinfra.io", + value = "addb6fbf-2bc3-4b59-b428-6fa4c594fb05" + ), + qualifiedToId = QualifiedID( + domain = "staging.zinfra.io", + value = "76ebeb16-a849-4be4-84a7-157654b492cf" + ), + status = ConnectionStateDTO.ACCEPTED, + toId = "76ebeb16-a849-4be4-84a7-157654b492cf" + ), + ConnectionDTO( + conversationId = "af6d3c9a-7934-4790-9ebf-f655e13acc76", + from = "36ef84a9-837a-4f75-af81-5a2e70e06836", + lastUpdate = Instant.parse("2022-03-23T16:53:32.515Z"), + qualifiedConversationId = ConversationId( + domain = "staging.zinfra.io", + value = "af6d3c9a-7934-4790-9ebf-f655e13acc76" + ), + qualifiedToId = QualifiedID( + domain = "staging.zinfra.io", + value = "787db7f1-f5ba-481b-af3e-9c27705a6440" + ), + status = ConnectionStateDTO.ACCEPTED, + toId = "787db7f1-f5ba-481b-af3e-9c27705a6440" + ), + ConnectionDTO( + conversationId = "f15a944a-b62b-4d9a-aff4-014d78a02294", + from = "36ef84a9-837a-4f75-af81-5a2e70e06836", + lastUpdate = Instant.parse("2022-03-25T17:20:13.637Z"), + qualifiedConversationId = ConversationId( + domain = "staging.zinfra.io", + value = "f15a944a-b62b-4d9a-aff4-014d78a02294" + ), + qualifiedToId = QualifiedID( + domain = "staging.zinfra.io", + value = "ba6b0fa1-32b1-4e25-8072-a71f07bfba5e" + ), + status = ConnectionStateDTO.ACCEPTED, + toId = "ba6b0fa1-32b1-4e25-8072-a71f07bfba5e" + ) + ) + + val connectionsResponse = ConnectionResponse( + connections = connectionList, + hasMore = false, + pagingState = "AQ==" + ) + + val connection = ConnectionDTO( + conversationId = "addb6fbf-2bc3-4b59-b428-6fa4c594fb05", + from = "36ef84a9-837a-4f75-af81-5a2e70e06836", + lastUpdate = Instant.parse("2022-04-04T16:11:28.388Z"), + qualifiedConversationId = ConversationId( + domain = "staging.zinfra.io", + value = "addb6fbf-2bc3-4b59-b428-6fa4c594fb05" + ), + qualifiedToId = QualifiedID( + domain = "staging.zinfra.io", + value = "76ebeb16-a849-4be4-84a7-157654b492cf" + ), + status = ConnectionStateDTO.ACCEPTED, + toId = "76ebeb16-a849-4be4-84a7-157654b492cf" + ) + + val emptyPaginationRequest = PaginationRequest( + size = 500, + pagingState = null + ) + + val paginationRequest = PaginationRequest( + size = 500, + pagingState = "PAGING_STATE_1234" + ) + + val acceptedConnectionRequest = UpdateConnectionRequest(ConnectionStateDTO.ACCEPTED) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/conversation/ConversationMocks.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/conversation/ConversationMocks.kt new file mode 100644 index 00000000000..b4ec178bc9d --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/conversation/ConversationMocks.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.mocks.conversation + +import com.wire.kalium.mocks.mocks.domain.DomainMocks +import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol +import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO +import com.wire.kalium.network.api.authenticated.conversation.ConversationMembersResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationPagingResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationsDetailsRequest +import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode +import com.wire.kalium.network.api.model.ConversationAccessDTO +import com.wire.kalium.network.api.model.ConversationAccessRoleDTO +import com.wire.kalium.network.api.model.ConversationId +import com.wire.kalium.network.api.model.UserId + +object ConversationMocks { + + val conversationId = ConversationId("conversation_id", DomainMocks.domain) + + val conversation = ConversationResponse( + "creator", + ConversationMembersResponse( + ConversationMemberDTO.Self(UserId("someValue", "someDomain"), "wire_member"), + emptyList() + ), + "group name", + conversationId, + null, + 0UL, + ConversationResponse.Type.GROUP, + 0, + null, + ConvProtocol.PROTEUS, + lastEventTime = "2024-03-30T15:36:00.000Z", + access = setOf(ConversationAccessDTO.INVITE, ConversationAccessDTO.CODE), + accessRole = setOf( + ConversationAccessRoleDTO.GUEST, + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.NON_TEAM_MEMBER + ), + mlsCipherSuiteTag = null, + receiptMode = ReceiptMode.DISABLED + ) + + val conversationListIdsResponse = ConversationPagingResponse( + conversationsIds = listOf( + conversationId, + ConversationId("f4680835-2cfe-4d4d-8491-cbb201bd5c2b", "anta.wire.link") + ), + hasMore = false, + pagingState = "AQ==" + ) + + val conversationsDetailsRequest = ConversationsDetailsRequest( + conversationsIds = listOf( + conversationId, + ConversationId("f4680835-2cfe-4d4d-8491-cbb201bd5c2b", "anta.wire.link") + ) + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/domain/DomainMocks.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/domain/DomainMocks.kt new file mode 100644 index 00000000000..04d3a2337ad --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/domain/DomainMocks.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.mocks.domain + +object DomainMocks { + + const val domain = "domain.com" + const val federatedDomain = "federated.com" + +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/user/UserMocks.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/user/UserMocks.kt new file mode 100644 index 00000000000..3aff76d8705 --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/mocks/user/UserMocks.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.mocks.user + +import com.wire.kalium.mocks.mocks.domain.DomainMocks +import com.wire.kalium.network.api.model.QualifiedID +import com.wire.kalium.network.api.model.SelfUserDTO + +object UserMocks { + + val selfId = QualifiedID("selfId", DomainMocks.domain) + val otherId = QualifiedID("otherId", DomainMocks.domain) + val secondId = QualifiedID("secondId", DomainMocks.domain) + val thirdId = QualifiedID("thirdId", DomainMocks.domain) + + val federatedId = QualifiedID("federatedId", DomainMocks.federatedDomain) + val federatedSecondId = QualifiedID("federatedSecondId", DomainMocks.federatedDomain) + + val selfUser = SelfUserDTO( + id = selfId, + name = "selfUser", + accentId = 2, + assets = listOf(), + deleted = null, + email = null, + handle = null, + service = null, + teamId = null, + expiresAt = "", + nonQualifiedId = "", + locale = "", + managedByDTO = null, + phone = null, + ssoID = null, + supportedProtocols = null + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConnectionRequests.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConnectionRequests.kt new file mode 100644 index 00000000000..26a17c5d3b1 --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConnectionRequests.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.requests + +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.connection.ConnectionMocks +import com.wire.kalium.mocks.responses.CommonResponses +import com.wire.kalium.network.api.authenticated.connection.ConnectionResponse +import com.wire.kalium.network.utils.TestRequestHandler +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode + +object ConnectionRequests { + private const val PATH_CONNECTION_LIST = "${CommonResponses.BASE_PATH_V1}list-connections" + fun connectionRequestResponseSuccess( + connectionResponse: ConnectionResponse = ConnectionMocks.connectionsResponse + ) = listOf( + TestRequestHandler( + path = PATH_CONNECTION_LIST, + httpMethod = HttpMethod.Post, + responseBody = connectionResponse.toJsonString(), + statusCode = HttpStatusCode.OK, + ) + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConversationRequests.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConversationRequests.kt new file mode 100644 index 00000000000..041950c0ffa --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/ConversationRequests.kt @@ -0,0 +1,64 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.requests + +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.conversation.ConversationMocks +import com.wire.kalium.mocks.responses.CommonResponses +import com.wire.kalium.mocks.responses.conversation.ConversationResponseJson +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseDTO +import com.wire.kalium.network.utils.TestRequestHandler +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode + +object ConversationRequests { + private const val PATH_CONVERSATION_ID_LIST = "${CommonResponses.BASE_PATH_V1}conversations/list-ids" + private const val PATH_CONVERSATIONS = "${CommonResponses.BASE_PATH_V1}conversations" + private const val PATH_CONVERSATIONS_LIST_V2 = "${CommonResponses.BASE_PATH_V1}/conversations/list/v2" + + private val conversationIdListApiRequestSuccess = TestRequestHandler( + path = PATH_CONVERSATION_ID_LIST, + httpMethod = HttpMethod.Post, + responseBody = ConversationMocks.conversationListIdsResponse.toJsonString(), + statusCode = HttpStatusCode.OK, + ) + + private val createConversationRequestSuccess = TestRequestHandler( + path = PATH_CONVERSATIONS, + httpMethod = HttpMethod.Post, + responseBody = ConversationResponseJson.v0().rawJson, + statusCode = HttpStatusCode.OK, + ) + + private val getConversationDetailsListRequestSuccess = TestRequestHandler( + path = PATH_CONVERSATIONS_LIST_V2, + httpMethod = HttpMethod.Post, + responseBody = ConversationResponseDTO( + conversationsFound = listOf(ConversationMocks.conversation), + conversationsNotFound = emptyList(), + conversationsFailed = emptyList() + ).toJsonString(), + statusCode = HttpStatusCode.OK, + ) + + val conversationsRequestResponseSuccess = listOf( + conversationIdListApiRequestSuccess, + createConversationRequestSuccess, + getConversationDetailsListRequestSuccess + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/NotificationRequests.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/NotificationRequests.kt index a75c8489b35..e31ce6c125d 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/NotificationRequests.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/NotificationRequests.kt @@ -31,6 +31,7 @@ object NotificationRequests { */ private const val PATH_LAST_NOTIFICATIONS = "${CommonResponses.BASE_PATH_V1}notifications/last" private const val PATH_PUSH_TOKENS = "${CommonResponses.BASE_PATH_V1}push/tokens" + private const val PATH_NOTIFICATIONS = "${CommonResponses.BASE_PATH_V1}notifications" /** * Request / Responses @@ -58,8 +59,16 @@ object NotificationRequests { statusCode = HttpStatusCode.OK, ) + private val notificationsListRequestResponseSuccess = TestRequestHandler( + path = PATH_NOTIFICATIONS, + httpMethod = HttpMethod.Get, + responseBody = NotificationEventsResponseJson.notificationResponseWithEmptyEvents.toJsonString(), + statusCode = HttpStatusCode.OK, + ) + val notificationsRequestResponseSuccess = listOf( pushTokenApiRequestSuccess, - lastNotificationsApiRequestSuccess + lastNotificationsApiRequestSuccess, + notificationsListRequestResponseSuccess ) } diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/PreKeyRequests.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/PreKeyRequests.kt new file mode 100644 index 00000000000..ed0fd4e5069 --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/requests/PreKeyRequests.kt @@ -0,0 +1,89 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.requests + +import com.wire.kalium.mocks.mocks.domain.DomainMocks +import com.wire.kalium.mocks.responses.CommonResponses +import com.wire.kalium.mocks.responses.ValidJsonProvider +import com.wire.kalium.network.api.authenticated.prekey.PreKeyDTO +import com.wire.kalium.network.utils.TestRequestHandler +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode + +object PreKeyRequests { + private const val PATH_CLIENTS_PRE_KEYS = "${CommonResponses.BASE_PATH_V1}clients/defkrr8e7grgsoufhg8/prekeys" + private const val PATH_CLIENTS = "${CommonResponses.BASE_PATH_V1}clients/defkrr8e7grgsoufhg8" + + private const val PATH_USERS_PRE_KEYS = "${CommonResponses.BASE_PATH_V1}users/list-prekeys" + + private const val USER_1 = "someValue" + private const val USER_1_CLIENT = "defkrr8e7grgsoufhg8" + private val USER_1_CLIENT_PREYKEY = PreKeyDTO(key = "preKey1CoQBYIOjl7hw0D8YRNq", id = 1) + + private val clientPreKeysApiRequestSuccess = TestRequestHandler( + path = PATH_CLIENTS_PRE_KEYS, + httpMethod = HttpMethod.Get, + responseBody = "[1]", + statusCode = HttpStatusCode.OK, + ) + private val putClientPreKeysApiRequestSuccess = TestRequestHandler( + path = PATH_CLIENTS, + httpMethod = HttpMethod.Put, + responseBody = "", + statusCode = HttpStatusCode.OK, + ) + + private val jsonProvider = { _: Map>> -> + """ + |{ + | "${DomainMocks.domain}": { + | "$USER_1": { + | "$USER_1_CLIENT": { + | "key": "${USER_1_CLIENT_PREYKEY.key}", + | "id": ${USER_1_CLIENT_PREYKEY.id} + | } + | } + | } + |} + """.trimMargin() + } + + val valid = ValidJsonProvider( + mapOf( + DomainMocks.domain to + mapOf( + USER_1 to + mapOf(USER_1_CLIENT to USER_1_CLIENT_PREYKEY) + ) + ), + jsonProvider + ) + + private val postUserPreKeysApiRequestSuccess = TestRequestHandler( + path = PATH_USERS_PRE_KEYS, + httpMethod = HttpMethod.Post, + responseBody = valid.rawJson, + statusCode = HttpStatusCode.OK, + ) + + val preKeyRequestResponseSuccess = listOf( + clientPreKeysApiRequestSuccess, + putClientPreKeysApiRequestSuccess, + postUserPreKeysApiRequestSuccess + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/AccessTokenDTOJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/AccessTokenDTOJson.kt deleted file mode 100644 index bef3dfac4c5..00000000000 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/AccessTokenDTOJson.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.kalium.mocks.responses - -import com.wire.kalium.network.api.model.AccessTokenDTO - -object AccessTokenDTOJson { - private val jsonProvider = { serializable: AccessTokenDTO -> - """ - |{ - | "expires_in": ${serializable.expiresIn}, - | "access_token": "${serializable.value}", - | "user": "${serializable.userId}", - | "token_type": "${serializable.tokenType}" - |} - """.trimMargin() - } - - fun createValid(accessTokenDTO: AccessTokenDTO) = ValidJsonProvider(accessTokenDTO, jsonProvider) - - val valid = ValidJsonProvider( - AccessTokenDTO( - userId = "user_id", - value = "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939." + - "t=a.l=.u=75ebeb16-a860-4be4-84a7-157654b492cf.c=18401233206926541098", - expiresIn = 900, - tokenType = "Bearer" - ), jsonProvider - ) - - val missingAccessToken = FaultyJsonProvider( - """ - |{ - | "expires_in": 900, - | "user": "75ebeb16-a860-4be4-84a7-157654b492", - | "token_type": "Bearer" - |} - """.trimMargin() - ) - val missingTokenType = FaultyJsonProvider( - """ - |{ - | "expires_in": 900, - | "access_token": "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939.t=a.l=.u=75ebeb16-a860-4be4-84a7-157654b492cf.c=18401233206926541098", - | "user": "75ebeb16-a860-4be4-84a7-157654b492" - |} - """.trimMargin() - ) - val missingUser = FaultyJsonProvider( - """ - |{ - | "expires_in": 900, - | "access_token": "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939.t=a.l=.u=75ebeb16-a860-4be4-84a7-157654b492cf.c=18401233206926541098", - | "token_type": "Bearer" - |} - """.trimMargin() - ) - val missingExpiration = FaultyJsonProvider( - """ - |{ - | "access_token": "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939.t=a.l=.u=75ebeb16-a860-4be4-84a7-157654b492cf.c=18401233206926541098", - | "user": "75ebeb16-a860-4be4-84a7-157654b492", - | "token_type": "Bearer" - |} - """.trimMargin() - ) -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ClientResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ClientResponseJson.kt index 6b5f180ec67..d3d7e0010e1 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ClientResponseJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ClientResponseJson.kt @@ -18,7 +18,6 @@ package com.wire.kalium.mocks.responses -import com.wire.kalium.network.api.authenticated.client.Capabilities import com.wire.kalium.network.api.authenticated.client.ClientCapabilityDTO import com.wire.kalium.network.api.authenticated.client.ClientDTO import com.wire.kalium.network.api.authenticated.client.ClientTypeDTO @@ -26,6 +25,26 @@ import com.wire.kalium.network.api.authenticated.client.DeviceTypeDTO object ClientResponseJson { private val jsonProvider = { serializable: ClientDTO -> + """ + |{ + | "id": "${serializable.clientId}", + | "type": "${serializable.type}", + | "time": "${serializable.registrationTime}", + | "last_active": "${serializable.lastActive}", + | "class": "${serializable.deviceType}", + | "label": "${serializable.label}", + | "cookie": "${serializable.cookie}", + | "model": "${serializable.model}", + | "capabilities": [ + | "${serializable.capabilities[0]}" + | ], + | "mls_public_keys": ${serializable.mlsPublicKeys} + |} + """.trimMargin() + } + + // This is backwards compatible with the old format till v5 API get deprecated + private val jsonProviderCapabilitiesObject = { serializable: ClientDTO -> """ |{ | "id": "${serializable.clientId}", @@ -38,13 +57,30 @@ object ClientResponseJson { | "model": "${serializable.model}", | "capabilities": { | "capabilities": [ - | "${serializable.capabilities!!.capabilities[0]}" + | "${serializable.capabilities[0]}" | ] | } + | "mls_public_keys": ${serializable.mlsPublicKeys} |} """.trimMargin() } + val validCapabilitiesObject = ValidJsonProvider( + ClientDTO( + clientId = "defkrr8e7grgsoufhg8", + type = ClientTypeDTO.Permanent, + deviceType = DeviceTypeDTO.Phone, + registrationTime = "2021-05-12T10:52:02.671Z", + lastActive = "2021-05-12T10:52:02.671Z", + label = "label", + cookie = "sldkfmdeklmwldwlek23kl44mntiuepfojfndkjd", + capabilities = listOf(ClientCapabilityDTO.LegalHoldImplicitConsent), + model = "model", + mlsPublicKeys = null + ), + jsonProviderCapabilitiesObject + ) + val valid = ValidJsonProvider( ClientDTO( clientId = "defkrr8e7grgsoufhg8", @@ -54,7 +90,7 @@ object ClientResponseJson { lastActive = "2021-05-12T10:52:02.671Z", label = "label", cookie = "sldkfmdeklmwldwlek23kl44mntiuepfojfndkjd", - capabilities = Capabilities(listOf(ClientCapabilityDTO.LegalHoldImplicitConsent)), + capabilities = listOf(ClientCapabilityDTO.LegalHoldImplicitConsent), model = "model", mlsPublicKeys = null ), diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/CommonResponses.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/CommonResponses.kt index 0d73a8495e5..12499c3ad4d 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/CommonResponses.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/CommonResponses.kt @@ -50,7 +50,7 @@ object CommonResponses { /** * DTO */ - val userID = QualifiedID("user_id", "user.domain.io") + val userID = QualifiedID("someValue", "someDomain") private val accessTokenDTO = AccessTokenDTO( userId = userID.value, value = "Nlrhltkj-NgJUjEVevHz8Ilgy_pyWCT2b0kQb-GlnamyswanghN9DcC3an5RUuA7sh1_nC3hv2ZzMRlIhPM7Ag==.v=1.k=1.d=1637254939." + diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ListOfClientsResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ListOfClientsResponseJson.kt index 944d1a4506f..242ce69b754 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ListOfClientsResponseJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/ListOfClientsResponseJson.kt @@ -18,7 +18,6 @@ package com.wire.kalium.mocks.responses -import com.wire.kalium.network.api.authenticated.client.Capabilities import com.wire.kalium.network.api.authenticated.client.ClientCapabilityDTO import com.wire.kalium.network.api.authenticated.client.ClientDTO import com.wire.kalium.network.api.authenticated.client.ClientTypeDTO @@ -38,7 +37,7 @@ object ListOfClientsResponseJson { | "model": "${serializable.model}", | "capabilities": { | "capabilities": [ - | "${serializable.capabilities!!.capabilities[0]}" + | "${serializable.capabilities[0]}" | ] | } |}] @@ -54,7 +53,7 @@ object ListOfClientsResponseJson { lastActive = "2023-05-12T10:52:02.671Z", label = "label", cookie = "sldkfmdeklmwldwlek23kl44mntiuepfojfndkjd", - capabilities = Capabilities(listOf(ClientCapabilityDTO.LegalHoldImplicitConsent)), + capabilities = listOf(ClientCapabilityDTO.LegalHoldImplicitConsent), model = "model", mlsPublicKeys = null ), diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/MigrationUserToTeamResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/MigrationUserToTeamResponseJson.kt new file mode 100644 index 00000000000..69fa04e4399 --- /dev/null +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/MigrationUserToTeamResponseJson.kt @@ -0,0 +1,68 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.mocks.responses + +import com.wire.kalium.network.api.authenticated.user.CreateUserTeamDTO +import com.wire.kalium.network.api.model.ErrorResponse +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put + +object MigrationUserToTeamResponseJson { + val success = ValidJsonProvider( + serializableData = CreateUserTeamDTO( + teamId = "teamId", + teamName = "teamName" + ), + jsonProvider = { serializable -> + buildJsonObject { + put("team_id", serializable.teamId) + put("team_name", serializable.teamName) + }.toString() + } + ) + + val failedUserInTeam = ValidJsonProvider( + serializableData = ErrorResponse( + code = 403, + label = "user-already-in-a-team", + message = "Switching teams is not allowed" + ), + jsonProvider = { serializable -> + buildJsonObject { + put("code", serializable.code) + put("label", serializable.label) + put("message", serializable.message) + }.toString() + } + ) + + val failedUserNotFound = ValidJsonProvider( + serializableData = ErrorResponse( + code = 404, + label = "not-found", + message = "User not found" + ), + jsonProvider = { serializable -> + buildJsonObject { + put("code", serializable.code) + put("label", serializable.label) + put("message", serializable.message) + }.toString() + } + ) +} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/NotificationEventsResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/NotificationEventsResponseJson.kt index d737b629a73..2d5458837f0 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/NotificationEventsResponseJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/NotificationEventsResponseJson.kt @@ -77,7 +77,7 @@ object NotificationEventsResponseJson { type = ClientTypeDTO.Permanent, deviceType = DeviceTypeDTO.Desktop, label = "OS X 10.15 10.15", - capabilities = null, + capabilities = listOf(), mlsPublicKeys = mapOf(Pair("key_variant", "public_key")), ) ), @@ -157,7 +157,7 @@ object NotificationEventsResponseJson { | "id" : "${eventData.qualifiedFrom.value}", | "domain" : "${eventData.qualifiedFrom.domain}" | }, - | "data" : ${ConversationResponseJson.conversationResponseSerializer(eventData.data)}, + | "data" : ${ConversationResponseJson.conversationResponseSerializerV3(eventData.data)}, | "time" : "2022-04-12T13:57:02.414Z", | "type" : "conversation.create" |} @@ -390,4 +390,16 @@ object NotificationEventsResponseJson { payload = listOf(), transient = false ) + + val notificationResponseWithEmptyEvents = NotificationResponse( + time = "2022-02-15T12:54:30Z", + hasMore = false, + notifications = listOf( + EventResponse( + id = "eventId", + payload = listOf(), + transient = false + ) + ) + ) } diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/UserDTOJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/UserDTOJson.kt deleted file mode 100644 index d61930e0c3d..00000000000 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/UserDTOJson.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.kalium.mocks.responses - -import com.wire.kalium.network.api.model.SelfUserDTO -import com.wire.kalium.network.api.model.UserId -import kotlinx.serialization.json.addJsonObject -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray -import kotlinx.serialization.json.putJsonObject - -object UserDTOJson { - - private val jsonProvider = { serializable: SelfUserDTO -> - buildJsonObject { - put("accent_id", serializable.accentId) - put("id", serializable.nonQualifiedId) - putJsonObject("qualified_id") { - put("id", serializable.id.value) - put("domain", serializable.id.domain) - } - put("name", serializable.name) - put("locale", serializable.locale) - putJsonArray("assets") { - if (serializable.assets.isNotEmpty()) { - addJsonObject { - serializable.assets.forEach { userAsset -> - put("key", userAsset.key) - put("type", userAsset.type.toString()) - userAsset.size?.let { put("size", it.toString()) } - } - } - } - } - serializable.deleted?.let { put("deleted", it) } - serializable.email?.let { put("email", it) } - serializable.phone?.let { put("phone", it) } - serializable.expiresAt?.let { put("expires_at", it) } - serializable.handle?.let { put("handle", it) } - serializable.service?.let { service -> - putJsonObject("service") { - put("id", service.id) - put("provider", service.provider) - } - } - serializable.teamId?.let { put("team", it) } - serializable.managedByDTO?.let { put("managed_by", it.toString()) } - serializable.ssoID?.let { userSsoID -> - putJsonObject("sso_id") { - userSsoID.subject?.let { put("subject", it) } - userSsoID.scimExternalId?.let { put("scim_external_id", it) } - userSsoID.tenant?.let { put("tenant", it) } - } - } - }.toString() - } - - fun createValid(userDTO: SelfUserDTO) = ValidJsonProvider(userDTO, jsonProvider) - - val valid = ValidJsonProvider( - SelfUserDTO( - id = UserId("user_id", "domain.com"), - name = "user_name_123", - accentId = 2, - assets = listOf(), - deleted = null, - email = null, - handle = null, - service = null, - teamId = null, - expiresAt = "", - nonQualifiedId = "", - locale = "", - managedByDTO = null, - phone = null, - ssoID = null, - supportedProtocols = null - ), jsonProvider - ) -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetUploadResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetUploadResponseJson.kt deleted file mode 100644 index 55fe101f76b..00000000000 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/asset/AssetUploadResponseJson.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.kalium.mocks.responses.asset - -import com.wire.kalium.mocks.responses.ValidJsonProvider -import com.wire.kalium.network.api.authenticated.asset.AssetResponse -import com.wire.kalium.network.api.model.ErrorResponse - -object AssetUploadResponseJson { - private val validJsonProvider = { serializable: AssetResponse -> - """ - |{ - | "key": "${serializable.key}", - | "expires": "${serializable.expires}", - | "token": "${serializable.token}", - | "domain": "${serializable.domain}" - |} - """.trimMargin() - } - private val invalidJsonProvider = { serializable: ErrorResponse -> - """ - |{ - | "code": "${serializable.code}", - | "message": "${serializable.message}", - | "label": "${serializable.label}" - |} - """.trimMargin() - } - - val valid = ValidJsonProvider( - AssetResponse( - key = "3-1-e7788668-1b22-488a-b63c-acede42f771f", - expires = "expiration_date", - token = "asset_token", - domain = "staging.wire.link" - ), - validJsonProvider - ) - - val invalid = ValidJsonProvider( - ErrorResponse(code = 401, message = "Invalid Asset Token", label = "invalid_asset_token"), - invalidJsonProvider - ) -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionResponsesJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionResponsesJson.kt deleted file mode 100644 index 2aff49f8346..00000000000 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/connection/ConnectionResponsesJson.kt +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.kalium.mocks.responses.connection - -import com.wire.kalium.mocks.responses.AnyResponseProvider -import com.wire.kalium.mocks.responses.ValidJsonProvider - -object ConnectionResponsesJson { - - object GetConnections { - private val jsonProvider = { _: String -> - """ - |{ - | "connections": [ - | { - | "conversation": "addb6fbf-2bc3-4b59-b428-6fa4c594fb05", - | "from": "36ef84a9-837a-4f75-af81-5a2e70e06836", - | "last_update": "2022-04-04T16:11:28.388Z", - | "qualified_conversation": { - | "domain": "staging.zinfra.io", - | "id": "addb6fbf-2bc3-4b59-b428-6fa4c594fb05" - | }, - | "qualified_to": { - | "domain": "staging.zinfra.io", - | "id": "76ebeb16-a849-4be4-84a7-157654b492cf" - | }, - | "status": "accepted", - | "to": "76ebeb16-a849-4be4-84a7-157654b492cf" - | }, - | { - | "conversation": "af6d3c9a-7934-4790-9ebf-f655e13acc76", - | "from": "36ef84a9-837a-4f75-af81-5a2e70e06836", - | "last_update": "2022-03-23T16:53:32.515Z", - | "qualified_conversation": { - | "domain": "staging.zinfra.io", - | "id": "af6d3c9a-7934-4790-9ebf-f655e13acc76" - | }, - | "qualified_to": { - | "domain": "staging.zinfra.io", - | "id": "787db7f1-f5ba-481b-af3e-9c27705a6440" - | }, - | "status": "accepted", - | "to": "787db7f1-f5ba-481b-af3e-9c27705a6440" - | }, - | { - | "conversation": "f15a944a-b62b-4d9a-aff4-014d78a02294", - | "from": "36ef84a9-837a-4f75-af81-5a2e70e06836", - | "last_update": "2022-03-25T17:20:13.637Z", - | "qualified_conversation": { - | "domain": "staging.zinfra.io", - | "id": "f15a944a-b62b-4d9a-aff4-014d78a02294" - | }, - | "qualified_to": { - | "domain": "staging.zinfra.io", - | "id": "ba6b0fa1-32b1-4e25-8072-a71f07bfba5e" - | }, - | "status": "accepted", - | "to": "ba6b0fa1-32b1-4e25-8072-a71f07bfba5e" - | } - | ], - | "has_more": false, - | "paging_state": "AQ==" - |} - """.trimIndent() - } - - val validGetConnections = AnyResponseProvider(data = "", jsonProvider) - } - - object CreateConnectionResponse { - val jsonProvider = ValidJsonProvider(String) { - """ - { - "conversation": "addb6fbf-2bc3-4b59-b428-6fa4c594fb05", - "from": "36ef84a9-837a-4f75-af81-5a2e70e06836", - "last_update": "2022-04-04T16:11:28.388Z", - "qualified_conversation": { - "domain": "staging.zinfra.io", - "id": "addb6fbf-2bc3-4b59-b428-6fa4c594fb05" - }, - "qualified_to": { - "domain": "staging.zinfra.io", - "id": "76ebeb16-a849-4be4-84a7-157654b492cf" - }, - "status": "accepted", - "to": "76ebeb16-a849-4be4-84a7-157654b492cf" - } - """.trimIndent() - } - } -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationListIdsResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationListIdsResponseJson.kt deleted file mode 100644 index b7fd220ab77..00000000000 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationListIdsResponseJson.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Wire - * Copyright (C) 2024 Wire Swiss GmbH - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see http://www.gnu.org/licenses/. - */ - -package com.wire.kalium.mocks.responses.conversation - -import com.wire.kalium.mocks.responses.AnyResponseProvider -import com.wire.kalium.mocks.responses.ValidJsonProvider -import com.wire.kalium.network.api.authenticated.conversation.ConversationsDetailsRequest - -object ConversationListIdsResponseJson { - - private val jsonProvider = { _: String -> - """ - |{ - | "has_more": false, - | "paging_state": "AQ==", - | "qualified_conversations": [ - | { - | "domain": "anta.wire.link", - | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" - | }, - | { - | "domain": "anta.wire.link", - | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" - | } - | ] - |} - """.trimIndent() - } - - val validGetIds = AnyResponseProvider(data = "", jsonProvider) - - val validRequestIds = ValidJsonProvider( - ConversationsDetailsRequest(emptyList()) - ) { - """ - |{ - | "qualified_ids": [ - | { - | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", - | "domain": "anta.wire.link" - | }, - | { - | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", - | "domain": "anta.wire.link" - | } - | ] - |} - """.trimIndent() - } - -} diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationResponseJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationResponseJson.kt index 8176a630e51..8fbd78dff25 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationResponseJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/ConversationResponseJson.kt @@ -24,9 +24,11 @@ import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberDTO import com.wire.kalium.network.api.authenticated.conversation.ConversationMembersResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV6 import com.wire.kalium.network.api.authenticated.conversation.MutedStatus import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode import com.wire.kalium.network.api.authenticated.conversation.ServiceReferenceDTO +import com.wire.kalium.network.api.authenticated.serverpublickey.MLSPublicKeysDTO import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.model.QualifiedID @@ -41,90 +43,136 @@ import kotlinx.serialization.json.putJsonObject object ConversationResponseJson { - val conversationResponseSerializer = { it: ConversationResponse -> - buildConversationResponse(it).toString() + val conversationResponseSerializerV6 = { it: ConversationResponseV6 -> + buildConversationResponseV6(it).toString() + } + + val conversationResponseSerializerV3 = { it: ConversationResponse -> + buildConversationResponseV3(it).toString() } val conversationResponseSerializerWithDeprecatedAccessRole = { it: ConversationResponse -> - buildConversationResponse(it, useDeprecatedAccessRole = true).toString() + buildConversationResponseV3(it, useDeprecatedAccessRole = true).toString() } - private val conversationResponse = ConversationResponse( - "fdf23116-42a5-472c-8316-e10655f5d11e", - ConversationMembersResponse( - ConversationMemberDTO.Self( - QualifiedIDSamples.one, - "wire_admin", - otrMutedRef = "2022-04-11T14:15:48.044Z", - otrMutedStatus = MutedStatus.fromOrdinal(0) + private val conversationResponseV6 = ConversationResponseV6( + conversation = ConversationResponse( + "fdf23116-42a5-472c-8316-e10655f5d11e", + ConversationMembersResponse( + ConversationMemberDTO.Self( + QualifiedIDSamples.one, + "wire_admin", + otrMutedRef = "2022-04-11T14:15:48.044Z", + otrMutedStatus = MutedStatus.fromOrdinal(0) + ), + listOf( + ConversationMemberDTO.Other( + id = QualifiedIDSamples.two, + conversationRole = "wire_member" + ) + ) ), - listOf(ConversationMemberDTO.Other(id = QualifiedIDSamples.two, conversationRole = "wire_member")) - ), - "group name", - QualifiedIDSamples.one, - "groupID", - 0UL, - ConversationResponse.Type.GROUP, - null, - "teamID", - ConvProtocol.PROTEUS, - lastEventTime = "2022-03-30T15:36:00.000Z", - access = setOf(ConversationAccessDTO.INVITE, ConversationAccessDTO.CODE), - accessRole = setOf( - ConversationAccessRoleDTO.GUEST, - ConversationAccessRoleDTO.TEAM_MEMBER, - ConversationAccessRoleDTO.NON_TEAM_MEMBER - ), - mlsCipherSuiteTag = null, - receiptMode = ReceiptMode.DISABLED + "group name", + QualifiedIDSamples.one, + "groupID", + 0UL, + ConversationResponse.Type.GROUP, + null, + "teamID", + ConvProtocol.PROTEUS, + lastEventTime = "2022-03-30T15:36:00.000Z", + access = setOf(ConversationAccessDTO.INVITE, ConversationAccessDTO.CODE), + accessRole = setOf( + ConversationAccessRoleDTO.GUEST, + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.NON_TEAM_MEMBER + ), + mlsCipherSuiteTag = null, + receiptMode = ReceiptMode.DISABLED, + + ), + publicKeys = MLSPublicKeysDTO( + removal = mapOf("ecdsa_secp256r1_sha256" to "string", "ed25519" to "string") + ) + ) + + val v6 = ValidJsonProvider( + conversationResponseV6, + conversationResponseSerializerV6 ) val v3 = ValidJsonProvider( - conversationResponse, conversationResponseSerializer + conversationResponseV6.conversation, + conversationResponseSerializerV3 ) - val v0 = ValidJsonProvider( - conversationResponse, conversationResponseSerializerWithDeprecatedAccessRole + fun v0(accessRole: Set? = null) = ValidJsonProvider( + conversationResponseV6.conversation.copy( + accessRole = accessRole ?: conversationResponseV6.conversation.accessRole + ), + conversationResponseSerializerWithDeprecatedAccessRole ) } -fun buildConversationResponse( +fun buildConversationResponseV6( + conversationResponse: ConversationResponseV6, + useDeprecatedAccessRole: Boolean = false, +): JsonObject = buildJsonObject { + putJsonObject("conversation") { + putConversation(conversationResponse.conversation, useDeprecatedAccessRole) + } + putJsonObject("public_keys") { + conversationResponse.publicKeys.removal?.forEach { (key, value) -> + put(key, value) + } + } + +} + +fun buildConversationResponseV3( conversationResponse: ConversationResponse, useDeprecatedAccessRole: Boolean = false -): JsonObject = - buildJsonObject { - put("creator", conversationResponse.creator) - putQualifiedId(conversationResponse.id) - conversationResponse.groupId?.let { put("group_id", it) } - putJsonObject("members") { - putSelfMember(conversationResponse.members.self) - putJsonArray("others") { - conversationResponse.members.otherMembers.forEach { otherMember -> - addJsonObject { - putOtherMember(otherMember) - } +): JsonObject = buildJsonObject { + putConversation(conversationResponse, useDeprecatedAccessRole) +} + +private fun JsonObjectBuilder.putConversation( + conversationResponse: ConversationResponse, + useDeprecatedAccessRole: Boolean +) { + put("creator", conversationResponse.creator) + putQualifiedId(conversationResponse.id) + conversationResponse.groupId?.let { put("group_id", it) } + putJsonObject("members") { + putSelfMember(conversationResponse.members.self) + putJsonArray("others") { + conversationResponse.members.otherMembers.forEach { otherMember -> + addJsonObject { + putOtherMember(otherMember) } } } - put("type", conversationResponse.type.ordinal) - put("protocol", conversationResponse.protocol.toString()) - put("last_event_time", conversationResponse.lastEventTime) - putAccessSet(conversationResponse.access) - if (useDeprecatedAccessRole) { - conversationResponse.accessRole?.let { putDeprecatedAccessRoleSet(it) } - } else { - conversationResponse.accessRole?.let { putAccessRoleSet(it) } - } - conversationResponse.messageTimer?.let { put("message_timer", it) } - conversationResponse.name?.let { put("name", it) } - conversationResponse.teamId?.let { put("team", it) } - conversationResponse.mlsCipherSuiteTag?.let { put("cipher_suite", it) } } - -fun JsonObjectBuilder.putAccessRoleSet(accessRole: Set) = putJsonArray("access_role") { - accessRole.forEach { add(it.toString()) } + put("type", conversationResponse.type.ordinal) + put("protocol", conversationResponse.protocol.toString()) + put("last_event_time", conversationResponse.lastEventTime) + putAccessSet(conversationResponse.access) + if (useDeprecatedAccessRole) { + conversationResponse.accessRole?.let { putDeprecatedAccessRoleSet(it) } + } else { + conversationResponse.accessRole?.let { putAccessRoleSet(it) } + } + conversationResponse.messageTimer?.let { put("message_timer", it) } + conversationResponse.name?.let { put("name", it) } + conversationResponse.teamId?.let { put("team", it) } + conversationResponse.mlsCipherSuiteTag?.let { put("cipher_suite", it) } } +fun JsonObjectBuilder.putAccessRoleSet(accessRole: Set) = + putJsonArray("access_role") { + accessRole.forEach { add(it.toString()) } + } + fun JsonObjectBuilder.putDeprecatedAccessRoleSet(accessRole: Set) = putJsonArray("access_role_v2") { accessRole.forEach { add(it.toString()) } diff --git a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/CreateConversationRequestJson.kt b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/CreateConversationRequestJson.kt index 92f99ee77ba..8e03a9de011 100644 --- a/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/CreateConversationRequestJson.kt +++ b/mocks/src/commonMain/kotlin/com/wire/kalium/mocks/responses/conversation/CreateConversationRequestJson.kt @@ -30,15 +30,15 @@ import com.wire.kalium.network.api.model.ConversationAccessRoleDTO object CreateConversationRequestJson { private val createConversationRequest = CreateConversationRequest( - listOf(QualifiedIDSamples.one), + qualifiedUsers = listOf(QualifiedIDSamples.one), name = "NameOfThisGroupConversation", - listOf(ConversationAccessDTO.PRIVATE), - listOf(ConversationAccessRoleDTO.TEAM_MEMBER), - ConvTeamInfo(false, "teamID"), - 0, - ReceiptMode.DISABLED, - "WIRE_MEMBER", - ConvProtocol.PROTEUS, + access = listOf(ConversationAccessDTO.PRIVATE), + accessRole = listOf(ConversationAccessRoleDTO.TEAM_MEMBER), + convTeamInfo = ConvTeamInfo(false, "teamID"), + messageTimer = 0, + receiptMode = ReceiptMode.DISABLED, + conversationRole = "WIRE_MEMBER", + protocol = ConvProtocol.PROTEUS, creatorClient = null ) @@ -72,8 +72,10 @@ object CreateConversationRequestJson { """.trimMargin() } - val v3 = ValidJsonProvider( - createConversationRequest + fun v3(accessRole: List? = null) = ValidJsonProvider( + createConversationRequest.copy( + accessRole = accessRole ?: createConversationRequest.accessRole + ) ) { """ |{ diff --git a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt index 86b973a04c3..5ad0ef16e75 100644 --- a/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt +++ b/monkeys/src/main/kotlin/com/wire/kalium/monkeys/homeDirectory.kt @@ -29,12 +29,14 @@ fun coreLogic( rootPath: String, ): CoreLogic { val coreLogic = CoreLogic( - rootPath, kaliumConfigs = KaliumConfigs( + rootPath = rootPath, + kaliumConfigs = KaliumConfigs( developmentApiEnabled = true, encryptProteusStorage = true, - isMLSSupportEnabled = true, wipeOnDeviceRemoval = true, - ), userAgent = "Wire Infinite Monkeys", useInMemoryStorage = true + ), + userAgent = "Wire Infinite Monkeys", + useInMemoryStorage = true ) coreLogic.updateApiVersionsScheduler.scheduleImmediateApiVersionUpdate() return coreLogic diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/CapabilitiesDeserializer.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/CapabilitiesDeserializer.kt new file mode 100644 index 00000000000..165eb7c1b0c --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/CapabilitiesDeserializer.kt @@ -0,0 +1,40 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.authenticated.client + +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonTransformingSerializer + +/** + * Sometimes the capabilities are wrapped in an object, sometimes they are just an array. + * See [documentation](https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/1309868033/API+changes+v6+v7) + */ +object CapabilitiesDeserializer : + JsonTransformingSerializer>(ListSerializer(ClientCapabilityDTO.serializer())) { + override fun transformDeserialize(element: JsonElement): JsonElement { + return when { + element is JsonObject && element.containsKey("capabilities") -> element["capabilities"]!! + element is JsonArray -> element + else -> throw SerializationException("Unexpected JSON format for capabilities") + } + } +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/ClientDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/ClientDTO.kt index 9d070bfa2bd..c8253498b87 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/ClientDTO.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/client/ClientDTO.kt @@ -30,7 +30,9 @@ data class ClientDTO( @SerialName("id") val clientId: String, @SerialName("type") val type: ClientTypeDTO, @SerialName("class") val deviceType: DeviceTypeDTO = DeviceTypeDTO.Unknown, - @SerialName("capabilities") val capabilities: Capabilities?, + @SerialName("capabilities") + @Serializable(with = CapabilitiesDeserializer::class) + val capabilities: List, @SerialName("label") val label: String?, @SerialName("mls_public_keys") val mlsPublicKeys: Map? ) diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationPagingResponse.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationPagingResponse.kt index ff80df964a8..0028e4393da 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationPagingResponse.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationPagingResponse.kt @@ -35,3 +35,10 @@ data class ConversationResponseDTO( @SerialName("not_found") val conversationsNotFound: List, @SerialName("failed") val conversationsFailed: List, ) + +@Serializable +data class ConversationResponseDTOV3( + @SerialName("found") val conversationsFound: List, + @SerialName("not_found") val conversationsNotFound: List, + @SerialName("failed") val conversationsFailed: List, +) diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt index f448e551672..71452d4f8b8 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/conversation/ConversationResponse.kt @@ -18,6 +18,7 @@ package com.wire.kalium.network.api.authenticated.conversation +import com.wire.kalium.network.api.authenticated.serverpublickey.MLSPublicKeysDTO import com.wire.kalium.network.api.model.ConversationAccessDTO import com.wire.kalium.network.api.model.ConversationAccessRoleDTO import com.wire.kalium.network.api.model.ConversationId @@ -83,10 +84,13 @@ data class ConversationResponse( val access: Set, @SerialName("access_role_v2") - val accessRole: Set = ConversationAccessRoleDTO.DEFAULT_VALUE_WHEN_NULL, + val accessRole: Set?, @SerialName("receipt_mode") - val receiptMode: ReceiptMode + val receiptMode: ReceiptMode, + + @SerialName("public_keys") + val publicKeys: MLSPublicKeysDTO? = null ) { @Suppress("MagicNumber") @@ -102,6 +106,9 @@ data class ConversationResponse( fun fromId(id: Int): Type = values().first { type -> type.id == id } } } + + fun toV6(): ConversationResponseV6 = + ConversationResponseV6(this, publicKeys ?: MLSPublicKeysDTO(null)) } @Serializable @@ -145,13 +152,24 @@ data class ConversationResponseV3( @SerialName("access") val access: Set, + @SerialName("access_role") + val accessRole: Set?, + @SerialName("access_role_v2") - val accessRole: Set = ConversationAccessRoleDTO.DEFAULT_VALUE_WHEN_NULL, + val accessRoleV2: Set?, @SerialName("receipt_mode") val receiptMode: ReceiptMode, ) +@Serializable +data class ConversationResponseV6( + @SerialName("conversation") + val conversation: ConversationResponse, + @SerialName("public_keys") + val publicKeys: MLSPublicKeysDTO +) + @Serializable data class ConversationMembersResponse( @SerialName("self") diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt index b704f947d78..1f29d78dead 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventContentDTO.kt @@ -39,6 +39,7 @@ import com.wire.kalium.network.api.authenticated.notification.conversation.Messa import com.wire.kalium.network.api.authenticated.notification.team.TeamMemberIdData import com.wire.kalium.network.api.authenticated.notification.user.RemoveClientEventData import com.wire.kalium.network.api.authenticated.notification.user.UserUpdateEventData +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.model.ConversationId import com.wire.kalium.network.api.model.TeamId import com.wire.kalium.network.api.model.UserId @@ -51,7 +52,6 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException -import kotlinx.serialization.Serializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor @@ -62,9 +62,12 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.decodeStructure import kotlinx.serialization.encoding.encodeStructure +import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonTransformingSerializer +import kotlinx.serialization.json.int import kotlinx.serialization.json.jsonObject import kotlin.jvm.JvmInline @@ -206,7 +209,9 @@ sealed class EventContentDTO { data class NewMessageDTO( @SerialName("qualified_conversation") val qualifiedConversation: ConversationId, @SerialName("qualified_from") val qualifiedFrom: UserId, + @SerialName("conversation") val conversation: String? = null, @SerialName("time") val time: Instant, + @SerialName("from") val from: String? = null, @SerialName("data") val data: MessageEventData, ) : Conversation() @@ -389,7 +394,6 @@ sealed class EventContentDTO { data class PropertiesDeleteDTO( @SerialName("key") val key: String, ) : UserProperty() - } @Serializable(with = FieldKeyValueDeserializer::class) @@ -403,33 +407,57 @@ sealed class EventContentDTO { @JvmInline value class FieldUnknownValue(val value: String) : FieldKeyValue + @Serializable + @JvmInline + value class FieldLabelListValue(val value: LabelListResponseDTO) : FieldKeyValue + @Serializable @SerialName("unknown") - data class Unknown( - val type: String - ) : EventContentDTO() + data class Unknown(val type: String) : EventContentDTO() } @OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -@Serializer(EventContentDTO.FieldKeyValue::class) object FieldKeyValueDeserializer : KSerializer { override val descriptor = buildSerialDescriptor("value", PolymorphicKind.SEALED) override fun serialize(encoder: Encoder, value: EventContentDTO.FieldKeyValue) { when (value) { is EventContentDTO.FieldKeyNumberValue -> encoder.encodeInt(value.value) + is EventContentDTO.FieldLabelListValue -> encoder.encodeSerializableValue( + EventContentDTO.FieldLabelListValue.serializer(), + value + ) + is EventContentDTO.FieldUnknownValue -> throw SerializationException("Not handled yet") } } - @Suppress("TooGenericExceptionCaught") + @Suppress("TooGenericExceptionCaught", "ReturnCount") override fun deserialize(decoder: Decoder): EventContentDTO.FieldKeyValue { - return try { - EventContentDTO.FieldKeyNumberValue(decoder.decodeInt()) + try { + val input = decoder as? JsonDecoder ?: throw SerializationException("Expected JsonDecoder") + return when (val element = input.decodeJsonElement()) { + is JsonPrimitive -> { + if (element.isString) { + EventContentDTO.FieldUnknownValue(element.content) + } else { + EventContentDTO.FieldKeyNumberValue(element.int) + } + } + + is JsonObject -> { + if (element.containsKey("labels")) { + return input.json.decodeFromJsonElement(EventContentDTO.FieldLabelListValue.serializer(), element) + } + EventContentDTO.FieldUnknownValue(element.toString()) + } + + else -> throw SerializationException("Unexpected JSON element type: ${element::class.simpleName}") + } } catch (exception: Exception) { val jsonElement = decoder.toJsonElement().toString() kaliumUtilLogger.d("Error deserializing 'user.properties-set', prop: $jsonElement") kaliumUtilLogger.w("Error deserializing 'user.properties-set', error: $exception") - EventContentDTO.FieldUnknownValue(jsonElement) + return EventContentDTO.FieldUnknownValue(jsonElement) } } } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt index 45c3b2c7f03..819b964aed3 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/notification/EventSerialization.kt @@ -25,9 +25,9 @@ import kotlinx.serialization.modules.polymorphic internal val eventSerializationModule = SerializersModule { polymorphic(EventContentDTO::class) { polymorphic(FeatureConfigData::class) { - default { FeatureConfigData.Unknown.serializer() } + defaultDeserializer { FeatureConfigData.Unknown.serializer() } } - default { EventContentDTO.Unknown.serializer() } + defaultDeserializer { EventContentDTO.Unknown.serializer() } } } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt new file mode 100644 index 00000000000..b5d15a9782f --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/LabelDTO.kt @@ -0,0 +1,61 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.authenticated.properties + +import com.wire.kalium.network.api.model.QualifiedID +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable +data class LabelListResponseDTO( + @SerialName("labels") val labels: List +) + +@Serializable +data class LabelDTO( + @SerialName("id") val id: String, + @SerialName("name") val name: String, + @Serializable(with = LabelTypeSerializer::class) + @SerialName("type") val type: LabelTypeDTO, + @Deprecated("Use qualifiedConversations instead") + @SerialName("conversations") val conversations: List, + @SerialName("qualified_conversations") val qualifiedConversations: List? +) + +enum class LabelTypeDTO { + USER, + FAVORITE +} + +object LabelTypeSerializer : KSerializer { + override val descriptor = PrimitiveSerialDescriptor("type", PrimitiveKind.INT) + + override fun serialize(encoder: Encoder, value: LabelTypeDTO) { + encoder.encodeInt(value.ordinal) + } + + override fun deserialize(decoder: Decoder): LabelTypeDTO { + val ordinal = decoder.decodeInt() + return LabelTypeDTO.entries.getOrElse(ordinal) { LabelTypeDTO.USER } + } +} diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt index 7e20e0b97e3..c532b1a8aad 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/properties/PropertyKey.kt @@ -19,6 +19,7 @@ package com.wire.kalium.network.api.authenticated.properties enum class PropertyKey(val key: String) { WIRE_RECEIPT_MODE("WIRE_RECEIPT_MODE"), - WIRE_TYPING_INDICATOR_MODE("WIRE_TYPING_INDICATOR_MODE") + WIRE_TYPING_INDICATOR_MODE("WIRE_TYPING_INDICATOR_MODE"), + WIRE_LABELS("labels"), // TODO map other event like -ie. 'labels'- } diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/user/CreateUserTeamDTO.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/user/CreateUserTeamDTO.kt new file mode 100644 index 00000000000..9a0cf6cad12 --- /dev/null +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/authenticated/user/CreateUserTeamDTO.kt @@ -0,0 +1,27 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.authenticated.user + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateUserTeamDTO( + @SerialName("team_id") val teamId: String, + @SerialName("team_name") val teamName: String, +) diff --git a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/model/ApiModelMapper.kt b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/model/ApiModelMapper.kt index f2bead843fc..46e7790bf67 100644 --- a/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/model/ApiModelMapper.kt +++ b/network-model/src/commonMain/kotlin/com/wire/kalium/network/api/model/ApiModelMapper.kt @@ -20,6 +20,7 @@ package com.wire.kalium.network.api.model import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV3 +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV6 import com.wire.kalium.network.api.authenticated.conversation.CreateConversationRequest import com.wire.kalium.network.api.authenticated.conversation.CreateConversationRequestV3 import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationAccessRequest @@ -33,6 +34,7 @@ interface ApiModelMapper { fun toApiV3(request: CreateConversationRequest): CreateConversationRequestV3 fun toApiV3(request: UpdateConversationAccessRequest): UpdateConversationAccessRequestV3 fun fromApiV3(response: ConversationResponseV3): ConversationResponse + fun fromApiV6(response: ConversationResponseV6): ConversationResponse } class ApiModelMapperImpl : ApiModelMapper { @@ -72,8 +74,27 @@ class ApiModelMapperImpl : ApiModelMapper { response.lastEventTime, response.mlsCipherSuiteTag, response.access, - response.accessRole, + accessRole = response.accessRole ?: response.accessRoleV2 ?: ConversationAccessRoleDTO.DEFAULT_VALUE_WHEN_NULL, response.receiptMode ) + override fun fromApiV6(response: ConversationResponseV6): ConversationResponse = + ConversationResponse( + creator = response.conversation.creator, + members = response.conversation.members, + name = response.conversation.name, + id = response.conversation.id, + groupId = response.conversation.groupId, + epoch = response.conversation.epoch, + type = response.conversation.type, + messageTimer = response.conversation.messageTimer, + teamId = response.conversation.teamId, + protocol = response.conversation.protocol, + lastEventTime = response.conversation.lastEventTime, + mlsCipherSuiteTag = response.conversation.mlsCipherSuiteTag, + access = response.conversation.access, + accessRole = response.conversation.accessRole, + receiptMode = response.conversation.receiptMode, + publicKeys = response.publicKeys + ) } diff --git a/network-util/src/commonMain/kotlin/com/wire/kalium/network/NetworkStateObserver.kt b/network-util/src/commonMain/kotlin/com/wire/kalium/network/NetworkStateObserver.kt index e5f2a0b4056..437fcd9d7be 100644 --- a/network-util/src/commonMain/kotlin/com/wire/kalium/network/NetworkStateObserver.kt +++ b/network-util/src/commonMain/kotlin/com/wire/kalium/network/NetworkStateObserver.kt @@ -31,7 +31,7 @@ interface NetworkStateObserver { // Delay which will be completed earlier if there is a reconnection in the meantime. suspend fun delayUntilConnectedWithInternetAgain(delay: Duration) { // Delay for given amount but break it if reconnected again. - kaliumUtilLogger.i("$TAG delayUntilConnectedWithInternetAgain") + kaliumUtilLogger.i("$TAG delayUntilConnectedWithInternetAgain for $delay") withTimeoutOrNull(delay) { // Drop the current value, so it will complete only if the connection changed again to connected during that time. observeNetworkState() diff --git a/network/build.gradle.kts b/network/build.gradle.kts index 7959cb38cda..fce0b545667 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -111,3 +111,37 @@ android { it.enabled = false } } + +tasks.register("generateNewApiVersion") { + group = "custom" + description = "Generates a new API version by calling the generate_new_api_version.sh script" + + val previousApiVersion = project.findProperty("previousApiVersion") as String? ?: "" + val currentApiVersion = project.findProperty("currentApiVersion") as String? ?: "" + val newApiVersion = project.findProperty("newApiVersion") as String? ?: "" + + doFirst { + if (previousApiVersion == "" || currentApiVersion == "" || newApiVersion == "") { + println( + "Usage: ./gradlew :network:generateNewApiVersion " + + "-PpreviousApiVersion= -PcurrentApiVersion= -PnewApiVersion=" + ) + println( + "Example: ./gradlew :network:generateNewApiVersion " + + "-PpreviousApiVersion=5 -PcurrentApiVersion=6 -PnewApiVersion=7" + ) + throw IllegalArgumentException( + "All parameters (previousApiVersion, " + + "currentApiVersion, newApiVersion) must be provided." + ) + } + } + + commandLine( + "bash", + "./../scripts/generate_new_api_version.sh", + previousApiVersion, + currentApiVersion, + newApiVersion + ) +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt index 1053fd2ecdc..be435578cf3 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt @@ -15,17 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ - -@file:Suppress("MagicNumber") - package com.wire.kalium.network import com.wire.kalium.network.api.unbound.configuration.ApiVersionDTO import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.unbound.versioning.VersionInfoDTO -val SupportedApiVersions = setOf(0, 1, 2, 4, 5) -val DevelopmentApiVersions = setOf(6) +// They are not truly constants as set is not a primitive type, yet are treated as one in this context +@Suppress("MagicNumber") +val SupportedApiVersions = setOf(0, 1, 2, 4, 5, 6) + +// They are not truly constants as set is not a primitive type, yet are treated as one in this context +@Suppress("MagicNumber") +val DevelopmentApiVersions = setOf(7) + +// You can use scripts/generate_new_api_version.sh or gradle task network:generateNewApiVersion to +// bump API version and generate all needed classes interface BackendMetaDataUtil { fun calculateApiVersion( @@ -45,8 +50,13 @@ object BackendMetaDataUtilImpl : BackendMetaDataUtil { developmentApiEnabled: Boolean ): ServerConfigDTO.MetaData { - val allSupportedApiVersions = if (developmentApiEnabled) supportedApiVersions + developmentApiVersions else supportedApiVersions - val apiVersion = commonApiVersion(versionInfoDTO, allSupportedApiVersions, developmentApiEnabled)?.let { maxCommonVersion -> + val allSupportedApiVersions = + if (developmentApiEnabled) supportedApiVersions + developmentApiVersions else supportedApiVersions + val apiVersion = commonApiVersion( + versionInfoDTO, + allSupportedApiVersions, + developmentApiEnabled + )?.let { maxCommonVersion -> ApiVersionDTO.Valid(maxCommonVersion) } ?: run { handleNoCommonVersion(versionInfoDTO.supported, allSupportedApiVersions) @@ -59,16 +69,24 @@ object BackendMetaDataUtilImpl : BackendMetaDataUtil { ) } - private fun commonApiVersion(serverVersion: VersionInfoDTO, supportedApiVersions: Set, developmentAPIEnabled: Boolean): Int? { - val serverSupportedApiVersions: List = if (developmentAPIEnabled && serverVersion.developmentSupported != null) { - serverVersion.supported + serverVersion.developmentSupported!! - } else { - serverVersion.supported - } + private fun commonApiVersion( + serverVersion: VersionInfoDTO, + supportedApiVersions: Set, + developmentAPIEnabled: Boolean + ): Int? { + val serverSupportedApiVersions: List = + if (developmentAPIEnabled && serverVersion.developmentSupported != null) { + serverVersion.supported + serverVersion.developmentSupported!! + } else { + serverVersion.supported + } return serverSupportedApiVersions.intersect(supportedApiVersions).maxOrNull() } - private fun handleNoCommonVersion(serverVersion: List, appVersion: Set): ApiVersionDTO.Invalid { + private fun handleNoCommonVersion( + serverVersion: List, + appVersion: Set + ): ApiVersionDTO.Invalid { return serverVersion.maxOrNull()?.let { maxBEVersion -> appVersion.maxOrNull()?.let { maxAppVersion -> if (maxBEVersion > maxAppVersion) ApiVersionDTO.Invalid.New diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumHttpLogger.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumHttpLogger.kt index e4593409cf7..e42ee3431d5 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumHttpLogger.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumHttpLogger.kt @@ -24,7 +24,6 @@ import com.wire.kalium.network.utils.obfuscatePath import com.wire.kalium.network.utils.obfuscatedJsonMessage import com.wire.kalium.util.serialization.toJsonElement import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger import io.ktor.client.request.HttpRequest import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.statement.HttpResponse @@ -41,7 +40,6 @@ import kotlinx.coroutines.Job internal class KaliumHttpLogger( private val level: LogLevel, - private val logger: Logger, private val kaliumLogger: KaliumLogger, ) { private val requestLog = mutableMapOf() @@ -134,7 +132,7 @@ internal class KaliumHttpLogger( } } - suspend fun logResponseBody(contentType: ContentType?, content: ByteReadChannel): Unit = with(logger) { + suspend fun logResponseBody(contentType: ContentType?, content: ByteReadChannel) { responseHeaderMonitor.join() val text = content.tryReadText(contentType?.charset() ?: Charsets.UTF_8) ?: "\"response body omitted\"" diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumKtorCustomLogging.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumKtorCustomLogging.kt index 30a88e2b376..36681582bca 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumKtorCustomLogging.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/KaliumKtorCustomLogging.kt @@ -49,17 +49,17 @@ private val DisableLogging = AttributeKey("DisableLogging") * A client's logging plugin. */ @Suppress("TooGenericExceptionCaught", "EmptyFinallyBlock") -public class KaliumKtorCustomLogging private constructor( - public val logger: Logger, - public val kaliumLogger: KaliumLogger, - public var level: LogLevel, - public var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList() +class KaliumKtorCustomLogging private constructor( + val logger: Logger, + val kaliumLogger: KaliumLogger, + var level: LogLevel, + var filters: List<(HttpRequestBuilder) -> Boolean> = emptyList() ) { /** * [Logging] plugin configuration */ - public class Config { + class Config { /** * filters */ @@ -70,7 +70,7 @@ public class KaliumKtorCustomLogging private constructor( /** * [Logger] instance to use */ - public var logger: Logger + var logger: Logger get() = _logger ?: Logger.DEFAULT set(value) { _logger = value @@ -79,7 +79,7 @@ public class KaliumKtorCustomLogging private constructor( /** * log [LogLevel] */ - public var level: LogLevel = LogLevel.HEADERS + var level: LogLevel = LogLevel.HEADERS /** * [KaliumLogger] instance to use @@ -89,7 +89,7 @@ public class KaliumKtorCustomLogging private constructor( /** * Log messages for calls matching a [predicate] */ - public fun filter(predicate: (HttpRequestBuilder) -> Boolean) { + fun filter(predicate: (HttpRequestBuilder) -> Boolean) { filters.add(predicate) } } @@ -171,7 +171,7 @@ public class KaliumKtorCustomLogging private constructor( } private fun logRequest(request: HttpRequestBuilder): OutgoingContent? { - val logger = KaliumHttpLogger(level, logger, kaliumLogger) + val logger = KaliumHttpLogger(level, kaliumLogger) request.attributes.put(KaliumHttpCustomLogger, logger) logger.logRequest(request) @@ -181,7 +181,7 @@ public class KaliumKtorCustomLogging private constructor( return null } - public companion object : HttpClientPlugin { + companion object : HttpClientPlugin { override val key: AttributeKey = AttributeKey("ClientLogging") override fun prepare(block: Config.() -> Unit): KaliumKtorCustomLogging { @@ -214,7 +214,7 @@ public class KaliumKtorCustomLogging private constructor( * Configure and install [Logging] in [HttpClient]. */ @Suppress("FunctionNaming") -public fun HttpClientConfig<*>.Logging(block: Logging.Config.() -> Unit = {}) { +fun HttpClientConfig<*>.Logging(block: Logging.Config.() -> Unit = {}) { install(Logging, block) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/NetworkClient.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/NetworkClient.kt index 2f8bca60917..a48e4e6b262 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/NetworkClient.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/NetworkClient.kt @@ -36,7 +36,10 @@ import io.ktor.client.plugins.compression.ContentEncoding import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.websocket.WebSockets +import io.ktor.client.plugins.websocket.webSocketSession +import io.ktor.client.request.HttpRequestBuilder import io.ktor.serialization.kotlinx.json.json +import io.ktor.websocket.WebSocketSession /** * Provides a [HttpClient] that has all the @@ -102,6 +105,7 @@ internal class AuthenticatedWebSocketClient( private val bearerAuthProvider: BearerAuthProvider, private val serverConfigDTO: ServerConfigDTO, private val kaliumLogger: KaliumLogger, + private val webSocketSessionProvider: ((HttpClient, String) -> WebSocketSession)? = null ) { /** * Creates a disposable [HttpClient] for a single use. @@ -123,6 +127,13 @@ internal class AuthenticatedWebSocketClient( pingInterval = WEBSOCKET_PING_INTERVAL_MILLIS } } + + suspend fun createWebSocketSession(clientId: String, block: HttpRequestBuilder.() -> Unit): WebSocketSession { + val client = createDisposableHttpClient() + return webSocketSessionProvider?.let { + return it(client, clientId) + } ?: client.webSocketSession(block) + } } internal fun provideBaseHttpClient( diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/UpgradePersonalToTeamApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/UpgradePersonalToTeamApi.kt new file mode 100644 index 00000000000..611e6efe256 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/UpgradePersonalToTeamApi.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.base.authenticated + +import com.wire.kalium.network.api.authenticated.user.CreateUserTeamDTO +import com.wire.kalium.network.utils.NetworkResponse + +interface UpgradePersonalToTeamApi : BaseApi { + + suspend fun migrateToTeam(teamName: String): NetworkResponse + + companion object { + const val MIN_API_VERSION = 7 + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/WildCardApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/WildCardApi.kt new file mode 100644 index 00000000000..8840ab6cfac --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/WildCardApi.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.base.authenticated + +import com.wire.kalium.network.utils.NetworkResponse +import io.ktor.http.HttpMethod + +interface WildCardApi { + suspend fun customRequest( + httpMethod: HttpMethod, + requestPath: List, + body: String?, + queryParam: Map, + customHeader: Map + ): NetworkResponse +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationApi.kt index 28fd67cb099..22bc70f3a0d 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/conversation/ConversationApi.kt @@ -18,23 +18,12 @@ package com.wire.kalium.network.api.base.authenticated.conversation -import com.wire.kalium.network.api.base.authenticated.BaseApi import com.wire.kalium.network.api.authenticated.conversation.AddConversationMembersRequest -import com.wire.kalium.network.api.authenticated.conversation.guestroomlink.ConversationInviteLinkResponse -import com.wire.kalium.network.api.authenticated.conversation.model.ConversationCodeInfo -import com.wire.kalium.network.api.authenticated.conversation.model.ConversationMemberRoleDTO -import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO -import com.wire.kalium.network.api.authenticated.notification.EventContentDTO -import com.wire.kalium.network.api.model.ConversationId -import com.wire.kalium.network.api.model.QualifiedID -import com.wire.kalium.network.api.model.ServiceAddedResponse import com.wire.kalium.network.api.authenticated.conversation.AddServiceRequest import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberAddedResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberRemovedResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationPagingResponse -import com.wire.kalium.network.api.model.SubconversationId -import com.wire.kalium.network.api.model.UserId import com.wire.kalium.network.api.authenticated.conversation.ConversationRenameResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseDTO @@ -47,6 +36,17 @@ import com.wire.kalium.network.api.authenticated.conversation.UpdateConversation import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationAccessResponse import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationProtocolResponse import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationReceiptModeResponse +import com.wire.kalium.network.api.authenticated.conversation.guestroomlink.ConversationInviteLinkResponse +import com.wire.kalium.network.api.authenticated.conversation.model.ConversationCodeInfo +import com.wire.kalium.network.api.authenticated.conversation.model.ConversationMemberRoleDTO +import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO +import com.wire.kalium.network.api.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.BaseApi +import com.wire.kalium.network.api.model.ConversationId +import com.wire.kalium.network.api.model.QualifiedID +import com.wire.kalium.network.api.model.ServiceAddedResponse +import com.wire.kalium.network.api.model.SubconversationId +import com.wire.kalium.network.api.model.UserId import com.wire.kalium.network.exceptions.APINotSupported import com.wire.kalium.network.utils.NetworkResponse @@ -75,10 +75,6 @@ interface ConversationApi : BaseApi { createConversationRequest: CreateConversationRequest ): NetworkResponse - suspend fun createOne2OneConversation( - createConversationRequest: CreateConversationRequest - ): NetworkResponse - suspend fun addMember( addParticipantRequest: AddConversationMembersRequest, conversationId: ConversationId diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/NotificationApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/NotificationApi.kt index 51bd9c57bff..35586b6ab6c 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/NotificationApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/notification/NotificationApi.kt @@ -35,9 +35,7 @@ sealed class WebSocketEvent { other as NonBinaryPayloadReceived<*> - if (!payload.contentEquals(other.payload)) return false - - return true + return payload.contentEquals(other.payload) } override fun hashCode(): Int { @@ -60,6 +58,7 @@ interface NotificationApi { */ suspend fun getAllNotifications(querySize: Int, queryClient: String): NetworkResponse + suspend fun getServerTime(querySize: Int): NetworkResponse suspend fun listenToLiveEvents(clientId: String): NetworkResponse>> } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt index c4760248b55..816278bbcda 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/authenticated/properties/PropertiesApi.kt @@ -18,6 +18,7 @@ package com.wire.kalium.network.api.base.authenticated.properties +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.authenticated.properties.PropertyKey import com.wire.kalium.network.utils.NetworkResponse @@ -25,5 +26,7 @@ interface PropertiesApi { suspend fun setProperty(propertyKey: PropertyKey, propertyValue: Any): NetworkResponse suspend fun deleteProperty(propertyKey: PropertyKey): NetworkResponse + suspend fun getLabels(): NetworkResponse + suspend fun updateLabels(labelList: LabelListResponseDTO): NetworkResponse } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt index 9d84bb4a61e..db159b5703c 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt @@ -44,7 +44,6 @@ import io.ktor.client.request.setBody import io.ktor.client.statement.HttpResponse import io.ktor.http.ContentType import io.ktor.http.URLBuilder -import io.ktor.http.URLProtocol import io.ktor.http.Url import io.ktor.http.contentType import io.ktor.http.isSuccess @@ -269,8 +268,11 @@ class ACMEApiImpl internal constructor( } return wrapKaliumResponse { - val httpUrl = if (proxyUrl.isNullOrEmpty()) URLBuilder(url).apply { this.protocol = URLProtocol.HTTP }.build() - else URLBuilder(proxyUrl).apply { this.pathSegments = this.pathSegments.plus(url) }.build() + val crlUrlBuilder: URLBuilder = URLBuilder(url) + val proxyUrlBuilder: URLBuilder? = if (proxyUrl.isNullOrEmpty()) null else URLBuilder(proxyUrl) + + val httpUrl = proxyUrlBuilder?.apply { this.pathSegments += crlUrlBuilder.host }?.build() + ?: crlUrlBuilder.build() clearTextTrafficHttpClient.get(httpUrl) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/ConversationApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/ConversationApiV0.kt index 5d2f3963694..145a833620f 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/ConversationApiV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/ConversationApiV0.kt @@ -22,7 +22,6 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.authenticated.conversation.AddConversationMembersRequest import com.wire.kalium.network.api.authenticated.conversation.AddServiceRequest import com.wire.kalium.network.api.authenticated.conversation.ConvProtocol -import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberAddedResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationMemberRemovedResponse import com.wire.kalium.network.api.authenticated.conversation.ConversationPagingResponse @@ -46,6 +45,7 @@ import com.wire.kalium.network.api.authenticated.conversation.model.Conversation import com.wire.kalium.network.api.authenticated.conversation.model.ConversationMemberRoleDTO import com.wire.kalium.network.api.authenticated.conversation.model.ConversationReceiptModeDTO import com.wire.kalium.network.api.authenticated.notification.EventContentDTO +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi import com.wire.kalium.network.api.model.AddServiceResponse import com.wire.kalium.network.api.model.ConversationId import com.wire.kalium.network.api.model.JoinConversationRequestV0 @@ -115,14 +115,6 @@ internal open class ConversationApiV0 internal constructor( } } - override suspend fun createOne2OneConversation( - createConversationRequest: CreateConversationRequest - ): NetworkResponse = wrapKaliumResponse { - httpClient.post("$PATH_CONVERSATIONS/$PATH_ONE_2_ONE") { - setBody(createConversationRequest) - } - } - /** * returns 200 conversation created or 204 conversation unchanged */ @@ -407,7 +399,6 @@ internal open class ConversationApiV0 internal constructor( const val PATH_CONVERSATIONS = "conversations" const val PATH_SELF = "self" const val PATH_MEMBERS = "members" - const val PATH_ONE_2_ONE = "one2one" const val PATH_V2 = "v2" const val PATH_CONVERSATIONS_LIST = "list" const val PATH_LIST_IDS = "list-ids" diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/NotificationApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/NotificationApiV0.kt index ca0e2a29633..e130b55527a 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/NotificationApiV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/NotificationApiV0.kt @@ -43,13 +43,12 @@ import com.wire.kalium.network.utils.deleteSensitiveItemsFromJson import com.wire.kalium.network.utils.mapSuccess import com.wire.kalium.network.utils.setWSSUrl import com.wire.kalium.network.utils.wrapKaliumResponse -import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession -import io.ktor.client.plugins.websocket.webSocket import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.http.HttpStatusCode import io.ktor.http.Url import io.ktor.websocket.Frame +import io.ktor.websocket.WebSocketSession import io.ktor.websocket.close import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.FlowCollector @@ -90,9 +89,12 @@ internal open class NotificationApiV0 internal constructor( override suspend fun getAllNotifications(querySize: Int, queryClient: String): NetworkResponse = notificationsCall(querySize = querySize, queryClient = queryClient, querySince = null) + override suspend fun getServerTime(querySize: Int): NetworkResponse = + notificationsCall(querySize = querySize, queryClient = null, querySince = null).mapSuccess { it.time } + protected open suspend fun notificationsCall( querySize: Int, - queryClient: String, + queryClient: String?, querySince: String? ): NetworkResponse { return wrapKaliumResponse({ @@ -104,7 +106,7 @@ internal open class NotificationApiV0 internal constructor( }) { httpClient.get(PATH_NOTIFICATIONS) { parameter(SIZE_QUERY_KEY, querySize) - parameter(CLIENT_QUERY_KEY, queryClient) + queryClient?.let { parameter(CLIENT_QUERY_KEY, it) } querySince?.let { parameter(SINCE_QUERY_KEY, it) } } } @@ -118,19 +120,16 @@ internal open class NotificationApiV0 internal constructor( // exceptions when the backend returns 401 instead of triggering a token refresh. // This call to lastNotification will make sure that if the token is expired, it will be refreshed // before attempting to open the websocket - authenticatedWebSocketClient - .createDisposableHttpClient() - .webSocket({ - setWSSUrl(Url(serverLinks.webSocket), PATH_AWAIT) - parameter(CLIENT_QUERY_KEY, clientId) - }) { - emitWebSocketEvents(this) - } + val webSocketSession = authenticatedWebSocketClient.createWebSocketSession(clientId) { + setWSSUrl(Url(serverLinks.webSocket), PATH_AWAIT) + parameter(CLIENT_QUERY_KEY, clientId) + } + emitWebSocketEvents(webSocketSession) } } private suspend fun FlowCollector>.emitWebSocketEvents( - defaultClientWebSocketSession: DefaultClientWebSocketSession + defaultClientWebSocketSession: WebSocketSession ) { val logger = kaliumLogger.withFeatureId(EVENT_RECEIVER) logger.i("Websocket open") diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt index 4672a33e35a..5601a5fa3c6 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/PropertiesApiV0.kt @@ -19,11 +19,13 @@ package com.wire.kalium.network.api.v0.authenticated import com.wire.kalium.network.AuthenticatedNetworkClient -import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.api.authenticated.properties.LabelListResponseDTO import com.wire.kalium.network.api.authenticated.properties.PropertyKey +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.wrapKaliumResponse import io.ktor.client.request.delete +import io.ktor.client.request.get import io.ktor.client.request.put import io.ktor.client.request.setBody @@ -35,6 +37,7 @@ internal open class PropertiesApiV0 internal constructor( private companion object { const val PATH_PROPERTIES = "properties" + const val PATH_LABELS = "labels" } override suspend fun setProperty(propertyKey: PropertyKey, propertyValue: Any): NetworkResponse = @@ -46,4 +49,11 @@ internal open class PropertiesApiV0 internal constructor( httpClient.delete("$PATH_PROPERTIES/${propertyKey.key}") } + override suspend fun getLabels(): NetworkResponse = wrapKaliumResponse { + httpClient.get("$PATH_PROPERTIES/$PATH_LABELS") + } + + override suspend fun updateLabels(labelList: LabelListResponseDTO): NetworkResponse = wrapKaliumResponse { + httpClient.put("$PATH_PROPERTIES/$PATH_LABELS") { setBody(labelList) } + } } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/UpgradePersonalToTeamApiV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/UpgradePersonalToTeamApiV0.kt new file mode 100644 index 00000000000..93a28c77bfe --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/UpgradePersonalToTeamApiV0.kt @@ -0,0 +1,33 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v0.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.authenticated.user.CreateUserTeamDTO +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.utils.NetworkResponse + +internal open class UpgradePersonalToTeamApiV0 internal constructor( + private val authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApi { + + internal val httpClient get() = authenticatedNetworkClient.httpClient + override suspend fun migrateToTeam(teamName: String): NetworkResponse = + getApiNotSupportedError(::migrateToTeam.name, UpgradePersonalToTeamApi.MIN_API_VERSION) +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/networkContainer/AuthenticatedNetworkContainerV0.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/networkContainer/AuthenticatedNetworkContainerV0.kt index 9063c6d9a92..a122fb77b92 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/networkContainer/AuthenticatedNetworkContainerV0.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v0/authenticated/networkContainer/AuthenticatedNetworkContainerV0.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -58,8 +60,10 @@ import com.wire.kalium.network.api.v0.authenticated.PreKeyApiV0 import com.wire.kalium.network.api.v0.authenticated.PropertiesApiV0 import com.wire.kalium.network.api.v0.authenticated.SelfApiV0 import com.wire.kalium.network.api.v0.authenticated.TeamsApiV0 +import com.wire.kalium.network.api.v0.authenticated.UpgradePersonalToTeamApiV0 import com.wire.kalium.network.api.v0.authenticated.UserDetailsApiV0 import com.wire.kalium.network.api.v0.authenticated.UserSearchApiV0 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -67,23 +71,30 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession internal class AuthenticatedNetworkContainerV0 internal constructor( private val sessionManager: SessionManager, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, proxyCredentials = sessionManager.proxyCredentials(), certificatePinning = certificatePinning - ) + ), ) : AuthenticatedNetworkContainer, AuthenticatedHttpClientProvider by AuthenticatedHttpClientProviderImpl( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV0(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV0(networkClient.httpClient) @@ -125,4 +136,11 @@ internal class AuthenticatedNetworkContainerV0 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV0() override val propertiesApi: PropertiesApi get() = PropertiesApiV0(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV0( + networkClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PreKeyApiV2.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PreKeyApiV2.kt index e643102c839..a1d8b277740 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PreKeyApiV2.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PreKeyApiV2.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v0.authenticated.PreKeyApiV0 internal open class PreKeyApiV2 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient + authenticatedNetworkClient: AuthenticatedNetworkClient ) : PreKeyApiV0(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PropertiesApiV2.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PropertiesApiV2.kt index 9abc1d89dbb..107d524c138 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PropertiesApiV2.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/PropertiesApiV2.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v0.authenticated.PropertiesApiV0 internal open class PropertiesApiV2 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, ) : PropertiesApiV0(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/UpgradePersonalToTeamApiV2.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/UpgradePersonalToTeamApiV2.kt new file mode 100644 index 00000000000..40d87735978 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/UpgradePersonalToTeamApiV2.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v2.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v0.authenticated.UpgradePersonalToTeamApiV0 + +internal open class UpgradePersonalToTeamApiV2 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV0(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/networkContainer/AuthenticatedNetworkContainerV2.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/networkContainer/AuthenticatedNetworkContainerV2.kt index 0f38f015a39..e5878c1ebfa 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/networkContainer/AuthenticatedNetworkContainerV2.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v2/authenticated/networkContainer/AuthenticatedNetworkContainerV2.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -59,8 +61,10 @@ import com.wire.kalium.network.api.v2.authenticated.PreKeyApiV2 import com.wire.kalium.network.api.v2.authenticated.PropertiesApiV2 import com.wire.kalium.network.api.v2.authenticated.SelfApiV2 import com.wire.kalium.network.api.v2.authenticated.TeamsApiV2 +import com.wire.kalium.network.api.v2.authenticated.UpgradePersonalToTeamApiV2 import com.wire.kalium.network.api.v2.authenticated.UserDetailsApiV2 import com.wire.kalium.network.api.v2.authenticated.UserSearchApiV2 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -68,6 +72,7 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession @Suppress("LongParameterList") internal class AuthenticatedNetworkContainerV2 internal constructor( @@ -75,6 +80,7 @@ internal class AuthenticatedNetworkContainerV2 internal constructor( private val selfUserId: UserId, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, @@ -86,7 +92,12 @@ internal class AuthenticatedNetworkContainerV2 internal constructor( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV2(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV2(networkClient.httpClient) @@ -128,4 +139,11 @@ internal class AuthenticatedNetworkContainerV2 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV2() override val propertiesApi: PropertiesApi get() = PropertiesApiV2(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV2( + networkClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/ConversationApiV3.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/ConversationApiV3.kt index fda75d1c5ea..1e2c7052f33 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/ConversationApiV3.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/ConversationApiV3.kt @@ -20,7 +20,10 @@ package com.wire.kalium.network.api.v3.authenticated import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseDTO +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseDTOV3 import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV3 +import com.wire.kalium.network.api.authenticated.conversation.ConversationsDetailsRequest import com.wire.kalium.network.api.authenticated.conversation.CreateConversationRequest import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationAccessRequest import com.wire.kalium.network.api.authenticated.conversation.UpdateConversationAccessResponse @@ -44,6 +47,23 @@ internal open class ConversationApiV3 internal constructor( private val apiModelMapper: ApiModelMapper = ApiModelMapperImpl() ) : ConversationApiV2(authenticatedNetworkClient) { + override suspend fun fetchConversationsListDetails( + conversationsIds: List + ): NetworkResponse = + wrapKaliumResponse { + httpClient.post("$PATH_CONVERSATIONS/$PATH_CONVERSATIONS_LIST") { + setBody(ConversationsDetailsRequest(conversationsIds = conversationsIds)) + } + }.mapSuccess { + ConversationResponseDTO( + conversationsFound = it.conversationsFound.map { conversationFound -> + apiModelMapper.fromApiV3(conversationFound) + }, + conversationsNotFound = it.conversationsNotFound, + conversationsFailed = it.conversationsFailed + ) + } + /** * returns 201 when a new conversation is created or 200 if the conversation already existed */ @@ -57,16 +77,6 @@ internal open class ConversationApiV3 internal constructor( apiModelMapper.fromApiV3(it) } - override suspend fun createOne2OneConversation( - createConversationRequest: CreateConversationRequest - ): NetworkResponse = wrapKaliumResponse { - httpClient.post("$PATH_CONVERSATIONS/$PATH_ONE_2_ONE") { - setBody(apiModelMapper.toApiV3(createConversationRequest)) - } - }.mapSuccess { - apiModelMapper.fromApiV3(it) - } - override suspend fun updateAccess( conversationId: ConversationId, updateConversationAccessRequest: UpdateConversationAccessRequest diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/NotificationApiV3.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/NotificationApiV3.kt index 529d1520eb6..a4bf26d714e 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/NotificationApiV3.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/NotificationApiV3.kt @@ -38,13 +38,13 @@ internal open class NotificationApiV3 internal constructor( override suspend fun notificationsCall( querySize: Int, - queryClient: String, + queryClient: String?, querySince: String? ): NetworkResponse = wrapKaliumResponse { // Pretty much the same V0 request, but without the 404 overwrite httpClient.get(V0.PATH_NOTIFICATIONS) { parameter(V0.SIZE_QUERY_KEY, querySize) - parameter(V0.CLIENT_QUERY_KEY, queryClient) + queryClient?.let { parameter(V0.CLIENT_QUERY_KEY, it) } querySince?.let { parameter(V0.SINCE_QUERY_KEY, it) } } } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/PropertiesApiV3.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/PropertiesApiV3.kt index ebf2fbe0489..b77b93f3767 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/PropertiesApiV3.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/PropertiesApiV3.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v2.authenticated.PropertiesApiV2 internal open class PropertiesApiV3 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, ) : PropertiesApiV2(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/UpgradePersonalToTeamApiV3.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/UpgradePersonalToTeamApiV3.kt new file mode 100644 index 00000000000..a2502fea5a8 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/UpgradePersonalToTeamApiV3.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v3.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v2.authenticated.UpgradePersonalToTeamApiV2 + +internal open class UpgradePersonalToTeamApiV3 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV2(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/networkContainer/AuthenticatedNetworkContainerV3.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/networkContainer/AuthenticatedNetworkContainerV3.kt index bd11b2989b8..55278eb3477 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/networkContainer/AuthenticatedNetworkContainerV3.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v3/authenticated/networkContainer/AuthenticatedNetworkContainerV3.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -60,8 +62,10 @@ import com.wire.kalium.network.api.v3.authenticated.PreKeyApiV3 import com.wire.kalium.network.api.v3.authenticated.PropertiesApiV3 import com.wire.kalium.network.api.v3.authenticated.SelfApiV3 import com.wire.kalium.network.api.v3.authenticated.TeamsApiV3 +import com.wire.kalium.network.api.v3.authenticated.UpgradePersonalToTeamApiV3 import com.wire.kalium.network.api.v3.authenticated.UserDetailsApiV3 import com.wire.kalium.network.api.v3.authenticated.UserSearchApiV3 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -69,6 +73,7 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession @Suppress("LongParameterList") internal class AuthenticatedNetworkContainerV3 internal constructor( @@ -76,6 +81,7 @@ internal class AuthenticatedNetworkContainerV3 internal constructor( private val selfUserId: UserId, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, @@ -87,7 +93,12 @@ internal class AuthenticatedNetworkContainerV3 internal constructor( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV3(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV3(networkClient.httpClient) @@ -129,4 +140,11 @@ internal class AuthenticatedNetworkContainerV3 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV3() override val propertiesApi: PropertiesApi get() = PropertiesApiV3(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV3( + networkClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/AccessTokenApiV4.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/AccessTokenApiV4.kt index d600fb6f3fb..f72a7fc0c28 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/AccessTokenApiV4.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/AccessTokenApiV4.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.api.v3.authenticated.AccessTokenApiV3 import io.ktor.client.HttpClient internal open class AccessTokenApiV4 internal constructor( - private val httpClient: HttpClient + httpClient: HttpClient ) : AccessTokenApiV3(httpClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/NotificationApiV4.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/NotificationApiV4.kt index 708ab68f776..11b383fbd2c 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/NotificationApiV4.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/NotificationApiV4.kt @@ -24,7 +24,7 @@ import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.v3.authenticated.NotificationApiV3 internal open class NotificationApiV4 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, authenticatedWebSocketClient: AuthenticatedWebSocketClient, serverLinks: ServerConfigDTO.Links ) : NotificationApiV3(authenticatedNetworkClient, authenticatedWebSocketClient, serverLinks) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/PropertiesApiV4.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/PropertiesApiV4.kt index 2bfa3a5fa7c..9333d602a4f 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/PropertiesApiV4.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/PropertiesApiV4.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v3.authenticated.PropertiesApiV3 internal open class PropertiesApiV4 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, ) : PropertiesApiV3(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/UpgradePersonalToTeamApiV4.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/UpgradePersonalToTeamApiV4.kt new file mode 100644 index 00000000000..6bff4efb352 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/UpgradePersonalToTeamApiV4.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v4.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v3.authenticated.UpgradePersonalToTeamApiV3 + +internal open class UpgradePersonalToTeamApiV4 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV3(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/networkContainer/AuthenticatedNetworkContainerV4.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/networkContainer/AuthenticatedNetworkContainerV4.kt index 667195c6fce..0bdf244f595 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/networkContainer/AuthenticatedNetworkContainerV4.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v4/authenticated/networkContainer/AuthenticatedNetworkContainerV4.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -59,8 +61,10 @@ import com.wire.kalium.network.api.v4.authenticated.PreKeyApiV4 import com.wire.kalium.network.api.v4.authenticated.PropertiesApiV4 import com.wire.kalium.network.api.v4.authenticated.SelfApiV4 import com.wire.kalium.network.api.v4.authenticated.TeamsApiV4 +import com.wire.kalium.network.api.v4.authenticated.UpgradePersonalToTeamApiV4 import com.wire.kalium.network.api.v4.authenticated.UserDetailsApiV4 import com.wire.kalium.network.api.v4.authenticated.UserSearchApiV4 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -68,6 +72,7 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession @Suppress("LongParameterList") internal class AuthenticatedNetworkContainerV4 internal constructor( @@ -75,6 +80,7 @@ internal class AuthenticatedNetworkContainerV4 internal constructor( private val selfUserId: UserId, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, @@ -86,7 +92,12 @@ internal class AuthenticatedNetworkContainerV4 internal constructor( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV4(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV4(networkClient.httpClient) @@ -128,4 +139,11 @@ internal class AuthenticatedNetworkContainerV4 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV4() override val propertiesApi: PropertiesApi get() = PropertiesApiV4(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV4( + networkClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/AccessTokenApiV5.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/AccessTokenApiV5.kt index 6dc480fd107..927a1919dc1 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/AccessTokenApiV5.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/AccessTokenApiV5.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.api.v4.authenticated.AccessTokenApiV4 import io.ktor.client.HttpClient internal open class AccessTokenApiV5 internal constructor( - private val httpClient: HttpClient + httpClient: HttpClient ) : AccessTokenApiV4(httpClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/NotificationApiV5.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/NotificationApiV5.kt index cb50ef1396a..52e0e02168b 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/NotificationApiV5.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/NotificationApiV5.kt @@ -24,7 +24,7 @@ import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.v4.authenticated.NotificationApiV4 internal open class NotificationApiV5 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, authenticatedWebSocketClient: AuthenticatedWebSocketClient, serverLinks: ServerConfigDTO.Links ) : NotificationApiV4(authenticatedNetworkClient, authenticatedWebSocketClient, serverLinks) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/UpgradePersonalToTeamApiV5.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/UpgradePersonalToTeamApiV5.kt new file mode 100644 index 00000000000..98428ed0f45 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/UpgradePersonalToTeamApiV5.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v5.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v4.authenticated.UpgradePersonalToTeamApiV4 + +internal open class UpgradePersonalToTeamApiV5 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV4(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/networkContainer/AuthenticatedNetworkContainerV5.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/networkContainer/AuthenticatedNetworkContainerV5.kt index b8a59f17e62..d010a486f99 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/networkContainer/AuthenticatedNetworkContainerV5.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v5/authenticated/networkContainer/AuthenticatedNetworkContainerV5.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -59,8 +61,10 @@ import com.wire.kalium.network.api.v5.authenticated.PreKeyApiV5 import com.wire.kalium.network.api.v5.authenticated.PropertiesApiV5 import com.wire.kalium.network.api.v5.authenticated.SelfApiV5 import com.wire.kalium.network.api.v5.authenticated.TeamsApiV5 +import com.wire.kalium.network.api.v5.authenticated.UpgradePersonalToTeamApiV5 import com.wire.kalium.network.api.v5.authenticated.UserDetailsApiV5 import com.wire.kalium.network.api.v5.authenticated.UserSearchApiV5 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -68,6 +72,7 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession @Suppress("LongParameterList") internal class AuthenticatedNetworkContainerV5 internal constructor( @@ -75,6 +80,7 @@ internal class AuthenticatedNetworkContainerV5 internal constructor( private val selfUserId: UserId, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, @@ -86,7 +92,12 @@ internal class AuthenticatedNetworkContainerV5 internal constructor( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV5(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV5(networkClient.httpClient) @@ -128,4 +139,11 @@ internal class AuthenticatedNetworkContainerV5 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV5(networkClient) override val propertiesApi: PropertiesApi get() = PropertiesApiV5(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV5( + networkClient + ) } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/AccessTokenApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/AccessTokenApiV6.kt index a37f21cd1a8..6818f2f8384 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/AccessTokenApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/AccessTokenApiV6.kt @@ -21,6 +21,6 @@ package com.wire.kalium.network.api.v6.authenticated import com.wire.kalium.network.api.v5.authenticated.AccessTokenApiV5 import io.ktor.client.HttpClient -internal class AccessTokenApiV6 internal constructor( - private val httpClient: HttpClient +internal open class AccessTokenApiV6 internal constructor( + httpClient: HttpClient ) : AccessTokenApiV5(httpClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/ConversationApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/ConversationApiV6.kt index bcdaa50c0a4..9ab818bb241 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/ConversationApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/ConversationApiV6.kt @@ -19,8 +19,25 @@ package com.wire.kalium.network.api.v6.authenticated import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV6 import com.wire.kalium.network.api.v5.authenticated.ConversationApiV5 +import com.wire.kalium.network.api.model.ApiModelMapper +import com.wire.kalium.network.api.model.ApiModelMapperImpl +import com.wire.kalium.network.api.model.UserId +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.network.utils.mapSuccess +import com.wire.kalium.network.utils.wrapKaliumResponse +import io.ktor.client.request.get internal open class ConversationApiV6 internal constructor( authenticatedNetworkClient: AuthenticatedNetworkClient, -) : ConversationApiV5(authenticatedNetworkClient) + private val apiModelMapper: ApiModelMapper = ApiModelMapperImpl() +) : ConversationApiV5(authenticatedNetworkClient) { + override suspend fun fetchMlsOneToOneConversation(userId: UserId): NetworkResponse = + wrapKaliumResponse { + httpClient.get("$PATH_CONVERSATIONS/$PATH_ONE_TO_ONE/${userId.domain}/${userId.value}") + }.mapSuccess { + apiModelMapper.fromApiV6(it) + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/E2EIApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/E2EIApiV6.kt index 62c439c72c8..b11c94f0729 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/E2EIApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/E2EIApiV6.kt @@ -21,5 +21,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v5.authenticated.E2EIApiV5 internal open class E2EIApiV6 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient + authenticatedNetworkClient: AuthenticatedNetworkClient ) : E2EIApiV5(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/KeyPackageApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/KeyPackageApiV6.kt index f4c82848c04..57405a23ff6 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/KeyPackageApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/KeyPackageApiV6.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v5.authenticated.KeyPackageApiV5 internal open class KeyPackageApiV6 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient + authenticatedNetworkClient: AuthenticatedNetworkClient ) : KeyPackageApiV5(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSMessageApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSMessageApiV6.kt index 00a28b82330..11aaa5924a0 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSMessageApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSMessageApiV6.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v5.authenticated.MLSMessageApiV5 internal open class MLSMessageApiV6 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient + authenticatedNetworkClient: AuthenticatedNetworkClient ) : MLSMessageApiV5(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSPublicKeyApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSPublicKeyApiV6.kt index 2657848f35d..2f4978f8d53 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSPublicKeyApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/MLSPublicKeyApiV6.kt @@ -22,5 +22,5 @@ import com.wire.kalium.network.AuthenticatedNetworkClient import com.wire.kalium.network.api.v5.authenticated.MLSPublicKeyApiV5 internal open class MLSPublicKeyApiV6 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient + authenticatedNetworkClient: AuthenticatedNetworkClient ) : MLSPublicKeyApiV5(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/NotificationApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/NotificationApiV6.kt index efe6083f0a4..e5883353969 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/NotificationApiV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/NotificationApiV6.kt @@ -24,7 +24,7 @@ import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.v5.authenticated.NotificationApiV5 internal open class NotificationApiV6 internal constructor( - private val authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedNetworkClient: AuthenticatedNetworkClient, authenticatedWebSocketClient: AuthenticatedWebSocketClient, serverLinks: ServerConfigDTO.Links ) : NotificationApiV5(authenticatedNetworkClient, authenticatedWebSocketClient, serverLinks) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/UpgradePersonalToTeamApiV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/UpgradePersonalToTeamApiV6.kt new file mode 100644 index 00000000000..f53025a78b4 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/UpgradePersonalToTeamApiV6.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v6.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v5.authenticated.UpgradePersonalToTeamApiV5 + +internal open class UpgradePersonalToTeamApiV6 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV5(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/networkContainer/AuthenticatedNetworkContainerV6.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/networkContainer/AuthenticatedNetworkContainerV6.kt index d7b26449871..22d83fe86ec 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/networkContainer/AuthenticatedNetworkContainerV6.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v6/authenticated/networkContainer/AuthenticatedNetworkContainerV6.kt @@ -22,6 +22,8 @@ import com.wire.kalium.logger.KaliumLogger import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -59,8 +61,10 @@ import com.wire.kalium.network.api.v6.authenticated.PreKeyApiV6 import com.wire.kalium.network.api.v6.authenticated.PropertiesApiV6 import com.wire.kalium.network.api.v6.authenticated.SelfApiV6 import com.wire.kalium.network.api.v6.authenticated.TeamsApiV6 +import com.wire.kalium.network.api.v6.authenticated.UpgradePersonalToTeamApiV6 import com.wire.kalium.network.api.v6.authenticated.UserDetailsApiV6 import com.wire.kalium.network.api.v6.authenticated.UserSearchApiV6 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl import com.wire.kalium.network.defaultHttpEngine import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl @@ -68,6 +72,7 @@ import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession @Suppress("LongParameterList") internal class AuthenticatedNetworkContainerV6 internal constructor( @@ -75,6 +80,7 @@ internal class AuthenticatedNetworkContainerV6 internal constructor( private val selfUserId: UserId, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, @@ -86,7 +92,12 @@ internal class AuthenticatedNetworkContainerV6 internal constructor( sessionManager = sessionManager, accessTokenApi = { httpClient -> AccessTokenApiV6(httpClient) }, engine = engine, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } ) { override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV6(networkClient.httpClient) @@ -128,4 +139,12 @@ internal class AuthenticatedNetworkContainerV6 internal constructor( override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV6(networkClient) override val propertiesApi: PropertiesApi get() = PropertiesApiV6(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV6( + networkClient + ) + } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AccessTokenApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AccessTokenApiV7.kt new file mode 100644 index 00000000000..cd092f4574e --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AccessTokenApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.api.v6.authenticated.AccessTokenApiV6 +import io.ktor.client.HttpClient + +internal open class AccessTokenApiV7 internal constructor( + httpClient: HttpClient +) : AccessTokenApiV6(httpClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AssetApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AssetApiV7.kt new file mode 100644 index 00000000000..8777e69fdc1 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/AssetApiV7.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.model.UserId +import com.wire.kalium.network.api.v6.authenticated.AssetApiV6 + +internal open class AssetApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + selfUserId: UserId +) : AssetApiV6(authenticatedNetworkClient, selfUserId) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/CallApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/CallApiV7.kt new file mode 100644 index 00000000000..09496e880a2 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/CallApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.CallApiV6 + +internal open class CallApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : CallApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ClientApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ClientApiV7.kt new file mode 100644 index 00000000000..2f3946f13ed --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ClientApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.ClientApiV6 + +internal open class ClientApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : ClientApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConnectionApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConnectionApiV7.kt new file mode 100644 index 00000000000..b3dbf8c4d26 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConnectionApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.ConnectionApiV6 + +internal open class ConnectionApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : ConnectionApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConversationApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConversationApiV7.kt new file mode 100644 index 00000000000..cd10ebd3b2f --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/ConversationApiV7.kt @@ -0,0 +1,48 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponse +import com.wire.kalium.network.api.authenticated.conversation.ConversationResponseV6 +import com.wire.kalium.network.api.model.ApiModelMapper +import com.wire.kalium.network.api.model.ApiModelMapperImpl +import com.wire.kalium.network.api.model.UserId +import com.wire.kalium.network.api.v6.authenticated.ConversationApiV6 +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.network.utils.mapSuccess +import com.wire.kalium.network.utils.wrapKaliumResponse +import io.ktor.client.request.get + +internal open class ConversationApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + private val apiModelMapper: ApiModelMapper = ApiModelMapperImpl(), +) : ConversationApiV6(authenticatedNetworkClient) { + + override suspend fun fetchMlsOneToOneConversation(userId: UserId): NetworkResponse = + wrapKaliumResponse { + httpClient.get("$PATH_ONE_2_ONE_CONVERSATIONS/${userId.domain}/${userId.value}") + }.mapSuccess { + apiModelMapper.fromApiV6(it) + } + + protected companion object { + const val PATH_ONE_2_ONE_CONVERSATIONS = "one2one-conversations" + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/E2EIApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/E2EIApiV7.kt new file mode 100644 index 00000000000..c645339d015 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/E2EIApiV7.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.E2EIApiV6 + +internal open class E2EIApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : E2EIApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/FeatureConfigApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/FeatureConfigApiV7.kt new file mode 100644 index 00000000000..ab9ec0f62b4 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/FeatureConfigApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.FeatureConfigApiV6 + +internal open class FeatureConfigApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : FeatureConfigApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/KeyPackageApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/KeyPackageApiV7.kt new file mode 100644 index 00000000000..ae9ab0f5cc7 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/KeyPackageApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.KeyPackageApiV6 + +internal open class KeyPackageApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : KeyPackageApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/LogoutApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/LogoutApiV7.kt new file mode 100644 index 00000000000..731121e8acd --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/LogoutApiV7.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.LogoutApiV6 +import com.wire.kalium.network.session.SessionManager + +internal open class LogoutApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + sessionManager: SessionManager +) : LogoutApiV6(authenticatedNetworkClient, sessionManager) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSMessageApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSMessageApiV7.kt new file mode 100644 index 00000000000..74757410650 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSMessageApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.MLSMessageApiV6 + +internal open class MLSMessageApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : MLSMessageApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSPublicKeyApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSPublicKeyApiV7.kt new file mode 100644 index 00000000000..371e72a5500 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MLSPublicKeyApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.MLSPublicKeyApiV6 + +internal open class MLSPublicKeyApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : MLSPublicKeyApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MessageApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MessageApiV7.kt new file mode 100644 index 00000000000..c7f967be701 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/MessageApiV7.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.base.authenticated.message.EnvelopeProtoMapper +import com.wire.kalium.network.api.v6.authenticated.MessageApiV6 + +internal open class MessageApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + envelopeProtoMapper: EnvelopeProtoMapper +) : MessageApiV6(authenticatedNetworkClient, envelopeProtoMapper) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/NotificationApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/NotificationApiV7.kt new file mode 100644 index 00000000000..b47dd923243 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/NotificationApiV7.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.AuthenticatedWebSocketClient +import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO +import com.wire.kalium.network.api.v6.authenticated.NotificationApiV6 + +internal open class NotificationApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + authenticatedWebSocketClient: AuthenticatedWebSocketClient, + serverLinks: ServerConfigDTO.Links +) : NotificationApiV6(authenticatedNetworkClient, authenticatedWebSocketClient, serverLinks) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PreKeyApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PreKeyApiV7.kt new file mode 100644 index 00000000000..c50ced3d815 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PreKeyApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.PreKeyApiV6 + +internal open class PreKeyApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : PreKeyApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PropertiesApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PropertiesApiV7.kt new file mode 100644 index 00000000000..7f35c7d8b17 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/PropertiesApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.PropertiesApiV6 + +internal open class PropertiesApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : PropertiesApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/SelfApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/SelfApiV7.kt new file mode 100644 index 00000000000..354a6519c78 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/SelfApiV7.kt @@ -0,0 +1,28 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.SelfApiV6 +import com.wire.kalium.network.session.SessionManager + +internal open class SelfApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, + sessionManager: SessionManager +) : SelfApiV6(authenticatedNetworkClient, sessionManager) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/TeamsApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/TeamsApiV7.kt new file mode 100644 index 00000000000..4459a2ce9ef --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/TeamsApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.TeamsApiV6 + +internal open class TeamsApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : TeamsApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UpgradePersonalToTeamApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UpgradePersonalToTeamApiV7.kt new file mode 100644 index 00000000000..b1c07ff5418 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UpgradePersonalToTeamApiV7.kt @@ -0,0 +1,54 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.authenticated.user.CreateUserTeamDTO +import com.wire.kalium.network.api.unauthenticated.register.NewBindingTeamDTO +import com.wire.kalium.network.api.v6.authenticated.UpgradePersonalToTeamApiV6 +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.network.utils.wrapKaliumResponse +import io.ktor.client.request.post +import io.ktor.client.request.setBody + +internal open class UpgradePersonalToTeamApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient, +) : UpgradePersonalToTeamApiV6(authenticatedNetworkClient) { + + override suspend fun migrateToTeam(teamName: String): NetworkResponse { + return wrapKaliumResponse { + + httpClient.post(PATH_MIGRATE_TO_TEAM) { + // We do not ask user for icon at this point, so we use hardcoded values from the backend + setBody( + NewBindingTeamDTO( + name = teamName, + iconAssetId = "default", + iconKey = "abc", + currency = null, + ) + ) + } + } + } + + companion object { + const val PATH_MIGRATE_TO_TEAM = "upgrade-personal-to-team" + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserDetailsApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserDetailsApiV7.kt new file mode 100644 index 00000000000..e335745290e --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserDetailsApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.UserDetailsApiV6 + +internal open class UserDetailsApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : UserDetailsApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserSearchApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserSearchApiV7.kt new file mode 100644 index 00000000000..debd4a9c985 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/UserSearchApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.v6.authenticated.UserSearchApiV6 + +internal open class UserSearchApiV7 internal constructor( + authenticatedNetworkClient: AuthenticatedNetworkClient +) : UserSearchApiV6(authenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/networkContainer/AuthenticatedNetworkContainerV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/networkContainer/AuthenticatedNetworkContainerV7.kt new file mode 100644 index 00000000000..5f283abaa7d --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/authenticated/networkContainer/AuthenticatedNetworkContainerV7.kt @@ -0,0 +1,158 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.authenticated.networkContainer + +import com.wire.kalium.logger.KaliumLogger +import com.wire.kalium.network.api.base.authenticated.AccessTokenApi +import com.wire.kalium.network.api.base.authenticated.CallApi +import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi +import com.wire.kalium.network.api.base.authenticated.asset.AssetApi +import com.wire.kalium.network.api.base.authenticated.client.ClientApi +import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi +import com.wire.kalium.network.api.base.authenticated.conversation.ConversationApi +import com.wire.kalium.network.api.base.authenticated.e2ei.E2EIApi +import com.wire.kalium.network.api.base.authenticated.featureConfigs.FeatureConfigApi +import com.wire.kalium.network.api.base.authenticated.keypackage.KeyPackageApi +import com.wire.kalium.network.api.base.authenticated.logout.LogoutApi +import com.wire.kalium.network.api.base.authenticated.message.EnvelopeProtoMapperImpl +import com.wire.kalium.network.api.base.authenticated.message.MLSMessageApi +import com.wire.kalium.network.api.base.authenticated.message.MessageApi +import com.wire.kalium.network.api.base.authenticated.notification.NotificationApi +import com.wire.kalium.network.api.base.authenticated.prekey.PreKeyApi +import com.wire.kalium.network.api.base.authenticated.properties.PropertiesApi +import com.wire.kalium.network.api.base.authenticated.search.UserSearchApi +import com.wire.kalium.network.api.base.authenticated.self.SelfApi +import com.wire.kalium.network.api.base.authenticated.serverpublickey.MLSPublicKeyApi +import com.wire.kalium.network.api.base.authenticated.userDetails.UserDetailsApi +import com.wire.kalium.network.api.model.UserId +import com.wire.kalium.network.api.v7.authenticated.AccessTokenApiV7 +import com.wire.kalium.network.api.v7.authenticated.AssetApiV7 +import com.wire.kalium.network.api.v7.authenticated.CallApiV7 +import com.wire.kalium.network.api.v7.authenticated.ClientApiV7 +import com.wire.kalium.network.api.v7.authenticated.ConnectionApiV7 +import com.wire.kalium.network.api.v7.authenticated.ConversationApiV7 +import com.wire.kalium.network.api.v7.authenticated.E2EIApiV7 +import com.wire.kalium.network.api.v7.authenticated.FeatureConfigApiV7 +import com.wire.kalium.network.api.v7.authenticated.KeyPackageApiV7 +import com.wire.kalium.network.api.v7.authenticated.LogoutApiV7 +import com.wire.kalium.network.api.v7.authenticated.MLSMessageApiV7 +import com.wire.kalium.network.api.v7.authenticated.MLSPublicKeyApiV7 +import com.wire.kalium.network.api.v7.authenticated.MessageApiV7 +import com.wire.kalium.network.api.v7.authenticated.NotificationApiV7 +import com.wire.kalium.network.api.v7.authenticated.PreKeyApiV7 +import com.wire.kalium.network.api.v7.authenticated.PropertiesApiV7 +import com.wire.kalium.network.api.v7.authenticated.SelfApiV7 +import com.wire.kalium.network.api.v7.authenticated.TeamsApiV7 +import com.wire.kalium.network.api.v7.authenticated.UpgradePersonalToTeamApiV7 +import com.wire.kalium.network.api.v7.authenticated.UserDetailsApiV7 +import com.wire.kalium.network.api.v7.authenticated.UserSearchApiV7 +import com.wire.kalium.network.api.vcommon.WildCardApiImpl +import com.wire.kalium.network.defaultHttpEngine +import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProvider +import com.wire.kalium.network.networkContainer.AuthenticatedHttpClientProviderImpl +import com.wire.kalium.network.networkContainer.AuthenticatedNetworkContainer +import com.wire.kalium.network.session.CertificatePinning +import com.wire.kalium.network.session.SessionManager +import io.ktor.client.engine.HttpClientEngine +import io.ktor.websocket.WebSocketSession + +@Suppress("LongParameterList") +internal class AuthenticatedNetworkContainerV7 internal constructor( + private val sessionManager: SessionManager, + private val selfUserId: UserId, + certificatePinning: CertificatePinning, + mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, + kaliumLogger: KaliumLogger, + engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( + serverConfigDTOApiProxy = sessionManager.serverConfig().links.apiProxy, + proxyCredentials = sessionManager.proxyCredentials(), + certificatePinning = certificatePinning + ) +) : AuthenticatedNetworkContainer, + AuthenticatedHttpClientProvider by AuthenticatedHttpClientProviderImpl( + sessionManager = sessionManager, + accessTokenApi = { httpClient -> AccessTokenApiV7(httpClient) }, + engine = engine, + kaliumLogger = kaliumLogger, + webSocketSessionProvider = if (mockWebSocketSession != null) { + { _, _ -> mockWebSocketSession } + } else { + null + } + ) { + + override val accessTokenApi: AccessTokenApi get() = AccessTokenApiV7(networkClient.httpClient) + + override val logoutApi: LogoutApi get() = LogoutApiV7(networkClient, sessionManager) + + override val clientApi: ClientApi get() = ClientApiV7(networkClient) + + override val messageApi: MessageApi + get() = MessageApiV7( + networkClient, + EnvelopeProtoMapperImpl() + ) + + override val mlsMessageApi: MLSMessageApi get() = MLSMessageApiV7(networkClient) + + override val e2eiApi: E2EIApi get() = E2EIApiV7(networkClient) + + override val conversationApi: ConversationApi get() = ConversationApiV7(networkClient) + + override val keyPackageApi: KeyPackageApi get() = KeyPackageApiV7(networkClient) + + override val preKeyApi: PreKeyApi get() = PreKeyApiV7(networkClient) + + override val assetApi: AssetApi get() = AssetApiV7(networkClientWithoutCompression, selfUserId) + + override val notificationApi: NotificationApi + get() = NotificationApiV7( + networkClient, + websocketClient, + backendConfig + ) + + override val teamsApi: TeamsApi get() = TeamsApiV7(networkClient) + + override val selfApi: SelfApi get() = SelfApiV7(networkClient, sessionManager) + + override val userDetailsApi: UserDetailsApi get() = UserDetailsApiV7(networkClient) + + override val userSearchApi: UserSearchApi get() = UserSearchApiV7(networkClient) + + override val callApi: CallApi get() = CallApiV7(networkClient) + + override val connectionApi: ConnectionApi get() = ConnectionApiV7(networkClient) + + override val featureConfigApi: FeatureConfigApi get() = FeatureConfigApiV7(networkClient) + + override val mlsPublicKeyApi: MLSPublicKeyApi get() = MLSPublicKeyApiV7(networkClient) + + override val propertiesApi: PropertiesApi get() = PropertiesApiV7(networkClient) + + override val wildCardApi: WildCardApi get() = WildCardApiImpl(networkClient) + + override val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + get() = UpgradePersonalToTeamApiV7( + networkClient + ) +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/DomainLookupApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/DomainLookupApiV7.kt new file mode 100644 index 00000000000..7eb49e09941 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/DomainLookupApiV7.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.v7.unauthenticated + +import com.wire.kalium.network.UnauthenticatedNetworkClient +import com.wire.kalium.network.api.v6.unauthenticated.DomainLookupApiV6 + +internal open class DomainLookupApiV7 internal constructor( + unauthenticatedNetworkClient: UnauthenticatedNetworkClient +) : DomainLookupApiV6(unauthenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/LoginApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/LoginApiV7.kt new file mode 100644 index 00000000000..47d3953e351 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/LoginApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.unauthenticated + +import com.wire.kalium.network.UnauthenticatedNetworkClient +import com.wire.kalium.network.api.v6.unauthenticated.LoginApiV6 + +internal open class LoginApiV7 internal constructor( + unauthenticatedNetworkClient: UnauthenticatedNetworkClient +) : LoginApiV6(unauthenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/RegisterApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/RegisterApiV7.kt new file mode 100644 index 00000000000..4f6b384955d --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/RegisterApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.unauthenticated + +import com.wire.kalium.network.UnauthenticatedNetworkClient +import com.wire.kalium.network.api.v6.unauthenticated.RegisterApiV6 + +internal open class RegisterApiV7 internal constructor( + unauthenticatedNetworkClient: UnauthenticatedNetworkClient +) : RegisterApiV6(unauthenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/SSOLoginApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/SSOLoginApiV7.kt new file mode 100644 index 00000000000..8ecc50ad97c --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/SSOLoginApiV7.kt @@ -0,0 +1,26 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.unauthenticated + +import com.wire.kalium.network.UnauthenticatedNetworkClient +import com.wire.kalium.network.api.v6.unauthenticated.SSOLoginApiV6 + +internal open class SSOLoginApiV7 internal constructor( + unauthenticatedNetworkClient: UnauthenticatedNetworkClient +) : SSOLoginApiV6(unauthenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/VerificationCodeApiV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/VerificationCodeApiV7.kt new file mode 100644 index 00000000000..fde0fbb268d --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/VerificationCodeApiV7.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.v7.unauthenticated + +import com.wire.kalium.network.UnauthenticatedNetworkClient +import com.wire.kalium.network.api.v6.unauthenticated.VerificationCodeApiV6 + +internal open class VerificationCodeApiV7 internal constructor( + unauthenticatedNetworkClient: UnauthenticatedNetworkClient +) : VerificationCodeApiV6(unauthenticatedNetworkClient) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/networkContainer/UnauthenticatedNetworkContainerV7.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/networkContainer/UnauthenticatedNetworkContainerV7.kt new file mode 100644 index 00000000000..3f3dd9a070f --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/v7/unauthenticated/networkContainer/UnauthenticatedNetworkContainerV7.kt @@ -0,0 +1,85 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.network.api.v7.unauthenticated.networkContainer + +import com.wire.kalium.network.api.base.unauthenticated.appVersioning.AppVersioningApi +import com.wire.kalium.network.api.base.unauthenticated.appVersioning.AppVersioningApiImpl +import com.wire.kalium.network.api.base.unauthenticated.domainLookup.DomainLookupApi +import com.wire.kalium.network.api.base.unauthenticated.login.LoginApi +import com.wire.kalium.network.api.base.unauthenticated.register.RegisterApi +import com.wire.kalium.network.api.base.unauthenticated.sso.SSOLoginApi +import com.wire.kalium.network.api.base.unauthenticated.verification.VerificationCodeApi +import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApi +import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApiImpl +import com.wire.kalium.network.api.base.unbound.versioning.VersionApi +import com.wire.kalium.network.api.base.unbound.versioning.VersionApiImpl +import com.wire.kalium.network.api.model.ProxyCredentialsDTO +import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO +import com.wire.kalium.network.api.v7.unauthenticated.DomainLookupApiV7 +import com.wire.kalium.network.api.v7.unauthenticated.LoginApiV7 +import com.wire.kalium.network.api.v7.unauthenticated.RegisterApiV7 +import com.wire.kalium.network.api.v7.unauthenticated.SSOLoginApiV7 +import com.wire.kalium.network.api.v7.unauthenticated.VerificationCodeApiV7 +import com.wire.kalium.network.defaultHttpEngine +import com.wire.kalium.network.networkContainer.UnauthenticatedNetworkClientProvider +import com.wire.kalium.network.networkContainer.UnauthenticatedNetworkClientProviderImpl +import com.wire.kalium.network.networkContainer.UnauthenticatedNetworkContainer +import com.wire.kalium.network.session.CertificatePinning +import io.ktor.client.engine.HttpClientEngine + +@Suppress("LongParameterList") +class UnauthenticatedNetworkContainerV7 internal constructor( + backendLinks: ServerConfigDTO, + proxyCredentials: ProxyCredentialsDTO?, + certificatePinning: CertificatePinning, + mockEngine: HttpClientEngine?, + engine: HttpClientEngine = mockEngine ?: defaultHttpEngine( + serverConfigDTOApiProxy = backendLinks.links.apiProxy, + proxyCredentials = proxyCredentials, + certificatePinning = certificatePinning + ), + private val developmentApiEnabled: Boolean +) : UnauthenticatedNetworkContainer, + UnauthenticatedNetworkClientProvider by UnauthenticatedNetworkClientProviderImpl( + backendLinks, + engine + ) { + override val loginApi: LoginApi get() = LoginApiV7(unauthenticatedNetworkClient) + override val verificationCodeApi: VerificationCodeApi + get() = VerificationCodeApiV7( + unauthenticatedNetworkClient + ) + override val domainLookupApi: DomainLookupApi + get() = DomainLookupApiV7( + unauthenticatedNetworkClient + ) + override val remoteVersion: VersionApi + get() = VersionApiImpl( + unauthenticatedNetworkClient, + developmentApiEnabled = developmentApiEnabled + ) + override val serverConfigApi: ServerConfigApi + get() = ServerConfigApiImpl(unauthenticatedNetworkClient) + override val registerApi: RegisterApi get() = RegisterApiV7(unauthenticatedNetworkClient) + override val sso: SSOLoginApi get() = SSOLoginApiV7(unauthenticatedNetworkClient) + override val appVersioningApi: AppVersioningApi + get() = AppVersioningApiImpl( + unauthenticatedNetworkClient + ) +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/vcommon/WildCardApiImpl.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/vcommon/WildCardApiImpl.kt new file mode 100644 index 00000000000..1c46937799c --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/vcommon/WildCardApiImpl.kt @@ -0,0 +1,52 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.api.vcommon + +import com.wire.kalium.network.AuthenticatedNetworkClient +import com.wire.kalium.network.api.base.authenticated.WildCardApi +import com.wire.kalium.network.utils.NetworkResponse +import com.wire.kalium.network.utils.wrapKaliumResponse +import io.ktor.client.request.parameter +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.http.HttpMethod + +internal class WildCardApiImpl( + private val authenticatedNetworkClient: AuthenticatedNetworkClient +) : WildCardApi { + override suspend fun customRequest( + httpMethod: HttpMethod, + requestPath: List, + body: String?, + queryParam: Map, + customHeader: Map + ): NetworkResponse = wrapKaliumResponse { + authenticatedNetworkClient.httpClient.request { + method = httpMethod + url(requestPath.joinToString("/")) + body?.let { setBody(it) } + queryParam.forEach { (key, value) -> + parameter(key, value) + } + customHeader.forEach { (key, value) -> + headers.append(key, value) + } + } + } +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/AuthenticatedNetworkContainer.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/AuthenticatedNetworkContainer.kt index 442421a431e..2799e45f637 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/AuthenticatedNetworkContainer.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/AuthenticatedNetworkContainer.kt @@ -24,6 +24,8 @@ import com.wire.kalium.network.AuthenticatedWebSocketClient import com.wire.kalium.network.api.base.authenticated.AccessTokenApi import com.wire.kalium.network.api.base.authenticated.CallApi import com.wire.kalium.network.api.base.authenticated.TeamsApi +import com.wire.kalium.network.api.base.authenticated.UpgradePersonalToTeamApi +import com.wire.kalium.network.api.base.authenticated.WildCardApi import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.base.authenticated.client.ClientApi import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi @@ -48,6 +50,7 @@ import com.wire.kalium.network.api.v2.authenticated.networkContainer.Authenticat import com.wire.kalium.network.api.v4.authenticated.networkContainer.AuthenticatedNetworkContainerV4 import com.wire.kalium.network.api.v5.authenticated.networkContainer.AuthenticatedNetworkContainerV5 import com.wire.kalium.network.api.v6.authenticated.networkContainer.AuthenticatedNetworkContainerV6 +import com.wire.kalium.network.api.v7.authenticated.networkContainer.AuthenticatedNetworkContainerV7 import com.wire.kalium.network.session.CertificatePinning import com.wire.kalium.network.session.SessionManager import io.ktor.client.HttpClient @@ -55,6 +58,7 @@ import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.auth.providers.BearerAuthProvider import io.ktor.client.plugins.auth.providers.BearerTokens import io.ktor.client.plugins.auth.providers.RefreshTokensParams +import io.ktor.websocket.WebSocketSession @Suppress("MagicNumber") interface AuthenticatedNetworkContainer { @@ -105,6 +109,10 @@ interface AuthenticatedNetworkContainer { val propertiesApi: PropertiesApi + val wildCardApi: WildCardApi + + val upgradePersonalToTeamApi: UpgradePersonalToTeamApi + companion object { @Suppress("LongParameterList", "LongMethod") @@ -114,6 +122,7 @@ interface AuthenticatedNetworkContainer { userAgent: String, certificatePinning: CertificatePinning, mockEngine: HttpClientEngine?, + mockWebSocketSession: WebSocketSession?, kaliumLogger: KaliumLogger, ): AuthenticatedNetworkContainer { @@ -124,14 +133,16 @@ interface AuthenticatedNetworkContainer { sessionManager, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) 1 -> AuthenticatedNetworkContainerV0( sessionManager, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) 2 -> AuthenticatedNetworkContainerV2( @@ -139,7 +150,8 @@ interface AuthenticatedNetworkContainer { selfUserId, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) // this is intentional since we should drop support for api v3 @@ -149,7 +161,8 @@ interface AuthenticatedNetworkContainer { selfUserId, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) 4 -> AuthenticatedNetworkContainerV4( @@ -157,7 +170,8 @@ interface AuthenticatedNetworkContainer { selfUserId, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) 5 -> AuthenticatedNetworkContainerV5( @@ -165,7 +179,8 @@ interface AuthenticatedNetworkContainer { selfUserId, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger ) 6 -> AuthenticatedNetworkContainerV6( @@ -173,9 +188,21 @@ interface AuthenticatedNetworkContainer { selfUserId, certificatePinning, mockEngine, - kaliumLogger, + mockWebSocketSession, + kaliumLogger + ) + + 7 -> AuthenticatedNetworkContainerV7( + sessionManager, + selfUserId, + certificatePinning, + mockEngine, + mockWebSocketSession, + kaliumLogger ) + // You can use scripts/generate_new_api_version.sh or gradle task network:generateNewApiVersion to + // bump API version and generate all needed classes else -> error("Unsupported version: $version") } } @@ -194,6 +221,7 @@ internal class AuthenticatedHttpClientProviderImpl( private val sessionManager: SessionManager, private val accessTokenApi: (httpClient: HttpClient) -> AccessTokenApi, private val engine: HttpClientEngine, + private val webSocketSessionProvider: ((HttpClient, String) -> WebSocketSession)?, private val kaliumLogger: KaliumLogger, ) : AuthenticatedHttpClientProvider { @@ -241,7 +269,8 @@ internal class AuthenticatedHttpClientProviderImpl( engine, bearerAuthProvider, sessionManager.serverConfig(), - kaliumLogger + kaliumLogger, + webSocketSessionProvider ) } override val networkClientWithoutCompression by lazy { diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnauthenticatedNetworkContainer.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnauthenticatedNetworkContainer.kt index 54a0579d0db..f647f9dcff3 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnauthenticatedNetworkContainer.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/networkContainer/UnauthenticatedNetworkContainer.kt @@ -19,21 +19,22 @@ package com.wire.kalium.network.networkContainer import com.wire.kalium.network.UnauthenticatedNetworkClient -import com.wire.kalium.network.api.model.ProxyCredentialsDTO +import com.wire.kalium.network.api.base.unauthenticated.appVersioning.AppVersioningApi import com.wire.kalium.network.api.base.unauthenticated.domainLookup.DomainLookupApi import com.wire.kalium.network.api.base.unauthenticated.login.LoginApi +import com.wire.kalium.network.api.base.unauthenticated.register.RegisterApi import com.wire.kalium.network.api.base.unauthenticated.sso.SSOLoginApi import com.wire.kalium.network.api.base.unauthenticated.verification.VerificationCodeApi -import com.wire.kalium.network.api.base.unauthenticated.appVersioning.AppVersioningApi -import com.wire.kalium.network.api.base.unauthenticated.register.RegisterApi import com.wire.kalium.network.api.base.unbound.configuration.ServerConfigApi -import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.base.unbound.versioning.VersionApi +import com.wire.kalium.network.api.model.ProxyCredentialsDTO +import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.api.v0.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV0 import com.wire.kalium.network.api.v2.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV2 import com.wire.kalium.network.api.v4.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV4 import com.wire.kalium.network.api.v5.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV5 import com.wire.kalium.network.api.v6.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV6 +import com.wire.kalium.network.api.v7.unauthenticated.networkContainer.UnauthenticatedNetworkContainerV7 import com.wire.kalium.network.session.CertificatePinning import io.ktor.client.engine.HttpClientEngine @@ -120,6 +121,16 @@ interface UnauthenticatedNetworkContainer { developmentApiEnabled = developmentApiEnabled ) + 7 -> UnauthenticatedNetworkContainerV7( + backendLinks = serverConfigDTO, + proxyCredentials = proxyCredentials, + certificatePinning = certificatePinning, + mockEngine = mockEngine, + developmentApiEnabled = developmentApiEnabled + ) + + // You can use scripts/generate_new_api_version.sh or gradle task network:generateNewApiVersion to + // bump API version and generate all needed classes else -> error("Unsupported version: ${serverConfigDTO.metaData.commonApiVersion.version}") } } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockUnboundNetworkClient.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockUnboundNetworkClient.kt index 7fff288ccb8..3e9031e6dff 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockUnboundNetworkClient.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockUnboundNetworkClient.kt @@ -68,6 +68,7 @@ object MockUnboundNetworkClient { ) } } + println("no expected response was found for ${currentRequest.method.value}:${currentRequest.url}") throw UnsupportedOperationException("no expected response was found for ${currentRequest.method.value}:${currentRequest.url}") } diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockWebSocketSession.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockWebSocketSession.kt new file mode 100644 index 00000000000..72548ef0923 --- /dev/null +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/MockWebSocketSession.kt @@ -0,0 +1,45 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.network.utils + +import io.ktor.websocket.Frame +import io.ktor.websocket.WebSocketExtension +import io.ktor.websocket.WebSocketSession +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.SendChannel +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +@Suppress("EmptyFunctionBlock") +class MockWebSocketSession( + override val coroutineContext: CoroutineContext = EmptyCoroutineContext +) : WebSocketSession { + override var masking: Boolean = false + override var maxFrameSize: Long = Long.MAX_VALUE + + override val incoming: ReceiveChannel = Channel(Channel.UNLIMITED) + override val outgoing: SendChannel = Channel(Channel.UNLIMITED) + + override val extensions: List> = emptyList() + + override suspend fun flush() {} + + @Suppress("DEPRECATION") + override fun terminate() {} +} diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/ObfuscateUtil.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/ObfuscateUtil.kt index 225d622fcca..881b0ccb6cb 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/utils/ObfuscateUtil.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/utils/ObfuscateUtil.kt @@ -25,7 +25,6 @@ import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logger.obfuscateUrlPath import com.wire.kalium.util.serialization.toJsonElement import io.ktor.http.Url -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/ApiTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/ApiTest.kt index a06822a59f7..1b942416156 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/ApiTest.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/ApiTest.kt @@ -121,7 +121,8 @@ internal abstract class ApiTest { sessionManager = TEST_SESSION_MANAGER, certificatePinning = emptyMap(), mockEngine = null, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + mockWebSocketSession = null ).networkClient } @@ -134,7 +135,8 @@ internal abstract class ApiTest { sessionManager = TEST_SESSION_MANAGER, certificatePinning = emptyMap(), mockEngine = null, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + mockWebSocketSession = null ).websocketClient } @@ -258,7 +260,8 @@ internal abstract class ApiTest { sessionManager = TEST_SESSION_MANAGER, certificatePinning = emptyMap(), mockEngine = null, - kaliumLogger = kaliumLogger + kaliumLogger = kaliumLogger, + mockWebSocketSession = null ).networkClient } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/authenticated/client/ClientDTOSerializationTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/authenticated/client/ClientDTOSerializationTest.kt new file mode 100644 index 00000000000..5d8c238d963 --- /dev/null +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/authenticated/client/ClientDTOSerializationTest.kt @@ -0,0 +1,41 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.api.authenticated.client + +import com.wire.kalium.mocks.responses.ClientResponseJson +import com.wire.kalium.network.api.authenticated.client.ClientDTO +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertTrue + + +class ClientDTOSerializationTest { + @Test + fun givenJsonWithCapabilitiesList_whenDeserialize_thenReturnCapabilities() { + val jsonString = ClientResponseJson.valid.rawJson + val deserializedClient = Json.decodeFromString(jsonString) + assertTrue(deserializedClient.capabilities.isNotEmpty()) + } + + @Test + fun givenJsonWithCapabilitiesObjectWrapperList_whenDeserialize_thenReturnCapabilities() { + val jsonString = ClientResponseJson.validCapabilitiesObject.rawJson + val deserializedClient = Json.decodeFromString(jsonString) + assertTrue(deserializedClient.capabilities.isNotEmpty()) + } +} \ No newline at end of file diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt index 960aed3c7d1..19c3090a873 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt @@ -211,6 +211,49 @@ internal class ACMEApiTest : ApiTest() { } } + @Test + fun givenProxyAndCrl_whenGettingClientDomainCRL_thenUseProxyUrlWithCRLHostAddedToPath() = runTest { + val crlUrl = "https://crl.wire.com/crl" + val proxyUrl = "https://proxy.wire:9000/proxy" + val expected = "$proxyUrl/crl.wire.com" + val networkClient = mockUnboundNetworkClient( + "", + statusCode = HttpStatusCode.OK, + assertion = { + assertJson() + assertUrlEqual(expected) + assertGet() + assertNoQueryParams() + } + ) + val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient) + + acmeApi.getClientDomainCRL(url = crlUrl, proxyUrl = proxyUrl).also { actual -> + assertIs>(actual) + } + } + + @Test + fun givenCRLWithHttpsProtocol_whenGettingClientDomainCRL_thenItShouldNotBeChanged() = runTest { + val crlUrl = "https://crl.wire.com/crl" + val expected = crlUrl + val networkClient = mockUnboundNetworkClient( + "", + statusCode = HttpStatusCode.OK, + assertion = { + assertJson() + assertUrlEqual(expected) + assertGet() + assertNoQueryParams() + } + ) + val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient) + + acmeApi.getClientDomainCRL(url = crlUrl, proxyUrl = null).also { actual -> + assertIs>(actual) + } + } + companion object { private const val ACME_DISCOVERY_URL = "https://balderdash.hogwash.work:9000/acme/google-android/directory" private const val ACME_DIRECTORIES_PATH = "https://balderdash.hogwash.work:9000/acme/google-android/directory" diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/asset/AssetApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/asset/AssetApiV0Test.kt index 6efc55a2a96..0043d3d25e8 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/asset/AssetApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/asset/AssetApiV0Test.kt @@ -19,8 +19,8 @@ package com.wire.kalium.api.v0.asset import com.wire.kalium.api.ApiTest -import com.wire.kalium.mocks.responses.asset.AssetDownloadResponseJson -import com.wire.kalium.mocks.responses.asset.AssetUploadResponseJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.asset.AssetMocks import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.authenticated.asset.AssetMetadataRequest import com.wire.kalium.network.api.model.AssetId @@ -54,7 +54,7 @@ internal class AssetApiV0Test : ApiTest() { val encryptedData = "some-data".encodeToByteArray() val encryptedDataSource = { getDummyDataSource(fileSystem, encryptedData) } val networkClient = mockAuthenticatedNetworkClient( - VALID_ASSET_UPLOAD_RESPONSE.rawJson, + VALID_ASSET_UPLOAD_RESPONSE.toJsonString(), statusCode = HttpStatusCode.Created, assertion = { assertPost() @@ -70,7 +70,7 @@ internal class AssetApiV0Test : ApiTest() { // Then assertTrue(response.isSuccessful()) - assertEquals(response.value, VALID_ASSET_UPLOAD_RESPONSE.serializableData) + assertEquals(response.value, VALID_ASSET_UPLOAD_RESPONSE) } private fun getDummyDataSource(fileSystem: FakeFileSystem, dummyData: ByteArray): Source { @@ -89,7 +89,7 @@ internal class AssetApiV0Test : ApiTest() { val encryptedData = "some-data".encodeToByteArray() val encryptedDataSource = { getDummyDataSource(fileSystem, encryptedData) } val networkClient = mockAuthenticatedNetworkClient( - INVALID_ASSET_UPLOAD_RESPONSE.rawJson, + INVALID_ASSET_UPLOAD_RESPONSE.toJsonString(), statusCode = HttpStatusCode.BadRequest, assertion = { assertPost() @@ -186,7 +186,7 @@ internal class AssetApiV0Test : ApiTest() { // Given val apiPath = "$PATH_ASSETS_V4/$ASSET_DOMAIN/$ASSET_KEY" val networkClient = mockAuthenticatedNetworkClient( - responseBody = AssetDownloadResponseJson.invalid.rawJson, + responseBody = AssetMocks.invalid.toJsonString(), statusCode = HttpStatusCode.BadRequest, assertion = { assertGet() @@ -204,8 +204,8 @@ internal class AssetApiV0Test : ApiTest() { } companion object { - val VALID_ASSET_UPLOAD_RESPONSE = AssetUploadResponseJson.valid - val INVALID_ASSET_UPLOAD_RESPONSE = AssetUploadResponseJson.invalid + val VALID_ASSET_UPLOAD_RESPONSE = AssetMocks.asset + val INVALID_ASSET_UPLOAD_RESPONSE = AssetMocks.invalid const val PATH_ASSETS_V3 = "/assets/v3" const val PATH_ASSETS_V4 = "/assets/v4" const val HEADER_ASSET_TOKEN = "Asset-Token" diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/connection/ConnectionApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/connection/ConnectionApiV0Test.kt index 56d644bf4b9..7f93fae9861 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/connection/ConnectionApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/connection/ConnectionApiV0Test.kt @@ -19,8 +19,8 @@ package com.wire.kalium.api.v0.connection import com.wire.kalium.api.ApiTest -import com.wire.kalium.mocks.responses.connection.ConnectionRequestsJson -import com.wire.kalium.mocks.responses.connection.ConnectionResponsesJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.connection.ConnectionMocks import com.wire.kalium.network.api.base.authenticated.connection.ConnectionApi import com.wire.kalium.network.api.authenticated.connection.ConnectionStateDTO import com.wire.kalium.network.api.model.UserId @@ -36,12 +36,12 @@ internal class ConnectionApiV0Test : ApiTest() { @Test fun givenAGetConnectionsRequest_whenRequestingAllConnectionsWithSuccess_thenRequestShouldBeConfiguredCorrectly() = runTest { val networkClient = mockAuthenticatedNetworkClient( - GET_CONNECTIONS_RESPONSE.rawJson, + GET_CONNECTIONS_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertJson() assertPost() - assertJsonBodyContent(GET_CONNECTIONS_NO_PAGING_REQUEST.rawJson) + assertJsonBodyContent(GET_CONNECTIONS_NO_PAGING_REQUEST.toJsonString()) assertPathEqual(PATH_CONNECTIONS) } ) @@ -53,18 +53,18 @@ internal class ConnectionApiV0Test : ApiTest() { @Test fun givenAGetConnectionsRequestWithPaging_whenRequestingAllConnectionsWithSuccess_thenRequestShouldBeConfiguredCorrectly() = runTest { val networkClient = mockAuthenticatedNetworkClient( - GET_CONNECTIONS_RESPONSE.rawJson, + GET_CONNECTIONS_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertJson() assertPost() - assertJsonBodyContent(GET_CONNECTIONS_WITH_PAGING_REQUEST.rawJson) + assertJsonBodyContent(GET_CONNECTIONS_WITH_PAGING_REQUEST.toJsonString()) assertPathEqual(PATH_CONNECTIONS) } ) val connectionApi: ConnectionApi = ConnectionApiV0(networkClient) - connectionApi.fetchSelfUserConnections(pagingState = GET_CONNECTIONS_WITH_PAGING_REQUEST.serializableData) + connectionApi.fetchSelfUserConnections(pagingState = GET_CONNECTIONS_WITH_PAGING_REQUEST.pagingState) } @Test @@ -72,7 +72,7 @@ internal class ConnectionApiV0Test : ApiTest() { // given val userId = UserId("user_id", "domain_id") val httpClient = mockAuthenticatedNetworkClient( - CREATE_CONNECTION_RESPONSE.rawJson, + CREATE_CONNECTION_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertJson() @@ -96,13 +96,13 @@ internal class ConnectionApiV0Test : ApiTest() { // given val userId = UserId("user_id", "domain_id") val httpClient = mockAuthenticatedNetworkClient( - CREATE_CONNECTION_RESPONSE.rawJson, + CREATE_CONNECTION_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertJson() assertPut() assertPathEqual("$PATH_CONNECTIONS_ENDPOINT/${userId.domain}/${userId.value}") - assertJsonBodyContent(GET_CONNECTION_STATUS_REQUEST.rawJson) + assertJsonBodyContent(GET_CONNECTION_STATUS_REQUEST.toJsonString()) } ) val connectionApi = ConnectionApiV0(httpClient) @@ -118,10 +118,10 @@ internal class ConnectionApiV0Test : ApiTest() { const val PATH_CONNECTIONS = "/list-connections" const val PATH_CONNECTIONS_ENDPOINT = "/connections" - val GET_CONNECTIONS_RESPONSE = ConnectionResponsesJson.GetConnections.validGetConnections - val CREATE_CONNECTION_RESPONSE = ConnectionResponsesJson.CreateConnectionResponse.jsonProvider - val GET_CONNECTIONS_NO_PAGING_REQUEST = ConnectionRequestsJson.validEmptyBody - val GET_CONNECTIONS_WITH_PAGING_REQUEST = ConnectionRequestsJson.validPagingState - val GET_CONNECTION_STATUS_REQUEST = ConnectionRequestsJson.validConnectionStatusUpdate + val GET_CONNECTIONS_RESPONSE = ConnectionMocks.connectionsResponse + val CREATE_CONNECTION_RESPONSE = ConnectionMocks.connection + val GET_CONNECTIONS_NO_PAGING_REQUEST = ConnectionMocks.emptyPaginationRequest + val GET_CONNECTIONS_WITH_PAGING_REQUEST = ConnectionMocks.paginationRequest + val GET_CONNECTION_STATUS_REQUEST = ConnectionMocks.acceptedConnectionRequest } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/conversation/ConversationApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/conversation/ConversationApiV0Test.kt index 70376fd2618..ee9d04dfee4 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/conversation/ConversationApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/conversation/ConversationApiV0Test.kt @@ -19,11 +19,12 @@ package com.wire.kalium.api.v0.conversation import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.conversation.ConversationMocks import com.wire.kalium.mocks.responses.AddServiceResponseJson import com.wire.kalium.mocks.responses.EventContentDTOJson import com.wire.kalium.mocks.responses.EventContentDTOJson.validGenerateGuestRoomLink import com.wire.kalium.mocks.responses.conversation.ConversationDetailsResponse -import com.wire.kalium.mocks.responses.conversation.ConversationListIdsResponseJson import com.wire.kalium.mocks.responses.conversation.ConversationResponseJson import com.wire.kalium.mocks.responses.conversation.CreateConversationRequestJson import com.wire.kalium.mocks.responses.conversation.MemberUpdateRequestJson @@ -103,7 +104,7 @@ internal class ConversationApiV0Test : ApiTest() { @Test fun givenFetchConversationsIds_whenCallingFetchConversations_thenTheRequestShouldBeConfiguredOK() = runTest { val networkClient = mockAuthenticatedNetworkClient( - responseBody = CONVERSATION_IDS_RESPONSE.rawJson, + responseBody = CONVERSATION_IDS_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertPost() @@ -124,7 +125,7 @@ internal class ConversationApiV0Test : ApiTest() { assertion = { assertPost() assertJson() - assertJsonBodyContent(CREATE_CONVERSATION_IDS_REQUEST.rawJson) + assertJsonBodyContent(CREATE_CONVERSATION_IDS_REQUEST.toJsonString()) assertPathEqual(PATH_CONVERSATIONS_LIST_V2) } ) @@ -132,7 +133,7 @@ internal class ConversationApiV0Test : ApiTest() { val conversationApi = ConversationApiV0(networkClient) conversationApi.fetchConversationsListDetails( listOf( - ConversationId("ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", "anta.wire.link"), + ConversationMocks.conversationId, ConversationId("f4680835-2cfe-4d4d-8491-cbb201bd5c2b", "anta.wire.link") ) ) @@ -464,11 +465,11 @@ internal class ConversationApiV0Test : ApiTest() { const val PATH_RECEIPT_MODE = "receipt-mode" const val PATH_CODE = "code" const val PATH_TYPING_NOTIFICATION = "typing" - val CREATE_CONVERSATION_RESPONSE = ConversationResponseJson.v0.rawJson + val CREATE_CONVERSATION_RESPONSE = ConversationResponseJson.v0().rawJson val CREATE_CONVERSATION_REQUEST = CreateConversationRequestJson.v0 - val CREATE_CONVERSATION_IDS_REQUEST = ConversationListIdsResponseJson.validRequestIds + val CREATE_CONVERSATION_IDS_REQUEST = ConversationMocks.conversationsDetailsRequest val UPDATE_ACCESS_ROLE_REQUEST = UpdateConversationAccessRequestJson.v0 - val CONVERSATION_IDS_RESPONSE = ConversationListIdsResponseJson.validGetIds + val CONVERSATION_IDS_RESPONSE = ConversationMocks.conversationListIdsResponse val CONVERSATION_DETAILS_RESPONSE = ConversationDetailsResponse.validGetDetailsForIds val MEMBER_UPDATE_REQUEST = MemberUpdateRequestJson.valid } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/LoginApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/LoginApiV0Test.kt index 1da54c1092c..3046d6170c5 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/LoginApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/LoginApiV0Test.kt @@ -20,9 +20,9 @@ package com.wire.kalium.api.v0.user.login import com.wire.kalium.api.ApiTest import com.wire.kalium.api.json.model.ErrorResponseJson -import com.wire.kalium.mocks.responses.AccessTokenDTOJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.client.TokenMocks import com.wire.kalium.mocks.responses.LoginWithEmailRequestJson -import com.wire.kalium.mocks.responses.UserDTOJson import com.wire.kalium.network.api.model.AccessTokenDTO import com.wire.kalium.network.api.model.QualifiedID import com.wire.kalium.network.api.model.SelfUserDTO @@ -49,7 +49,7 @@ internal class LoginApiV0Test : ApiTest() { fun givenAValidLoginRequest_whenCallingTheLoginEndpoint_theRequestShouldBeConfiguredCorrectly() = runTest { val expectedLoginRequest = TestRequestHandler( path = PATH_LOGIN, - responseBody = VALID_ACCESS_TOKEN_RESPONSE.rawJson, + responseBody = VALID_ACCESS_TOKEN_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertPost() @@ -61,9 +61,9 @@ internal class LoginApiV0Test : ApiTest() { }, headers = mapOf("set-cookie" to "zuid=$refreshToken") ) - val expectedSelfResponse = ApiTest.TestRequestHandler( + val expectedSelfResponse = TestRequestHandler( path = PATH_SELF, - responseBody = VALID_SELF_RESPONSE.rawJson, + responseBody = VALID_SELF_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertGet() @@ -74,9 +74,9 @@ internal class LoginApiV0Test : ApiTest() { val networkClient = mockUnauthenticatedNetworkClient( listOf(expectedLoginRequest, expectedSelfResponse) ) - val expected = with(VALID_ACCESS_TOKEN_RESPONSE.serializableData) { + val expected = with(VALID_ACCESS_TOKEN_RESPONSE) { SessionDTO( - userId = VALID_SELF_RESPONSE.serializableData.id, + userId = VALID_SELF_RESPONSE.id, accessToken = value, tokenType = tokenType, refreshToken = refreshToken, @@ -106,13 +106,13 @@ internal class LoginApiV0Test : ApiTest() { @Test fun givenLoginRequestSuccessAndSelfInfoFail_thenExceptionIsPropagated() = runTest { - val expectedLoginRequest = ApiTest.TestRequestHandler( + val expectedLoginRequest = TestRequestHandler( path = PATH_LOGIN, - responseBody = VALID_ACCESS_TOKEN_RESPONSE.rawJson, + responseBody = VALID_ACCESS_TOKEN_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, headers = mapOf("set-cookie" to "zuid=$refreshToken") ) - val expectedSelfResponse = ApiTest.TestRequestHandler( + val expectedSelfResponse = TestRequestHandler( path = PATH_SELF, responseBody = ErrorResponseJson.valid.rawJson, statusCode = HttpStatusCode.BadRequest @@ -157,8 +157,8 @@ internal class LoginApiV0Test : ApiTest() { ssoID = null, supportedProtocols = null ) - val VALID_ACCESS_TOKEN_RESPONSE = AccessTokenDTOJson.createValid(accessTokenDto) - val VALID_SELF_RESPONSE = UserDTOJson.createValid(userDTO) + val VALID_ACCESS_TOKEN_RESPONSE = accessTokenDto + val VALID_SELF_RESPONSE = userDTO val LOGIN_WITH_EMAIL_REQUEST = LoginWithEmailRequestJson.validLoginWithEmail val ERROR_RESPONSE = ErrorResponseJson.valid.serializableData diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/SSOLoginApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/SSOLoginApiV0Test.kt index 904223b9343..3e992549b99 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/SSOLoginApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/login/SSOLoginApiV0Test.kt @@ -20,11 +20,12 @@ package com.wire.kalium.api.v0.user.login import com.wire.kalium.api.ApiTest import com.wire.kalium.api.TEST_BACKEND -import com.wire.kalium.mocks.responses.AccessTokenDTOJson -import com.wire.kalium.mocks.responses.UserDTOJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.client.TokenMocks +import com.wire.kalium.mocks.mocks.user.UserMocks +import com.wire.kalium.network.api.base.unauthenticated.sso.SSOLoginApi import com.wire.kalium.network.api.model.AuthenticationResultDTO import com.wire.kalium.network.api.unauthenticated.sso.InitiateParam -import com.wire.kalium.network.api.base.unauthenticated.sso.SSOLoginApi import com.wire.kalium.network.api.v0.unauthenticated.SSOLoginApiV0 import com.wire.kalium.network.utils.CustomErrors import com.wire.kalium.network.utils.NetworkResponse @@ -32,13 +33,11 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.Url import io.ktor.http.protocolWithAuthority -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertIs -@OptIn(ExperimentalCoroutinesApi::class) internal class SSOLoginApiV0Test : ApiTest() { @Test @@ -107,13 +106,13 @@ internal class SSOLoginApiV0Test : ApiTest() { @Test fun givenBEResponseSuccess_whenFetchingAuthToken_thenTheRefreshTokenIsClean() = runTest { val cookie = "zuid=cookie" - val authResponse = AccessTokenDTOJson.valid - val selfResponse = UserDTOJson.valid + val authResponse = TokenMocks.accessToken + val selfResponse = UserMocks.selfUser val networkClient = mockUnauthenticatedNetworkClient( listOf( - ApiTest.TestRequestHandler( + TestRequestHandler( path = PATH_ACCESS, - authResponse.rawJson, + authResponse.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertGet() @@ -121,9 +120,9 @@ internal class SSOLoginApiV0Test : ApiTest() { assertPathEqual(PATH_ACCESS) } ), - ApiTest.TestRequestHandler( + TestRequestHandler( path = PATH_SELF, - selfResponse.rawJson, + selfResponse.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertGet() @@ -137,21 +136,21 @@ internal class SSOLoginApiV0Test : ApiTest() { assertIs>(actual) assertEquals(cookie.removePrefix("zuid="), actual.value.sessionDTO.refreshToken) - assertEquals(authResponse.serializableData.value, actual.value.sessionDTO.accessToken) - assertEquals(authResponse.serializableData.tokenType, actual.value.sessionDTO.tokenType) - assertEquals(selfResponse.serializableData.id, actual.value.sessionDTO.userId) - assertEquals(selfResponse.serializableData, actual.value.userDTO) + assertEquals(authResponse.value, actual.value.sessionDTO.accessToken) + assertEquals(authResponse.tokenType, actual.value.sessionDTO.tokenType) + assertEquals(selfResponse.id, actual.value.sessionDTO.userId) + assertEquals(selfResponse, actual.value.userDTO) } @Test fun cookieIsMissingZuidToke_whenFetchingAuthToken_thenReturnError() = runTest { val cookie = "cookie" - val authResponse = AccessTokenDTOJson.valid + val authResponse = TokenMocks.accessToken val networkClient = mockUnauthenticatedNetworkClient( listOf( - ApiTest.TestRequestHandler( + TestRequestHandler( path = PATH_ACCESS, - authResponse.rawJson, + authResponse.toJsonString(), statusCode = HttpStatusCode.OK ) ) diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/register/RegisterApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/register/RegisterApiV0Test.kt index 25fc8e33984..0116020a95e 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/register/RegisterApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/register/RegisterApiV0Test.kt @@ -20,10 +20,11 @@ package com.wire.kalium.api.v0.user.register import com.wire.kalium.api.ApiTest import com.wire.kalium.api.json.model.ErrorResponseJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.user.UserMocks import com.wire.kalium.mocks.responses.ActivationRequestJson import com.wire.kalium.mocks.responses.RegisterAccountJson import com.wire.kalium.mocks.responses.RequestActivationCodeJson -import com.wire.kalium.mocks.responses.UserDTOJson import io.ktor.http.HttpStatusCode import kotlinx.coroutines.test.runTest import kotlin.test.Ignore @@ -36,7 +37,7 @@ internal class RegisterApiV0Test : ApiTest() { @Test fun givenAValidEmail_whenRegisteringAccountWithEMail_theRequestShouldBeConfiguredCorrectly() = runTest { val networkClient = mockUnauthenticatedNetworkClient( - VALID_REGISTER_RESPONSE.rawJson, + VALID_REGISTER_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertPost() @@ -170,7 +171,7 @@ internal class RegisterApiV0Test : ApiTest() { private companion object { val VALID_PERSONAL_ACCOUNT_REQUEST = RegisterAccountJson.validPersonalAccountRegister - val VALID_REGISTER_RESPONSE = UserDTOJson.valid + val VALID_REGISTER_RESPONSE = UserMocks.selfUser val VALID_SEND_ACTIVATE_EMAIL = RequestActivationCodeJson.validActivateEmail val VALID_ACTIVATE_EMAIL = ActivationRequestJson.validActivateEmail val ERROR_RESPONSE = ErrorResponseJson.valid diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/self/SelfApiV0Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/self/SelfApiV0Test.kt index d55565a5234..2af47d29ff4 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/self/SelfApiV0Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v0/user/self/SelfApiV0Test.kt @@ -20,7 +20,8 @@ package com.wire.kalium.api.v0.user.self import com.wire.kalium.api.ApiTest import com.wire.kalium.api.json.model.ErrorResponseJson -import com.wire.kalium.mocks.responses.UserDTOJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.user.UserMocks import com.wire.kalium.network.api.model.SupportedProtocolDTO import com.wire.kalium.network.api.v0.authenticated.SelfApiV0 import com.wire.kalium.network.exceptions.KaliumException @@ -40,7 +41,7 @@ internal class SelfApiV0Test : ApiTest() { fun givenAValidRegisterLogoutRequest_whenCallingTheRegisterLogoutEndpoint_theRequestShouldBeConfiguredCorrectly() = runTest { val networkClient = mockAuthenticatedNetworkClient( - VALID_SELF_RESPONSE.rawJson, + VALID_SELF_RESPONSE.toJsonString(), statusCode = HttpStatusCode.Created, assertion = { assertGet() @@ -51,7 +52,7 @@ internal class SelfApiV0Test : ApiTest() { val selfApi = SelfApiV0(networkClient, TEST_SESSION_MANAGER) val response = selfApi.getSelfInfo() assertTrue(response.isSuccessful()) - assertEquals(response.value, VALID_SELF_RESPONSE.serializableData) + assertEquals(response.value, VALID_SELF_RESPONSE) } @Test @@ -135,7 +136,7 @@ internal class SelfApiV0Test : ApiTest() { private companion object { const val PATH_SELF = "/self" - val VALID_SELF_RESPONSE = UserDTOJson.valid + val VALID_SELF_RESPONSE = UserMocks.selfUser val ERROR_RESPONSE = ErrorResponseJson.valid.serializableData } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v2/AssetApiV2Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/AssetApiV2Test.kt index b5ee2f4f576..fd87882f5ec 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v2/AssetApiV2Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/AssetApiV2Test.kt @@ -19,9 +19,10 @@ package com.wire.kalium.api.v2 import com.wire.kalium.api.ApiTest -import com.wire.kalium.mocks.responses.asset.AssetUploadResponseJson -import com.wire.kalium.network.api.base.authenticated.asset.AssetApi +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.asset.AssetMocks import com.wire.kalium.network.api.authenticated.asset.AssetMetadataRequest +import com.wire.kalium.network.api.base.authenticated.asset.AssetApi import com.wire.kalium.network.api.model.AssetId import com.wire.kalium.network.api.model.AssetRetentionType import com.wire.kalium.network.api.model.UserId @@ -31,7 +32,6 @@ import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.isSuccessful import io.ktor.http.HttpStatusCode import io.ktor.utils.io.ByteReadChannel -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okio.Path.Companion.toPath import okio.Source @@ -41,7 +41,6 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) internal class AssetApiV2Test : ApiTest() { private val userId: UserId = UserId("user_id", "domain") @@ -54,7 +53,7 @@ internal class AssetApiV2Test : ApiTest() { val encryptedData = "some-data".encodeToByteArray() val encryptedDataSource = { getDummyDataSource(fileSystem, encryptedData) } val networkClient = mockAuthenticatedNetworkClient( - VALID_ASSET_UPLOAD_RESPONSE.rawJson, + VALID_ASSET_UPLOAD_RESPONSE.toJsonString(), statusCode = HttpStatusCode.Created, assertion = { assertPost() @@ -70,7 +69,7 @@ internal class AssetApiV2Test : ApiTest() { // Then assertTrue(response.isSuccessful()) - assertEquals(response.value, VALID_ASSET_UPLOAD_RESPONSE.serializableData) + assertEquals(response.value, VALID_ASSET_UPLOAD_RESPONSE) } @Test @@ -81,7 +80,7 @@ internal class AssetApiV2Test : ApiTest() { val encryptedData = "some-data".encodeToByteArray() val encryptedDataSource = { getDummyDataSource(fileSystem, encryptedData) } val networkClient = mockAuthenticatedNetworkClient( - INVALID_ASSET_UPLOAD_RESPONSE.rawJson, + INVALID_ASSET_UPLOAD_RESPONSE.toJsonString(), statusCode = HttpStatusCode.BadRequest, assertion = { assertPost() @@ -161,8 +160,8 @@ internal class AssetApiV2Test : ApiTest() { const val ASSET_KEY = "3-1-e7788668-1b22-488a-b63c-acede42f771f" const val ASSET_DOMAIN = "wire.com" const val ASSET_TOKEN = "assetToken" - val VALID_ASSET_UPLOAD_RESPONSE = AssetUploadResponseJson.valid - val INVALID_ASSET_UPLOAD_RESPONSE = AssetUploadResponseJson.invalid + val VALID_ASSET_UPLOAD_RESPONSE = AssetMocks.asset + val INVALID_ASSET_UPLOAD_RESPONSE = AssetMocks.invalid val assetId: AssetId = AssetId(ASSET_KEY, ASSET_DOMAIN) val tempFileSink = blackholeSink() } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationApiV2Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationApiV2Test.kt index e60bd61a637..4daea7c82fd 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationApiV2Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationApiV2Test.kt @@ -19,9 +19,10 @@ package com.wire.kalium.api.v2 import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.conversation.ConversationMocks import com.wire.kalium.mocks.responses.EventContentDTOJson import com.wire.kalium.mocks.responses.conversation.ConversationDetailsResponse -import com.wire.kalium.mocks.responses.conversation.ConversationListIdsResponseJson import com.wire.kalium.network.api.authenticated.conversation.AddConversationMembersRequest import com.wire.kalium.network.api.authenticated.conversation.ReceiptMode import com.wire.kalium.network.api.model.ConversationId @@ -43,7 +44,7 @@ internal class ConversationApiV2Test : ApiTest() { assertion = { assertPost() assertJson() - assertJsonBodyContent(CREATE_CONVERSATION_IDS_REQUEST.rawJson) + assertJsonBodyContent(CREATE_CONVERSATION_IDS_REQUEST.toJsonString()) assertPathEqual(PATH_CONVERSATIONS_LIST) } ) @@ -51,7 +52,7 @@ internal class ConversationApiV2Test : ApiTest() { val conversationApi = ConversationApiV2(networkClient) conversationApi.fetchConversationsListDetails( listOf( - ConversationId("ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", "anta.wire.link"), + ConversationMocks.conversationId, ConversationId("f4680835-2cfe-4d4d-8491-cbb201bd5c2b", "anta.wire.link") ) ) @@ -94,7 +95,7 @@ internal class ConversationApiV2Test : ApiTest() { const val PATH_CONVERSATIONS_LIST = "/conversations/list" const val PATH_CONVERSATIONS = "/conversations" const val PATH_MEMBERS = "members" - val CREATE_CONVERSATION_IDS_REQUEST = ConversationListIdsResponseJson.validRequestIds + val CREATE_CONVERSATION_IDS_REQUEST = ConversationMocks.conversationsDetailsRequest val CONVERSATION_DETAILS_RESPONSE = ConversationDetailsResponse.validGetDetailsForIds } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationResponseTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationResponseTest.kt new file mode 100644 index 00000000000..6a08761334d --- /dev/null +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v2/ConversationResponseTest.kt @@ -0,0 +1,209 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.api.v2 + +import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.responses.AnyResponseProvider +import com.wire.kalium.mocks.responses.conversation.ConversationDetailsResponse +import com.wire.kalium.network.api.model.ConversationAccessRoleDTO +import com.wire.kalium.network.api.v2.authenticated.ConversationApiV2 +import com.wire.kalium.network.utils.mapSuccess +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class ConversationResponseTest : ApiTest() { + + @Test + fun givenConversationResponseWithOnlyAccessRoleV2_whenMappingToConversation_thenItMapsCorrectly() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + conversationResponseWithAccessRoleV2.rawJson, + statusCode = HttpStatusCode.OK + ) + + val conversationApi = ConversationApiV2(networkClient) + + val response = conversationApi.fetchConversationsListDetails(listOf()) + + response.mapSuccess { + assertEquals( + setOf( + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.SERVICE + ), + it.conversationsFound.first().accessRole + ) + } + } + + @Test + fun givenConversationResponseWithBothAccessRole_whenMappingToConversation_thenItMapsCorrectly() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + conversationResponseWithBothAccessRole.rawJson, + statusCode = HttpStatusCode.OK + ) + + val conversationApi = ConversationApiV2(networkClient) + + val response = conversationApi.fetchConversationsListDetails(listOf()) + + response.mapSuccess { + assertEquals( + setOf( + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.SERVICE + ), + it.conversationsFound.first().accessRole + ) + } + } + + private companion object { + val conversationResponseWithAccessRoleV2 = AnyResponseProvider(data = "") { + """ + |{ + | "failed": [], + | "found": [ + | { + | "access": [ + | "invite" + | ], + | "access_role_v2": [ + | "team_member", + | "service" + | ], + | "creator": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", + | "last_event": "0.0", + | "last_event_time": "1970-01-01T00:00:00.000Z", + | "members": { + | "others": [ + | { + | "conversation_role": "wire_member", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613", + | "qualified_id": { + | "domain": "bella.wire.link", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613" + | }, + | "status": 0 + | } + | ], + | "self": { + | "conversation_role": "wire_admin", + | "hidden": false, + | "hidden_ref": null, + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "otr_archived": false, + | "otr_archived_ref": null, + | "otr_muted_ref": null, + | "otr_muted_status": null, + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" + | }, + | "service": null, + | "status": 0, + | "status_ref": "0.0", + | "status_time": "1970-01-01T00:00:00.000Z" + | } + | }, + | "message_timer": null, + | "name": "test-anta-grp", + | "protocol": "proteus", + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" + | }, + | "receipt_mode": null, + | "team": null, + | "type": 0 + | } + | ], + | "not_found": [] + |} + """.trimMargin() + } + + val conversationResponseWithBothAccessRole = AnyResponseProvider(data = "") { + """ + |{ + | "failed": [], + | "found": [ + | { + | "access": [ + | "invite" + | ], + | "access_role": "activated", + | "access_role_v2": [ + | "team_member", + | "service" + | ], + | "creator": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", + | "last_event": "0.0", + | "last_event_time": "1970-01-01T00:00:00.000Z", + | "members": { + | "others": [ + | { + | "conversation_role": "wire_member", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613", + | "qualified_id": { + | "domain": "bella.wire.link", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613" + | }, + | "status": 0 + | } + | ], + | "self": { + | "conversation_role": "wire_admin", + | "hidden": false, + | "hidden_ref": null, + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "otr_archived": false, + | "otr_archived_ref": null, + | "otr_muted_ref": null, + | "otr_muted_status": null, + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" + | }, + | "service": null, + | "status": 0, + | "status_ref": "0.0", + | "status_time": "1970-01-01T00:00:00.000Z" + | } + | }, + | "message_timer": null, + | "name": "test-anta-grp", + | "protocol": "proteus", + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" + | }, + | "receipt_mode": null, + | "team": null, + | "type": 0 + | } + | ], + | "not_found": [] + |} + """.trimMargin() + } + } +} diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationApiV3Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationApiV3Test.kt index 9239c4efb55..9e9b04fea39 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationApiV3Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationApiV3Test.kt @@ -33,17 +33,16 @@ import com.wire.kalium.network.api.v3.authenticated.ConversationApiV3 import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.isSuccessful import io.ktor.http.HttpStatusCode -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertIs import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) internal class ConversationApiV3Test : ApiTest() { @Test - fun givenACreateNewConversationRequest_whenCallingCreateNewConversaton_thenTheRequestShouldBeConfiguredOK() = runTest { + fun givenACreateNewConversationRequest_whenCallingCreateNewConversation_thenTheRequestShouldBeConfiguredOK() = runTest { val networkClient = mockAuthenticatedNetworkClient( CREATE_CONVERSATION_RESPONSE, statusCode = HttpStatusCode.Created, @@ -60,6 +59,28 @@ internal class ConversationApiV3Test : ApiTest() { assertTrue(result.isSuccessful()) } + @Test + fun givenCreateNewConversationRequest_whenCallingCreateNewConversation_thenDeprecatedResponseShouldBeConfiguredOK() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + CREATE_CONVERSATION_RESPONSE_WITH_SERVICES.rawJson, + statusCode = HttpStatusCode.Created, + assertion = { + assertJson() + assertPost() + assertPathEqual(PATH_CONVERSATIONS) + assertJsonBodyContent(CREATE_CONVERSATION_REQUEST_WITH_SERVICES.rawJson) + } + ) + + val conversationApi: ConversationApi = ConversationApiV3(networkClient) + val result = conversationApi.createNewConversation(CREATE_CONVERSATION_REQUEST_WITH_SERVICES.serializableData) + + assertEquals( + setOf(ConversationAccessRoleDTO.SERVICE), + (result as NetworkResponse.Success).value.accessRole + ) + } + @Test fun whenUpdatingAccessRole_thenTheRequestShouldBeConfiguredCorrectly() = runTest { val accessRoles = UpdateConversationAccessRequest( @@ -126,7 +147,15 @@ internal class ConversationApiV3Test : ApiTest() { private companion object { const val PATH_CONVERSATIONS = "/conversations" val CREATE_CONVERSATION_RESPONSE = ConversationResponseJson.v3.rawJson - val CREATE_CONVERSATION_REQUEST = CreateConversationRequestJson.v3 + val CREATE_CONVERSATION_REQUEST = CreateConversationRequestJson.v3() val ACCESS_ROLE_UPDATE_REQUEST = UpdateConversationAccessRequestJson.v3 + + val CREATE_CONVERSATION_RESPONSE_WITH_SERVICES = ConversationResponseJson.v0( + accessRole = setOf(ConversationAccessRoleDTO.SERVICE) + ) + + val CREATE_CONVERSATION_REQUEST_WITH_SERVICES = CreateConversationRequestJson.v3( + accessRole = listOf(ConversationAccessRoleDTO.SERVICE) + ) } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationV3ResponseTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationV3ResponseTest.kt new file mode 100644 index 00000000000..302f0a3d4f9 --- /dev/null +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v3/ConversationV3ResponseTest.kt @@ -0,0 +1,299 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.api.v3 + +import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.responses.AnyResponseProvider +import com.wire.kalium.network.api.model.ConversationAccessRoleDTO +import com.wire.kalium.network.api.v3.authenticated.ConversationApiV3 +import com.wire.kalium.network.utils.mapSuccess +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class ConversationV3ResponseTest : ApiTest() { + + @Test + fun givenConversationV3ResponseWithOnlyAccessRoleV2_whenMappingToConversation_thenItMapsCorrectly() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + conversationV3ResponseWithAccessRoleV2.rawJson, + statusCode = HttpStatusCode.OK + ) + + val conversationApiV3 = ConversationApiV3(networkClient) + + val response = conversationApiV3.fetchConversationsListDetails(listOf()) + + response.mapSuccess { + assertEquals( + setOf( + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.SERVICE + ), + it.conversationsFound.first().accessRole + ) + } + } + + @Test + fun givenConversationV3ResponseWithOnlyAccessRole_whenMappingToConversation_thenItMapsCorrectly() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + conversationV3ResponseWithAccessRole.rawJson, + statusCode = HttpStatusCode.OK + ) + + val conversationApiV3 = ConversationApiV3(networkClient) + + val response = conversationApiV3.fetchConversationsListDetails(listOf()) + + response.mapSuccess { + assertEquals( + setOf( + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.SERVICE + ), + it.conversationsFound.first().accessRole + ) + } + } + + @Test + fun givenConversationV3ResponseWithBothAccessRole_whenMappingToConversation_thenItMapsCorrectly() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + conversationV3ResponseWithBothAccessRole.rawJson, + statusCode = HttpStatusCode.OK + ) + + val conversationApiV3 = ConversationApiV3(networkClient) + + val response = conversationApiV3.fetchConversationsListDetails(listOf()) + + response.mapSuccess { + assertEquals( + setOf( + ConversationAccessRoleDTO.TEAM_MEMBER, + ConversationAccessRoleDTO.SERVICE + ), + it.conversationsFound.first().accessRole + ) + } + } + + private companion object { + val conversationV3ResponseWithAccessRoleV2 = AnyResponseProvider(data = "") { + """ + |{ + | "failed": [], + | "found": [ + | { + | "access": [ + | "invite" + | ], + | "access_role_v2": [ + | "team_member", + | "service" + | ], + | "creator": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", + | "last_event": "0.0", + | "last_event_time": "1970-01-01T00:00:00.000Z", + | "members": { + | "others": [ + | { + | "conversation_role": "wire_member", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613", + | "qualified_id": { + | "domain": "bella.wire.link", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613" + | }, + | "status": 0 + | } + | ], + | "self": { + | "conversation_role": "wire_admin", + | "hidden": false, + | "hidden_ref": null, + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "otr_archived": false, + | "otr_archived_ref": null, + | "otr_muted_ref": null, + | "otr_muted_status": null, + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" + | }, + | "service": null, + | "status": 0, + | "status_ref": "0.0", + | "status_time": "1970-01-01T00:00:00.000Z" + | } + | }, + | "message_timer": null, + | "name": "test-anta-grp", + | "protocol": "proteus", + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" + | }, + | "receipt_mode": null, + | "team": null, + | "type": 0 + | } + | ], + | "not_found": [] + |} + """.trimMargin() + } + + val conversationV3ResponseWithAccessRole = AnyResponseProvider(data = "") { + """ + |{ + | "failed": [], + | "found": [ + | { + | "access": [ + | "invite" + | ], + | "access_role": [ + | "team_member", + | "service" + | ], + | "creator": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", + | "last_event": "0.0", + | "last_event_time": "1970-01-01T00:00:00.000Z", + | "members": { + | "others": [ + | { + | "conversation_role": "wire_member", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613", + | "qualified_id": { + | "domain": "bella.wire.link", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613" + | }, + | "status": 0 + | } + | ], + | "self": { + | "conversation_role": "wire_admin", + | "hidden": false, + | "hidden_ref": null, + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "otr_archived": false, + | "otr_archived_ref": null, + | "otr_muted_ref": null, + | "otr_muted_status": null, + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" + | }, + | "service": null, + | "status": 0, + | "status_ref": "0.0", + | "status_time": "1970-01-01T00:00:00.000Z" + | } + | }, + | "message_timer": null, + | "name": "test-anta-grp", + | "protocol": "proteus", + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" + | }, + | "receipt_mode": null, + | "team": null, + | "type": 0 + | } + | ], + | "not_found": [] + |} + """.trimMargin() + } + + val conversationV3ResponseWithBothAccessRole = AnyResponseProvider(data = "") { + """ + |{ + | "failed": [], + | "found": [ + | { + | "access": [ + | "invite" + | ], + | "access_role": [ + | "team_member", + | "service" + | ], + | "access_role_v2": [ + | "team_member", + | "non_team_member", + | "service" + | ], + | "creator": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b", + | "last_event": "0.0", + | "last_event_time": "1970-01-01T00:00:00.000Z", + | "members": { + | "others": [ + | { + | "conversation_role": "wire_member", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613", + | "qualified_id": { + | "domain": "bella.wire.link", + | "id": "22dfd5cc-11ae-4a9d-9046-ba27585f4613" + | }, + | "status": 0 + | } + | ], + | "self": { + | "conversation_role": "wire_admin", + | "hidden": false, + | "hidden_ref": null, + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b", + | "otr_archived": false, + | "otr_archived_ref": null, + | "otr_muted_ref": null, + | "otr_muted_status": null, + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "f4680835-2cfe-4d4d-8491-cbb201bd5c2b" + | }, + | "service": null, + | "status": 0, + | "status_ref": "0.0", + | "status_time": "1970-01-01T00:00:00.000Z" + | } + | }, + | "message_timer": null, + | "name": "test-anta-grp", + | "protocol": "proteus", + | "qualified_id": { + | "domain": "anta.wire.link", + | "id": "ebafd3d4-1548-49f2-ac4e-b2757e6ca44b" + | }, + | "receipt_mode": null, + | "team": null, + | "type": 0 + | } + | ], + | "not_found": [] + |} + """.trimMargin() + } + } +} diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConnectionApiV4Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConnectionApiV4Test.kt index 0227b9d0807..e0cbfb4766d 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConnectionApiV4Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConnectionApiV4Test.kt @@ -20,7 +20,8 @@ package com.wire.kalium.api.v4 import com.wire.kalium.api.ApiTest import com.wire.kalium.api.json.model.ErrorResponseJson -import com.wire.kalium.mocks.responses.connection.ConnectionResponsesJson +import com.wire.kalium.mocks.extensions.toJsonString +import com.wire.kalium.mocks.mocks.connection.ConnectionMocks import com.wire.kalium.network.api.model.ErrorResponse import com.wire.kalium.network.api.model.UserId import com.wire.kalium.network.api.v4.authenticated.ConnectionApiV4 @@ -40,7 +41,7 @@ internal class ConnectionApiV4Test : ApiTest() { // given val userId = UserId("user_id", "domain_id") val httpClient = mockAuthenticatedNetworkClient( - CREATE_CONNECTION_RESPONSE.rawJson, + CREATE_CONNECTION_RESPONSE.toJsonString(), statusCode = HttpStatusCode.OK, assertion = { assertJson() @@ -93,6 +94,6 @@ internal class ConnectionApiV4Test : ApiTest() { private companion object { const val PATH_CONNECTIONS_ENDPOINT = "/connections" - val CREATE_CONNECTION_RESPONSE = ConnectionResponsesJson.CreateConnectionResponse.jsonProvider + val CREATE_CONNECTION_RESPONSE = ConnectionMocks.connection } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConversationApiV4Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConversationApiV4Test.kt index 8f7742c91ec..98b8fb41d6f 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConversationApiV4Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v4/ConversationApiV4Test.kt @@ -151,6 +151,6 @@ internal class ConversationApiV4Test : ApiTest() { const val PATH_CONVERSATIONS = "/conversations" const val PATH_MEMBERS = "members" const val PATH_TYPING_NOTIFICATION = "typing" - val CREATE_CONVERSATION_REQUEST = CreateConversationRequestJson.v3 + val CREATE_CONVERSATION_REQUEST = CreateConversationRequestJson.v3() } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v5/ConversationApiV5Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v5/ConversationApiV5Test.kt index a1e55d8cfd8..0ffdc7bb0ee 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/v5/ConversationApiV5Test.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v5/ConversationApiV5Test.kt @@ -211,6 +211,6 @@ internal class ConversationApiV5Test : ApiTest() { const val PATH_CONVERSATIONS = "/conversations" const val PATH_PROTOCOL = "protocol" val USER_ID = UserId("id", "domain") - val FETCH_CONVERSATION_RESPONSE = ConversationResponseJson.v0.rawJson + val FETCH_CONVERSATION_RESPONSE = ConversationResponseJson.v0().rawJson } } diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v7/ConversationApiV7Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v7/ConversationApiV7Test.kt new file mode 100644 index 00000000000..a9b2946f9c1 --- /dev/null +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v7/ConversationApiV7Test.kt @@ -0,0 +1,64 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.api.v7 + +import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.responses.conversation.ConversationResponseJson +import com.wire.kalium.network.api.model.UserId +import com.wire.kalium.network.api.v7.authenticated.ConversationApiV7 +import com.wire.kalium.network.utils.isSuccessful +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertTrue + +internal class ConversationApiV7Test : ApiTest() { + + @Test + fun whenCallingFetchMlsOneToOneConversation_thenTheRequestShouldBeConfiguredOK() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + FETCH_CONVERSATION_RESPONSE, + statusCode = HttpStatusCode.OK, + assertion = { + assertGet() + assertPathEqual("/one2one-conversations/${USER_ID.domain}/${USER_ID.value}") + } + ) + val conversationApi = ConversationApiV7(networkClient) + conversationApi.fetchMlsOneToOneConversation(USER_ID) + } + + @Test + fun given200Response_whenCallingFetchMlsOneToOneConversation_thenResponseIsParsedCorrectly() = + runTest { + val networkClient = mockAuthenticatedNetworkClient( + FETCH_CONVERSATION_RESPONSE, + statusCode = HttpStatusCode.OK + ) + val conversationApi = ConversationApiV7(networkClient) + + val fetchMlsOneToOneConversation = conversationApi.fetchMlsOneToOneConversation(USER_ID) + assertTrue(fetchMlsOneToOneConversation.isSuccessful()) + } + + companion object { + val USER_ID = UserId("id", "domain") + val FETCH_CONVERSATION_RESPONSE = ConversationResponseJson.v6.rawJson + } +} diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/v7/UpgradePersonalToTeamApiV7Test.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/v7/UpgradePersonalToTeamApiV7Test.kt new file mode 100644 index 00000000000..d4b852bed55 --- /dev/null +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/v7/UpgradePersonalToTeamApiV7Test.kt @@ -0,0 +1,97 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.api.v7 + +import com.wire.kalium.api.ApiTest +import com.wire.kalium.mocks.responses.MigrationUserToTeamResponseJson +import com.wire.kalium.network.api.v7.authenticated.UpgradePersonalToTeamApiV7 +import com.wire.kalium.network.utils.isSuccessful +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + + +internal class UpgradePersonalToTeamApiV7Test : ApiTest() { + + @Test + fun whenCallingUpgradePersonalToTeam_thenTheRequestShouldBeConfiguredOK() = runTest { + val networkClient = mockAuthenticatedNetworkClient( + SUCCESS_RESPONSE, + statusCode = HttpStatusCode.OK, + assertion = { + assertPost() + assertPathEqual("upgrade-personal-to-team") + } + ) + val upgradePersonalToTeamApi = UpgradePersonalToTeamApiV7(networkClient) + upgradePersonalToTeamApi.migrateToTeam(SUCCESS_RESPONSE) + } + + @Test + fun given200Response_whenCallingUpgradePersonalToTeam_thenResponseIsParsedCorrectly() = + runTest { + val networkClient = mockAuthenticatedNetworkClient( + SUCCESS_RESPONSE, + statusCode = HttpStatusCode.OK + ) + val upgradePersonalToTeamApi = UpgradePersonalToTeamApiV7(networkClient) + + val upgradePersonalToTeam = upgradePersonalToTeamApi.migrateToTeam(TEAM_NAME) + assertTrue(upgradePersonalToTeam.isSuccessful()) + } + + @Test + fun givenUserInTeamResponse_whenCallingUpgradePersonalToTeam_thenResponseIsParsedCorrectly() = + runTest { + val networkClient = mockAuthenticatedNetworkClient( + FAILED_USER_IN_TEAM_RESPONSE, + statusCode = HttpStatusCode.Forbidden + ) + val upgradePersonalToTeamApi = UpgradePersonalToTeamApiV7(networkClient) + + val upgradePersonalToTeam = upgradePersonalToTeamApi.migrateToTeam(TEAM_NAME) + assertFalse(upgradePersonalToTeam.isSuccessful()) + } + + @Test + fun givenUserNotFoundResponse_whenCallingUpgradePersonalToTeam_thenResponseIsParsedCorrectly() = + runTest { + val networkClient = mockAuthenticatedNetworkClient( + FAILED_USER_NOT_FOUND_RESPONSE, + statusCode = HttpStatusCode.NotFound + ) + val upgradePersonalToTeamApi = UpgradePersonalToTeamApiV7(networkClient) + + val upgradePersonalToTeam = upgradePersonalToTeamApi.migrateToTeam(TEAM_NAME) + assertFalse(upgradePersonalToTeam.isSuccessful()) + } + + companion object { + val SUCCESS_RESPONSE = + MigrationUserToTeamResponseJson.success.rawJson + val TEAM_NAME = + MigrationUserToTeamResponseJson.success.serializableData.teamName + val FAILED_USER_IN_TEAM_RESPONSE = + MigrationUserToTeamResponseJson.failedUserInTeam.rawJson + val FAILED_USER_NOT_FOUND_RESPONSE = + MigrationUserToTeamResponseJson.failedUserNotFound.rawJson + } +} \ No newline at end of file diff --git a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq index b09efc6e29a..48a8d2fe6fe 100644 --- a/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq +++ b/persistence/src/commonMain/db_global/com/wire/kalium/persistence/ServerConfiguration.sq @@ -35,6 +35,9 @@ UPDATE ServerConfiguration SET commonApiVersion = ? WHERE id = ?; updateApiVersionAndDomain: UPDATE ServerConfiguration SET commonApiVersion = ?, domain = ? WHERE id = ?; +getCommonApiVersionByDomain: +SELECT commonApiVersion FROM ServerConfiguration WHERE domain = ?; + updateLastBlackListCheckByIds: UPDATE ServerConfiguration SET lastBlackListCheck = ? WHERE id IN ?; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq index a2764f0a990..8a084426572 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Calls.sq @@ -15,6 +15,7 @@ CREATE TABLE Call ( CREATE INDEX call_date_index ON Call(created_at); CREATE INDEX call_conversation_index ON Call(conversation_id); CREATE INDEX call_caller_index ON Call(caller_id); +CREATE INDEX call_status ON Call(status); insertCall: INSERT INTO Call(conversation_id, id, status, caller_id, conversation_type, created_at, type) diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq new file mode 100644 index 00000000000..b521bb3c216 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetails.sq @@ -0,0 +1,156 @@ +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; + +selectAllConversationDetails: +SELECT * FROM ConversationDetails +WHERE + type IS NOT 'SELF' + AND ( + type IS 'GROUP' + OR (type IS 'ONE_ON_ONE' AND (name IS NOT NULL AND otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (type IS 'CONNECTION_PENDING' AND otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) + AND archived = :fromArchive + AND isActive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END +ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; + +selectConversationDetailsByQualifiedId: +SELECT * FROM ConversationDetails WHERE qualifiedId = ?; + +selectConversationDetailsByGroupId: +SELECT * FROM ConversationDetails WHERE mls_group_id = ?; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq new file mode 100644 index 00000000000..55b4081b6b2 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationDetailsWithEvents.sq @@ -0,0 +1,187 @@ +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + -- unread events + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.knocksCount + UnreadEventCountsGrouped.missedCallsCount + UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount + UnreadEventCountsGrouped.messagesCount) > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + Message.id AS lastMessageId, + Message.content_type AS lastMessageContentType, + Message.creation_date AS lastMessageDate, + Message.visibility AS lastMessageVisibility, + Message.sender_user_id AS lastMessageSenderUserId, + (Message.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (Message.sender_user_id IS NOT NULL AND Message.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON UnreadEventCountsGrouped.conversationId = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN LastMessage + ON LastMessage.conversation_id = ConversationDetails.qualifiedId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN Message + ON LastMessage.message_id = Message.id AND LastMessage.conversation_id = Message.conversation_id +LEFT JOIN User + ON Message.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.message_id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.message_id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.message_id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.message_id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.message_id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive; + +selectAllConversationDetailsWithEvents: +SELECT * FROM ConversationDetailsWithEvents +WHERE archived = :fromArchive + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END +ORDER BY + CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC; + +selectConversationDetailsWithEvents: +SELECT * FROM ConversationDetailsWithEvents +WHERE + archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END + ORDER BY + CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC + LIMIT :limit + OFFSET :offset; + +selectConversationDetailsWithEventsFromSearch: +SELECT * FROM ConversationDetailsWithEvents +WHERE + archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END + AND name LIKE ('%' || :searchQuery || '%') +ORDER BY + CASE WHEN :newActivitiesOnTop THEN hasNewActivitiesToShow ELSE 0 END DESC, + lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC +LIMIT :limit +OFFSET :offset; + +countConversationDetailsWithEvents: +SELECT COUNT(*) FROM ConversationDetails +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive + AND archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END; + +countConversationDetailsWithEventsFromSearch: +SELECT COUNT(*) FROM ConversationDetails +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive + AND archived = :fromArchive + AND CASE + -- When filter is ALL, do not apply additional filters on conversation type + WHEN :conversationFilter = 'ALL' THEN 1 = 1 + -- When filter is GROUPS, filter only group conversations + WHEN :conversationFilter = 'GROUPS' THEN type = 'GROUP' + -- When filter is ONE_ON_ONE, filter only one-on-one conversations + WHEN :conversationFilter = 'ONE_ON_ONE' THEN type = 'ONE_ON_ONE' + -- When filter is FAVORITES (future implementation) + ELSE 1 = 0 + END + AND CASE WHEN :onlyInteractionsEnabled THEN interactionEnabled = 1 ELSE 1 END + AND name LIKE ('%' || :searchQuery || '%'); diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq new file mode 100644 index 00000000000..e1e5d8f14cb --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/ConversationFolders.sq @@ -0,0 +1,64 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity; + +CREATE TABLE ConversationFolder ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + folder_type TEXT AS ConversationFolderTypeEntity NOT NULL +); + +CREATE TABLE LabeledConversation ( + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + folder_id TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, + + PRIMARY KEY (folder_id, conversation_id) +); + +getAllFoldersWithConversations: +SELECT + conversationFolder.id AS label_id, + conversationFolder.name AS label_name, + conversationFolder.folder_type AS label_type, + labeledConversation.conversation_id +FROM + ConversationFolder conversationFolder +LEFT JOIN + LabeledConversation labeledConversation ON conversationFolder.id = labeledConversation.folder_id +ORDER BY + conversationFolder.id; + +getConversationsFromFolder: +SELECT ConversationDetailsWithEvents.* +FROM LabeledConversation +JOIN ConversationDetailsWithEvents + ON LabeledConversation.conversation_id = ConversationDetailsWithEvents.qualifiedId +WHERE LabeledConversation.folder_id = :folderId + AND ConversationDetailsWithEvents.archived = 0 +ORDER BY + ConversationDetailsWithEvents.lastModifiedDate DESC, + name IS NULL, + name COLLATE NOCASE ASC; + +getFavoriteFolder: +SELECT * FROM ConversationFolder WHERE folder_type = 'FAVORITE' +LIMIT 1; + +upsertFolder: +INSERT INTO ConversationFolder(id, name, folder_type) +VALUES( ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET +name = excluded.name, +folder_type = excluded.folder_type; + +insertLabeledConversation: +INSERT OR IGNORE INTO LabeledConversation(conversation_id, folder_id) +VALUES(?, ?); + +deleteLabeledConversation: +DELETE FROM LabeledConversation WHERE conversation_id = ? AND folder_id = ?; + +clearFolders: +DELETE FROM ConversationFolder; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq index 699735f4426..62042685973 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Conversations.sq @@ -81,6 +81,7 @@ type = excluded.type, team_id = excluded.team_id, mls_group_id = excluded.mls_group_id, mls_epoch = excluded.mls_epoch, +mls_group_state = excluded.mls_group_state, protocol = excluded.protocol, muted_status = excluded.muted_status, muted_time = excluded.muted_time, @@ -166,128 +167,6 @@ UPDATE Conversation SET degraded_conversation_notified = ? WHERE qualified_id = :qualified_id; -CREATE VIEW IF NOT EXISTS ConversationDetails AS -SELECT -Conversation.qualified_id AS qualifiedId, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.name - WHEN 'CONNECTION_PENDING' THEN connection_user.name - ELSE Conversation.name -END AS name, -Conversation.type, -Call.status AS callStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.preview_asset_id - WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id -END AS previewAssetId, -Conversation.muted_status AS mutedStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.team - ELSE Conversation.team_id -END AS teamId, -CASE (Conversation.type) - WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date - ELSE Conversation.last_modified_date -END AS lastModifiedDate, -Conversation.last_read_date AS lastReadDate, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.user_availability_status - WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status -END AS userAvailabilityStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.user_type - WHEN 'CONNECTION_PENDING' THEN connection_user.user_type -END AS userType, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.bot_service - WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service -END AS botService, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.deleted - WHEN 'CONNECTION_PENDING' THEN connection_user.deleted -END AS userDeleted, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.defederated - WHEN 'CONNECTION_PENDING' THEN connection_user.defederated -END AS userDefederated, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.supported_protocols - WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols -END AS userSupportedProtocols, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.connection_status - WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status -END AS connectionStatus, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.qualified_id - WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id -END AS otherUserId, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id - WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id -END AS otherUserActiveConversationId, -CASE - WHEN ((SELECT id FROM SelfUser LIMIT 1) LIKE (Conversation.creator_id || '@%')) THEN 1 - ELSE 0 -END AS isCreator, -CASE (Conversation.type) - WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) - ELSE 1 -END AS isActive, -Conversation.last_notified_date AS lastNotifiedMessageDate, -memberRole. role AS selfRole, -Conversation.protocol, -Conversation.mls_cipher_suite, -Conversation.mls_epoch, -Conversation.mls_group_id, -Conversation.mls_last_keying_material_update_date, -Conversation.mls_group_state, -Conversation.access_list, -Conversation.access_role_list, -Conversation.team_id, -Conversation.mls_proposal_timer, -Conversation.muted_time, -Conversation.creator_id, -Conversation.last_modified_date, -Conversation.receipt_mode, -Conversation.message_timer, -Conversation.user_message_timer, -Conversation.incomplete_metadata, -Conversation.archived, -Conversation.archived_date_time, -Conversation.verification_status AS mls_verification_status, -Conversation.proteus_verification_status, -Conversation.legal_hold_status -FROM Conversation -LEFT JOIN Member ON Conversation.qualified_id = Member.conversation - AND Conversation.type IS 'ONE_ON_ONE' - AND Member.user IS NOT (SELECT SelfUser.id FROM SelfUser LIMIT 1) -LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation - AND memberRole.user IS (SELECT SelfUser.id FROM SelfUser LIMIT 1) -LEFT JOIN User ON User.qualified_id = Member.user -LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id - AND (Connection.status = 'SENT' - OR Connection.status = 'PENDING' - OR Connection.status = 'NOT_CONNECTED' - AND Conversation.type IS 'CONNECTION_PENDING') -LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id -LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1); - -selectAllConversationDetails: -SELECT * FROM ConversationDetails -WHERE - type IS NOT 'SELF' - AND ( - type IS 'GROUP' - OR (type IS 'ONE_ON_ONE' AND (name IS NOT NULL AND otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata - OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic - OR (type IS 'CONNECTION_PENDING' AND otherUserId IS NOT NULL) -- show connection requests even without metadata - ) - AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) - AND archived = :fromArchive - AND isActive -ORDER BY lastModifiedDate DESC, name IS NULL, name COLLATE NOCASE ASC; - selectAllConversations: SELECT * FROM Conversation WHERE type IS NOT 'CONNECTION_PENDING' ORDER BY last_modified_date DESC, name ASC; @@ -300,7 +179,7 @@ FROM Conversation WHERE type IS 'GROUP' AND protocol IS 'MIXED' AND team_id = ? AND memberCount = mlsCapableMemberCount; selectByQualifiedId: -SELECT * FROM ConversationDetails WHERE qualifiedId = ?; +SELECT * FROM Conversation WHERE qualified_id = ?; selectConversationByQualifiedId: SELECT @@ -312,10 +191,10 @@ SELECT protocol, mls_group_id, mls_group_state, mls_epoch , mls_last_keying_material_update_date, mls_cipher_suite FROM Conversation WHERE qualified_id = ?; selectReceiptModeFromGroupConversationByQualifiedId: -SELECT receipt_mode FROM ConversationDetails WHERE qualifiedId = ? AND type IS 'GROUP'; +SELECT receipt_mode FROM Conversation WHERE qualified_id = ? AND type IS 'GROUP'; selectByGroupId: -SELECT * FROM ConversationDetails WHERE mls_group_id = ?; +SELECT * FROM Conversation WHERE mls_group_id = ?; selectByGroupState: SELECT * FROM Conversation WHERE mls_group_state = ? AND (protocol IS 'MLS' OR protocol IS 'MIXED'); diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/DumpContent.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/DumpContent.sq index 2393310f240..4ebb5bbd3a3 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/DumpContent.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/DumpContent.sq @@ -76,7 +76,8 @@ INSERT INTO MessageFailedToDecryptContent SELECT local_db.MessageFailedToDecryptContent.message_id, local_db.MessageFailedToDecryptContent.conversation_id, local_db.MessageFailedToDecryptContent.unknown_encoded_data, - local_db.MessageFailedToDecryptContent.is_decryption_resolved + local_db.MessageFailedToDecryptContent.is_decryption_resolved, + local_db.MessageFailedToDecryptContent.error_code FROM local_db.MessageFailedToDecryptContent LEFT JOIN selfdelete_message_id ON local_db.MessageFailedToDecryptContent.message_id = selfdelete_message_id.id diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq new file mode 100644 index 00000000000..d9c63f6e6b6 --- /dev/null +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/LastMessage.sq @@ -0,0 +1,111 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.ConversationEntity; +import com.wire.kalium.persistence.dao.message.MessageEntity.ContentType; +import com.wire.kalium.persistence.dao.message.MessageEntity.FederationType; +import com.wire.kalium.persistence.dao.message.MessageEntity.LegalHoldType; +import com.wire.kalium.persistence.dao.message.MessageEntity.MemberChangeType; +import com.wire.kalium.persistence.dao.message.MessageEntity; +import com.wire.kalium.persistence.dao.message.RecipientFailureTypeEntity; +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE TABLE LastMessage ( + conversation_id TEXT AS QualifiedIDEntity, + message_id TEXT, + creation_date INTEGER AS Instant, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + PRIMARY KEY (conversation_id) +); + +-- update last message when newly inserted message is newer than the current last message +CREATE TRIGGER updateLastMessageAfterInsertingNewMessage +AFTER INSERT ON Message +WHEN + new.visibility IN ('VISIBLE', 'DELETED') + AND new.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + VALUES (new.conversation_id, new.id, new.creation_date) + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after deleting the current last message by finding new last message for the conversation +CREATE TRIGGER updateLastMessageAfterDeletingLastMessage +AFTER DELETE ON LastMessage +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after a message got updated and now this one should be the new last message +-- or if the current last message shouldn't be the last message anymore because of the visibility update for instance +CREATE TRIGGER updateLastMessageAfterUpdatingMessage +AFTER UPDATE OF id, conversation_id, visibility, content_type, creation_date ON Message +WHEN + new.creation_date >= (SELECT creation_date FROM LastMessage WHERE conversation_id = new.conversation_id LIMIT 1) +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + new.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after there was a foreign key updated to null by finding new last message for that conversation +-- this happens when last message is moved to another conversation or id of last message is changed +CREATE TRIGGER updateLastMessageAfterForeignKeyUpdatedToNull +AFTER UPDATE OF conversation_id ON LastMessage +WHEN + new.conversation_id IS NULL +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; + DELETE FROM LastMessage WHERE conversation_id IS NULL; +END; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq index df890ee58e8..06e927cdaf3 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDetailsView.sq @@ -69,6 +69,7 @@ RestrictedAssetContent.asset_mime_type AS restrictedAssetMimeType, RestrictedAssetContent.asset_size AS restrictedAssetSize, RestrictedAssetContent.asset_name AS restrictedAssetName, FailedToDecryptContent.unknown_encoded_data AS failedToDecryptData, +FailedToDecryptContent.error_code AS decryptionErrorCode, FailedToDecryptContent.is_decryption_resolved AS isDecryptionResolved, ConversationNameChangedContent.conversation_name AS conversationName, '{' || IFNULL( diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq index a89e37fe693..1ec7f1e3097 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/MessageDrafts.sq @@ -22,13 +22,21 @@ upsertDraft: INSERT INTO MessageDraft(conversation_id, text, edit_message_id, quoted_message_id, mention_list) VALUES( ?, ?, ?, ?, ?) ON CONFLICT(conversation_id) DO UPDATE SET -text = excluded.text, -edit_message_id = excluded.edit_message_id, -quoted_message_id = excluded.quoted_message_id, -mention_list = excluded.mention_list; + text = excluded.text, + edit_message_id = excluded.edit_message_id, + quoted_message_id = excluded.quoted_message_id, + mention_list = excluded.mention_list +WHERE -- execute the update only if any of the fields changed + MessageDraft.text != excluded.text + OR MessageDraft.edit_message_id IS NOT excluded.edit_message_id + OR MessageDraft.quoted_message_id IS NOT excluded.quoted_message_id + OR MessageDraft.mention_list != excluded.mention_list; getDraft: SELECT * FROM MessageDraft WHERE conversation_id = ?; getDrafts: SELECT * FROM MessageDraft; + +selectChanges: +SELECT changes(); diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq index 82764a8a468..90308374887 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Messages.sq @@ -160,6 +160,7 @@ CREATE TABLE MessageFailedToDecryptContent ( unknown_encoded_data BLOB, is_decryption_resolved INTEGER AS Boolean NOT NULL DEFAULT(0), + error_code INTEGER, FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (message_id, conversation_id) @@ -410,8 +411,8 @@ INSERT OR IGNORE INTO MessageUnknownContent(message_id, conversation_id, unknown VALUES(?, ?, ?, ?); insertFailedDecryptionMessageContent: -INSERT OR IGNORE INTO MessageFailedToDecryptContent(message_id, conversation_id, unknown_encoded_data) -VALUES(?, ?, ?); +INSERT OR IGNORE INTO MessageFailedToDecryptContent(message_id, conversation_id, unknown_encoded_data, error_code) +VALUES(?, ?, ?, ?); insertMissedCallMessage: INSERT OR IGNORE INTO MessageMissedCallContent(message_id, conversation_id, caller_id) @@ -487,6 +488,9 @@ UPDATE Message SET id = :newId WHERE id = :oldId AND conversation_id = :conversationId; +getMessage: +SELECT * FROM Message WHERE id = ? AND conversation_id = ?; + selectById: SELECT * FROM MessageDetailsView WHERE id = ? AND conversationId = ?; @@ -497,14 +501,11 @@ selectByConversationIdAndVisibility: SELECT * FROM MessageDetailsView WHERE conversationId = :conversationId AND visibility IN :visibility ORDER BY date DESC LIMIT :limit OFFSET :offset; selectLastMessagesByConversationIds: -SELECT MessageDetailsView.* FROM MessageDetailsView -JOIN ( - SELECT conversationId, MAX(date) AS latest_date - FROM MessageDetailsView - WHERE conversationId IN :conversationIds - GROUP BY conversationId -) AS lastMessage -ON MessageDetailsView.conversationId = lastMessage.conversationId AND MessageDetailsView.date = lastMessage.latest_date; +SELECT MessageDetailsView.* +FROM LastMessage +INNER JOIN MessageDetailsView +ON MessageDetailsView.conversationId = LastMessage.conversation_id AND MessageDetailsView.id = LastMessage.message_id +WHERE LastMessage.conversation_id IN :conversationIds; selectMessagesByConversationIdAndVisibilityAfterDate: SELECT * FROM MessageDetailsView WHERE MessageDetailsView.conversationId = ? AND visibility IN ? AND date > ? ORDER BY date DESC; @@ -558,11 +559,9 @@ WHERE conversation_id = :conversation_id AND status = 'PENDING'; selectPendingEphemeralMessages: SELECT * FROM MessageDetailsView WHERE expireAfterMillis NOT NULL +AND selfDeletionEndDate NOT NULL AND visibility = "VISIBLE" -AND ( - selfDeletionEndDate IS NULL - OR selfDeletionEndDate > STRFTIME('%s', 'now') * 1000 -- Checks if message end date is higher than current time in millis -); +AND selfDeletionEndDate > STRFTIME('%s', 'now') * 1000; -- Checks if message end date is higher than current time in millis selectAlreadyEndedEphemeralMessages: SELECT * FROM MessageDetailsView diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq index 3aac7289142..50e7c3a23bb 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Reactions.sq @@ -44,7 +44,8 @@ AS SELECT User.user_type AS userType, User.deleted, User.connection_status AS connectionStatus, - User.user_availability_status AS userAvailabilityStatus + User.user_availability_status AS userAvailabilityStatus, + User.accent_id AS accentId FROM Reaction INNER JOIN User ON User.qualified_id = Reaction.sender_id diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq index c416f0c61ea..4476c480f7c 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Receipts.sq @@ -45,7 +45,8 @@ AS SELECT User.user_type AS userType, User.deleted AS isUserDeleted, User.connection_status AS connectionStatus, - User.user_availability_status AS userAvailabilityStatus + User.user_availability_status AS userAvailabilityStatus, + User.accent_id AS accentId FROM Receipt INNER JOIN User ON User.qualified_id = Receipt.user_id diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq index 020b4745972..ac20a8a5d00 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/UnreadEvents.sq @@ -11,6 +11,9 @@ CREATE TABLE UnreadEvent ( FOREIGN KEY (id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE CASCADE, PRIMARY KEY (id, conversation_id) ); +CREATE INDEX unread_event_conversation ON UnreadEvent(conversation_id); +CREATE INDEX unread_event_date ON UnreadEvent(creation_date); +CREATE INDEX unread_event_type ON UnreadEvent(type); deleteUnreadEvent: DELETE FROM UnreadEvent WHERE id = ? AND conversation_id = ?; @@ -30,7 +33,15 @@ WHERE id = :id AND conversation_id = :conversation_id; getUnreadEvents: SELECT conversation_id, type FROM UnreadEvent; -getConversationsUnreadEvents: +getConversationUnreadEventsCount: +SELECT COUNT(*) FROM UnreadEvent WHERE conversation_id = ?; + +getUnreadArchivedConversationsCount: +SELECT COUNT(DISTINCT ue.conversation_id) FROM UnreadEvent ue +INNER JOIN Conversation c ON ue.conversation_id = c.qualified_id +WHERE c.archived = 1; + +CREATE VIEW IF NOT EXISTS UnreadEventCountsGrouped AS SELECT conversation_id AS conversationId, SUM(CASE WHEN type = 'KNOCK' THEN 1 ELSE 0 END) AS knocksCount, @@ -41,13 +52,5 @@ SELECT FROM UnreadEvent GROUP BY conversation_id; -getPaginatedUnreadEvents: -SELECT * FROM UnreadEvent LIMIT ? OFFSET ?; - -getConversationUnreadEventsCount: -SELECT COUNT(*) FROM UnreadEvent WHERE conversation_id = ?; - -getUnreadArchivedConversationsCount: -SELECT COUNT(DISTINCT ue.conversation_id) FROM UnreadEvent ue -INNER JOIN Conversation c ON ue.conversation_id = c.qualified_id -WHERE c.archived = 1; +getConversationsUnreadEventCountsGrouped: +SELECT * FROM UnreadEventCountsGrouped; diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq index cf6e7b3aba5..385d13f2602 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/Users.sq @@ -173,7 +173,7 @@ selectDetailsByQualifiedId: SELECT * FROM UserDetails WHERE qualified_id IN ?; selectMinimizedByQualifiedId: -SELECT qualified_id, name, complete_asset_id, user_type FROM User WHERE qualified_id IN ?; +SELECT qualified_id, name, complete_asset_id, user_type, accent_id FROM User WHERE qualified_id IN ?; selectWithTeamByQualifiedId: SELECT * FROM UserDetails LEFT JOIN Team ON UserDetails.team == Team.id WHERE UserDetails.qualified_id IN ?; @@ -270,3 +270,9 @@ WHERE m.user = :userId selectOneOnOnConversationId: SELECT active_one_on_one_conversation_id FROM User WHERE qualified_id = :userId; + +selectNamesAndHandle: +SELECT name, handle FROM User WHERE qualified_id = :userId; + +updateTeamId: +UPDATE User SET team = ? WHERE qualified_id = ?; diff --git a/persistence/src/commonMain/db_user/migrations/84.sqm b/persistence/src/commonMain/db_user/migrations/84.sqm new file mode 100644 index 00000000000..dc4d60413dd --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/84.sqm @@ -0,0 +1,112 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN ((SELECT id FROM SelfUser LIMIT 1) LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.team_id, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.last_modified_date, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status +FROM Conversation +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT (SELECT SelfUser.id FROM SelfUser LIMIT 1) +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS (SELECT SelfUser.id FROM SelfUser LIMIT 1) +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1); diff --git a/persistence/src/commonMain/db_user/migrations/85.sqm b/persistence/src/commonMain/db_user/migrations/85.sqm new file mode 100644 index 00000000000..34dbb0c0089 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/85.sqm @@ -0,0 +1,41 @@ +DROP VIEW IF EXISTS ReceiptDetails; +DROP VIEW IF EXISTS MessageDetailsReactions; + +CREATE VIEW IF NOT EXISTS ReceiptDetails +AS SELECT + Receipt.type, + Receipt.date, + Receipt.message_id AS messageId, + Receipt.conversation_id AS conversationId, + User.qualified_id AS userId, + User.name AS userName, + User.handle AS userHandle, + User.preview_asset_id AS previewAssetId, + User.user_type AS userType, + User.deleted AS isUserDeleted, + User.connection_status AS connectionStatus, + User.user_availability_status AS userAvailabilityStatus, + User.accent_id AS accentId +FROM + Receipt +INNER JOIN User ON User.qualified_id = Receipt.user_id +ORDER BY User.name; + +CREATE VIEW IF NOT EXISTS MessageDetailsReactions +AS SELECT + Reaction.emoji, + Reaction.message_id AS messageId, + Reaction.conversation_id AS conversationId, + User.qualified_id AS userId, + User.name, + User.handle, + User.preview_asset_id AS previewAssetId, + User.user_type AS userType, + User.deleted, + User.connection_status AS connectionStatus, + User.user_availability_status AS userAvailabilityStatus, + User.accent_id AS accentId +FROM + Reaction +INNER JOIN User ON User.qualified_id = Reaction.sender_id +ORDER BY Reaction.emoji; diff --git a/persistence/src/commonMain/db_user/migrations/86.sqm b/persistence/src/commonMain/db_user/migrations/86.sqm new file mode 100644 index 00000000000..92bfc8fb8cf --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/86.sqm @@ -0,0 +1,166 @@ +ALTER TABLE MessageFailedToDecryptContent ADD COLUMN error_code INTEGER; + +DROP VIEW IF EXISTS MessageDetailsView; + +CREATE VIEW IF NOT EXISTS MessageDetailsView +AS SELECT +Message.id AS id, +Message.conversation_id AS conversationId, +Message.content_type AS contentType, +Message.creation_date AS date, +Message.sender_user_id AS senderUserId, +Message.sender_client_id AS senderClientId, +Message.status AS status, +Message.last_edit_date AS lastEditTimestamp, +Message.visibility AS visibility, +Message.expects_read_confirmation AS expectsReadConfirmation, +Message.expire_after_millis AS expireAfterMillis, +Message.self_deletion_end_date AS selfDeletionEndDate, +IFNULL ((SELECT COUNT (*) FROM Receipt WHERE message_id = Message.id AND type = 'READ'), 0) AS readCount, +UserDetails.name AS senderName, +UserDetails.handle AS senderHandle, +UserDetails.email AS senderEmail, +UserDetails.phone AS senderPhone, +UserDetails.accent_id AS senderAccentId, +UserDetails.team AS senderTeamId, +UserDetails.connection_status AS senderConnectionStatus, +UserDetails.preview_asset_id AS senderPreviewAssetId, +UserDetails.complete_asset_id AS senderCompleteAssetId, +UserDetails.user_availability_status AS senderAvailabilityStatus, +UserDetails.user_type AS senderUserType, +UserDetails.bot_service AS senderBotService, +UserDetails.deleted AS senderIsDeleted, +UserDetails.expires_at AS senderExpiresAt, +UserDetails.defederated AS senderDefederated, +UserDetails.supported_protocols AS senderSupportedProtocols, +UserDetails.active_one_on_one_conversation_id AS senderActiveOneOnOneConversationId, +UserDetails.is_proteus_verified AS senderIsProteusVerified, +UserDetails.is_under_legal_hold AS senderIsUnderLegalHold, +(Message.sender_user_id == SelfUser.id) AS isSelfMessage, +TextContent.text_body AS text, +TextContent.is_quoting_self AS isQuotingSelfUser, +AssetContent.asset_size AS assetSize, +AssetContent.asset_name AS assetName, +AssetContent.asset_mime_type AS assetMimeType, +AssetContent.asset_otr_key AS assetOtrKey, +AssetContent.asset_sha256 AS assetSha256, +AssetContent.asset_id AS assetId, +AssetContent.asset_token AS assetToken, +AssetContent.asset_domain AS assetDomain, +AssetContent.asset_encryption_algorithm AS assetEncryptionAlgorithm, +AssetContent.asset_width AS assetWidth, +AssetContent.asset_height AS assetHeight, +AssetContent.asset_duration_ms AS assetDuration, +AssetContent.asset_normalized_loudness AS assetNormalizedLoudness, +MissedCallContent.caller_id AS callerId, +MemberChangeContent.member_change_list AS memberChangeList, +MemberChangeContent.member_change_type AS memberChangeType, +UnknownContent.unknown_type_name AS unknownContentTypeName, +UnknownContent.unknown_encoded_data AS unknownContentData, +RestrictedAssetContent.asset_mime_type AS restrictedAssetMimeType, +RestrictedAssetContent.asset_size AS restrictedAssetSize, +RestrictedAssetContent.asset_name AS restrictedAssetName, +FailedToDecryptContent.unknown_encoded_data AS failedToDecryptData, +FailedToDecryptContent.error_code AS decryptionErrorCode, +FailedToDecryptContent.is_decryption_resolved AS isDecryptionResolved, +ConversationNameChangedContent.conversation_name AS conversationName, +'{' || IFNULL( + (SELECT GROUP_CONCAT('"' || emoji || '":' || count) + FROM ( + SELECT COUNT(*) count, Reaction.emoji emoji + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + GROUP BY Reaction.emoji + )), + '') +|| '}' AS allReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT('"' || Reaction.emoji || '"') || ']' + FROM Reaction + WHERE Reaction.message_id = Message.id + AND Reaction.conversation_id = Message.conversation_id + AND Reaction.sender_id = SelfUser.id + ), + '[]' +) AS selfReactionsJson, +IFNULL( + (SELECT '[' || GROUP_CONCAT( + '{"start":' || start || ', "length":' || length || + ', "userId":{"value":"' || replace(substr(user_id, 0, instr(user_id, '@')), '@', '') || '"' || + ',"domain":"' || replace(substr(user_id, instr(user_id, '@')+1, length(user_id)), '@', '') || '"' || + '}' || '}') || ']' + FROM MessageMention + WHERE MessageMention.message_id = Message.id + AND MessageMention.conversation_id = Message.conversation_id + ), + '[]' +) AS mentions, +TextContent.quoted_message_id AS quotedMessageId, +QuotedMessage.sender_user_id AS quotedSenderId, +TextContent.is_quote_verified AS isQuoteVerified, +QuotedSender.name AS quotedSenderName, +QuotedMessage.creation_date AS quotedMessageDateTime, +QuotedMessage.last_edit_date AS quotedMessageEditTimestamp, +QuotedMessage.visibility AS quotedMessageVisibility, +QuotedMessage.content_type AS quotedMessageContentType, +QuotedTextContent.text_body AS quotedTextBody, +QuotedAssetContent.asset_mime_type AS quotedAssetMimeType, +QuotedAssetContent.asset_name AS quotedAssetName, +QuotedLocationContent.name AS quotedLocationName, + +NewConversationReceiptMode.receipt_mode AS newConversationReceiptMode, + +ConversationReceiptModeChanged.receipt_mode AS conversationReceiptModeChanged, +ConversationTimerChangedContent.message_timer AS messageTimerChanged, +FailedRecipientsWithNoClients.recipient_failure_list AS recipientsFailedWithNoClientsList, +FailedRecipientsDeliveryFailed.recipient_failure_list AS recipientsFailedDeliveryList, + +IFNULL( + (SELECT '[' || + GROUP_CONCAT('{"text":"' || text || '", "id":"' || id || '""is_selected":' || is_selected || '}') + || ']' + FROM ButtonContent + WHERE ButtonContent.message_id = Message.id + AND ButtonContent.conversation_id = Message.conversation_id + ), + '[]' +) AS buttonsJson, +FederationTerminatedContent.domain_list AS federationDomainList, +FederationTerminatedContent.federation_type AS federationType, +ConversationProtocolChangedContent.protocol AS conversationProtocolChanged, +ConversationLocationContent.latitude AS latitude, +ConversationLocationContent.longitude AS longitude, +ConversationLocationContent.name AS locationName, +ConversationLocationContent.zoom AS locationZoom, +LegalHoldContent.legal_hold_member_list AS legalHoldMemberList, +LegalHoldContent.legal_hold_type AS legalHoldType + +FROM Message +JOIN UserDetails ON Message.sender_user_id = UserDetails.qualified_id +LEFT JOIN MessageTextContent AS TextContent ON Message.id = TextContent.message_id AND Message.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent ON Message.id = AssetContent.message_id AND Message.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageMissedCallContent AS MissedCallContent ON Message.id = MissedCallContent.message_id AND Message.conversation_id = MissedCallContent.conversation_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent ON Message.id = MemberChangeContent.message_id AND Message.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageUnknownContent AS UnknownContent ON Message.id = UnknownContent.message_id AND Message.conversation_id = UnknownContent.conversation_id +LEFT JOIN MessageRestrictedAssetContent AS RestrictedAssetContent ON Message.id = RestrictedAssetContent.message_id AND RestrictedAssetContent.conversation_id = RestrictedAssetContent.conversation_id +LEFT JOIN MessageFailedToDecryptContent AS FailedToDecryptContent ON Message.id = FailedToDecryptContent.message_id AND Message.conversation_id = FailedToDecryptContent.conversation_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent ON Message.id = ConversationNameChangedContent.message_id AND Message.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageRecipientFailure AS FailedRecipientsWithNoClients ON Message.id = FailedRecipientsWithNoClients.message_id AND Message.conversation_id = FailedRecipientsWithNoClients.conversation_id AND FailedRecipientsWithNoClients.recipient_failure_type = 'NO_CLIENTS_TO_DELIVER' +LEFT JOIN MessageRecipientFailure AS FailedRecipientsDeliveryFailed ON Message.id = FailedRecipientsDeliveryFailed.message_id AND Message.conversation_id = FailedRecipientsDeliveryFailed.conversation_id AND FailedRecipientsDeliveryFailed.recipient_failure_type = 'MESSAGE_DELIVERY_FAILED' + +-- joins for quoted messages +LEFT JOIN Message AS QuotedMessage ON QuotedMessage.id = TextContent.quoted_message_id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN User AS QuotedSender ON QuotedMessage.sender_user_id = QuotedSender.qualified_id +LEFT JOIN MessageTextContent AS QuotedTextContent ON QuotedTextContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageAssetContent AS QuotedAssetContent ON QuotedAssetContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS QuotedLocationContent ON QuotedLocationContent.message_id = QuotedMessage.id AND QuotedMessage.conversation_id = TextContent.conversation_id +-- end joins for quoted messages +LEFT JOIN MessageNewConversationReceiptModeContent AS NewConversationReceiptMode ON Message.id = NewConversationReceiptMode.message_id AND Message.conversation_id = NewConversationReceiptMode.conversation_id +LEFT JOIN MessageConversationReceiptModeChangedContent AS ConversationReceiptModeChanged ON Message.id = ConversationReceiptModeChanged.message_id AND Message.conversation_id = ConversationReceiptModeChanged.conversation_id +LEFT JOIN MessageConversationTimerChangedContent AS ConversationTimerChangedContent ON Message.id = ConversationTimerChangedContent.message_id AND Message.conversation_id = ConversationTimerChangedContent.conversation_id +LEFT JOIN MessageFederationTerminatedContent AS FederationTerminatedContent ON Message.id = FederationTerminatedContent.message_id AND Message.conversation_id = FederationTerminatedContent.conversation_id +LEFT JOIN MessageConversationProtocolChangedContent AS ConversationProtocolChangedContent ON Message.id = ConversationProtocolChangedContent.message_id AND Message.conversation_id = ConversationProtocolChangedContent.conversation_id +LEFT JOIN MessageConversationLocationContent AS ConversationLocationContent ON Message.id = ConversationLocationContent.message_id AND Message.conversation_id = ConversationLocationContent.conversation_id +LEFT JOIN MessageLegalHoldContent AS LegalHoldContent ON Message.id = LegalHoldContent.message_id AND Message.conversation_id = LegalHoldContent.conversation_id +LEFT JOIN SelfUser; diff --git a/persistence/src/commonMain/db_user/migrations/87.sqm b/persistence/src/commonMain/db_user/migrations/87.sqm new file mode 100644 index 00000000000..60285644325 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/87.sqm @@ -0,0 +1,129 @@ +CREATE VIEW IF NOT EXISTS LastMessagePreview +AS SELECT + MessagePreview.id AS id, + MessagePreview.conversationId AS conversationId, + MessagePreview.contentType AS contentType, + MessagePreview.date AS date, + MessagePreview.visibility AS visibility, + MessagePreview.senderUserId AS senderUserId, + MessagePreview.isEphemeral AS isEphemeral, + MessagePreview.senderName AS senderName, + MessagePreview.senderConnectionStatus AS senderConnectionStatus, + MessagePreview.senderIsDeleted AS senderIsDeleted, + MessagePreview.selfUserId AS selfUserId, + MessagePreview.isSelfMessage AS isSelfMessage, + MessagePreview.memberChangeList AS memberChangeList, + MessagePreview.memberChangeType AS memberChangeType, + MessagePreview.updateConversationName AS updateConversationName, + MessagePreview.conversationName AS conversationName, + MessagePreview.isMentioningSelfUser AS isMentioningSelfUser, + MessagePreview.isQuotingSelfUser AS isQuotingSelfUser, + MessagePreview.text AS text, + MessagePreview.assetMimeType AS assetMimeType, + MessagePreview.isUnread AS isUnread, + MessagePreview.shouldNotify AS shouldNotify, + MessagePreview.mutedStatus AS mutedStatus, + MessagePreview.conversationType AS conversationType +FROM MessagePreview +WHERE MessagePreview.id IN ( + SELECT id FROM Message + WHERE + Message.visibility IN ('VISIBLE', 'DELETED') AND + Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + GROUP BY Message.conversation_id + HAVING Message.creation_date = MAX(Message.creation_date) +); + +CREATE VIEW IF NOT EXISTS UnreadEventCountsGrouped AS +SELECT + conversation_id AS conversationId, + SUM(CASE WHEN type = 'KNOCK' THEN 1 ELSE 0 END) AS knocksCount, + SUM(CASE WHEN type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS missedCallsCount, + SUM(CASE WHEN type = 'MENTION' THEN 1 ELSE 0 END) AS mentionsCount, + SUM(CASE WHEN type = 'REPLY' THEN 1 ELSE 0 END) AS repliesCount, + SUM(CASE WHEN type = 'MESSAGE' THEN 1 ELSE 0 END) AS messagesCount +FROM UnreadEvent +GROUP BY conversation_id; + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + CASE + WHEN ConversationDetails.type = 'GROUP' THEN + CASE + WHEN ConversationDetails.selfRole IS NOT NULL THEN 1 + ELSE 0 + END + WHEN ConversationDetails.type = 'ONE_ON_ONE' THEN + CASE + WHEN userDefederated = 1 THEN 0 + WHEN userDeleted = 1 THEN 0 + WHEN connectionStatus = 'BLOCKED' THEN 0 + WHEN legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 + END AS interactionEnabled, + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN knocksCount + missedCallsCount + mentionsCount + repliesCount + messagesCount > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN mentionsCount + repliesCount > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + LastMessagePreview.id AS lastMessageId, + LastMessagePreview.contentType AS lastMessageContentType, + LastMessagePreview.date AS lastMessageDate, + LastMessagePreview.visibility AS lastMessageVisibility, + LastMessagePreview.senderUserId AS lastMessageSenderUserId, + LastMessagePreview.isEphemeral AS lastMessageIsEphemeral, + LastMessagePreview.senderName AS lastMessageSenderName, + LastMessagePreview.senderConnectionStatus AS lastMessageSenderConnectionStatus, + LastMessagePreview.senderIsDeleted AS lastMessageSenderIsDeleted, + LastMessagePreview.selfUserId AS lastMessageSelfUserId, + LastMessagePreview.isSelfMessage AS lastMessageIsSelfMessage, + LastMessagePreview.memberChangeList AS lastMessageMemberChangeList, + LastMessagePreview.memberChangeType AS lastMessageMemberChangeType, + LastMessagePreview.updateConversationName AS lastMessageUpdateConversationName, + LastMessagePreview.conversationName AS lastMessageConversationName, + LastMessagePreview.isMentioningSelfUser AS lastMessageIsMentioningSelfUser, + LastMessagePreview.isQuotingSelfUser AS lastMessageIsQuotingSelfUser, + LastMessagePreview.text AS lastMessageText, + LastMessagePreview.assetMimeType AS lastMessageAssetMimeType, + LastMessagePreview.isUnread AS lastMessageIsUnread, + LastMessagePreview.shouldNotify AS lastMessageShouldNotify, + LastMessagePreview.mutedStatus AS lastMessageMutedStatus, + LastMessagePreview.conversationType AS lastMessageConversationType, + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON ConversationDetails.qualifiedId = UnreadEventCountsGrouped.conversationId +LEFT JOIN LastMessagePreview ON ConversationDetails.qualifiedId = LastMessagePreview.conversationId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN MessageDraft ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +WHERE + type IS NOT 'SELF' + AND ( + type IS 'GROUP' + OR (type IS 'ONE_ON_ONE' AND (name IS NOT NULL AND otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (type IS 'ONE_ON_ONE' AND userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (type IS 'CONNECTION_PENDING' AND otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (protocol IS 'PROTEUS' OR protocol IS 'MIXED' OR (protocol IS 'MLS' AND mls_group_state IS 'ESTABLISHED')) + AND isActive; diff --git a/persistence/src/commonMain/db_user/migrations/88.sqm b/persistence/src/commonMain/db_user/migrations/88.sqm new file mode 100644 index 00000000000..c7675b06eb5 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/88.sqm @@ -0,0 +1,215 @@ +DROP VIEW IF EXISTS LastMessagePreview; +DROP VIEW IF EXISTS ConversationDetails; +DROP VIEW IF EXISTS ConversationDetailsWithEvents; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1); + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + -- unread events + SUM(CASE WHEN UnreadEvent.type = 'KNOCK' THEN 1 ELSE 0 END) AS unreadKnocksCount, + SUM(CASE WHEN UnreadEvent.type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS unreadMissedCallsCount, + SUM(CASE WHEN UnreadEvent.type = 'MENTION' THEN 1 ELSE 0 END) AS unreadMentionsCount, + SUM(CASE WHEN UnreadEvent.type = 'REPLY' THEN 1 ELSE 0 END) AS unreadRepliesCount, + SUM(CASE WHEN UnreadEvent.type = 'MESSAGE' THEN 1 ELSE 0 END) AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN COUNT(UnreadEvent.id) > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN SUM(CASE WHEN UnreadEvent.type IN ('MENTION', 'REPLY') THEN 1 ELSE 0 END) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + LastMessage.id AS lastMessageId, + LastMessage.content_type AS lastMessageContentType, + LastMessage.creation_date AS lastMessageDate, + LastMessage.visibility AS lastMessageVisibility, + LastMessage.sender_user_id AS lastMessageSenderUserId, + (LastMessage.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (LastMessage.sender_user_id IS NOT NULL AND LastMessage.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEvent + ON UnreadEvent.conversation_id = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN Message AS LastMessage + ON LastMessage.id = ( + SELECT Message.id + FROM Message + WHERE ConversationDetails.qualifiedId = Message.conversation_id + ORDER BY Message.creation_date DESC + LIMIT 1 + ) AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN User + ON LastMessage.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive +GROUP BY ConversationDetails.qualifiedId; diff --git a/persistence/src/commonMain/db_user/migrations/89.sqm b/persistence/src/commonMain/db_user/migrations/89.sqm new file mode 100644 index 00000000000..079151f3da5 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/89.sqm @@ -0,0 +1,80 @@ +DROP VIEW IF EXISTS ConversationDetailsWithEvents; + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + -- unread events + SUM(CASE WHEN UnreadEvent.type = 'KNOCK' THEN 1 ELSE 0 END) AS unreadKnocksCount, + SUM(CASE WHEN UnreadEvent.type = 'MISSED_CALL' THEN 1 ELSE 0 END) AS unreadMissedCallsCount, + SUM(CASE WHEN UnreadEvent.type = 'MENTION' THEN 1 ELSE 0 END) AS unreadMentionsCount, + SUM(CASE WHEN UnreadEvent.type = 'REPLY' THEN 1 ELSE 0 END) AS unreadRepliesCount, + SUM(CASE WHEN UnreadEvent.type = 'MESSAGE' THEN 1 ELSE 0 END) AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN COUNT(UnreadEvent.id) > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN SUM(CASE WHEN UnreadEvent.type IN ('MENTION', 'REPLY') THEN 1 ELSE 0 END) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + LastMessage.id AS lastMessageId, + LastMessage.content_type AS lastMessageContentType, + MAX(LastMessage.creation_date) AS lastMessageDate, + LastMessage.visibility AS lastMessageVisibility, + LastMessage.sender_user_id AS lastMessageSenderUserId, + (LastMessage.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (LastMessage.sender_user_id IS NOT NULL AND LastMessage.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEvent + ON UnreadEvent.conversation_id = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN Message AS LastMessage + ON LastMessage.conversation_id = ConversationDetails.qualifiedId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN User + ON LastMessage.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive +GROUP BY ConversationDetails.qualifiedId; diff --git a/persistence/src/commonMain/db_user/migrations/90.sqm b/persistence/src/commonMain/db_user/migrations/90.sqm new file mode 100644 index 00000000000..129f67d433f --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/90.sqm @@ -0,0 +1,208 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.ConversationEntity; +import com.wire.kalium.persistence.dao.message.MessageEntity.ContentType; +import com.wire.kalium.persistence.dao.message.MessageEntity.FederationType; +import com.wire.kalium.persistence.dao.message.MessageEntity.LegalHoldType; +import com.wire.kalium.persistence.dao.message.MessageEntity.MemberChangeType; +import com.wire.kalium.persistence.dao.message.MessageEntity; +import com.wire.kalium.persistence.dao.message.RecipientFailureTypeEntity; +import kotlin.Boolean; +import kotlin.Float; +import kotlin.Int; +import kotlin.String; +import kotlin.collections.List; +import kotlinx.datetime.Instant; + +CREATE INDEX call_status ON Call(status); +CREATE INDEX unread_event_conversation ON UnreadEvent(conversation_id); +CREATE INDEX unread_event_date ON UnreadEvent(creation_date); +CREATE INDEX unread_event_type ON UnreadEvent(type); + +CREATE TABLE LastMessage ( + conversation_id TEXT AS QualifiedIDEntity, + message_id TEXT, + creation_date INTEGER AS Instant, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + FOREIGN KEY (message_id, conversation_id) REFERENCES Message(id, conversation_id) ON DELETE CASCADE ON UPDATE SET NULL, -- there is a trigger to handle null values + PRIMARY KEY (conversation_id) +); + +-- update last message when newly inserted message is newer than the current last message +CREATE TRIGGER updateLastMessageAfterInsertingNewMessage +AFTER INSERT ON Message +WHEN + new.visibility IN ('VISIBLE', 'DELETED') + AND new.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + VALUES (new.conversation_id, new.id, new.creation_date) + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after deleting the current last message by finding new last message for the conversation +CREATE TRIGGER updateLastMessageAfterDeletingLastMessage +AFTER DELETE ON LastMessage +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after a message got updated and now this one should be the new last message +-- or if the current last message shouldn't be the last message anymore because of the visibility update for instance +CREATE TRIGGER updateLastMessageAfterUpdatingMessage +AFTER UPDATE OF id, conversation_id, visibility, content_type, creation_date ON Message +WHEN + new.creation_date >= (SELECT creation_date FROM LastMessage WHERE conversation_id = new.conversation_id LIMIT 1) +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + new.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; +END; + +-- update last message after there was a foreign key updated to null by finding new last message for that conversation +-- this happens when last message is moved to another conversation or id of last message is changed +CREATE TRIGGER updateLastMessageAfterForeignKeyUpdatedToNull +AFTER UPDATE OF conversation_id ON LastMessage +WHEN + new.conversation_id IS NULL +BEGIN + INSERT INTO LastMessage(conversation_id, message_id, creation_date) + SELECT conversation_id, id, creation_date + FROM Message + WHERE + old.conversation_id = Message.conversation_id + AND Message.visibility IN ('VISIBLE', 'DELETED') + AND Message.content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') + ORDER BY creation_date DESC + LIMIT 1 + ON CONFLICT(conversation_id) + DO UPDATE SET + message_id = excluded.message_id, + creation_date = excluded.creation_date + WHERE + excluded.creation_date > LastMessage.creation_date; + DELETE FROM LastMessage WHERE conversation_id IS NULL; +END; + +-- populate LastMessage table with the last message of each conversation +INSERT INTO LastMessage(conversation_id, message_id, creation_date) +SELECT conversation_id, id, creation_date +FROM Message +WHERE + visibility IN ('VISIBLE', 'DELETED') + AND content_type IN ('TEXT', 'ASSET', 'KNOCK', 'MISSED_CALL', 'CONVERSATION_RENAMED', 'MEMBER_CHANGE', 'COMPOSITE', 'CONVERSATION_DEGRADED_MLS', 'CONVERSATION_DEGRADED_PROTEUS', 'CONVERSATION_VERIFIED_MLS', 'CONVERSATION_VERIFIED_PROTEUS', 'LOCATION') +GROUP BY conversation_id +HAVING creation_date = MAX(creation_date); + +DROP VIEW IF EXISTS ConversationDetailsWithEvents; + +CREATE VIEW IF NOT EXISTS ConversationDetailsWithEvents AS +SELECT + ConversationDetails.*, + -- unread events + UnreadEventCountsGrouped.knocksCount AS unreadKnocksCount, + UnreadEventCountsGrouped.missedCallsCount AS unreadMissedCallsCount, + UnreadEventCountsGrouped.mentionsCount AS unreadMentionsCount, + UnreadEventCountsGrouped.repliesCount AS unreadRepliesCount, + UnreadEventCountsGrouped.messagesCount AS unreadMessagesCount, + CASE + WHEN ConversationDetails.callStatus = 'STILL_ONGOING' AND ConversationDetails.type = 'GROUP' THEN 1 -- if ongoing call in a group, move it to the top + WHEN ConversationDetails.mutedStatus = 'ALL_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.knocksCount + UnreadEventCountsGrouped.missedCallsCount + UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount + UnreadEventCountsGrouped.messagesCount) > 0 THEN 1 -- if any unread events, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + WHEN ConversationDetails.mutedStatus = 'ONLY_MENTIONS_AND_REPLIES_ALLOWED' THEN + CASE + WHEN (UnreadEventCountsGrouped.mentionsCount + UnreadEventCountsGrouped.repliesCount) > 0 THEN 1 -- only if unread mentions or replies, move it to the top + WHEN ConversationDetails.type = 'CONNECTION_PENDING' AND ConversationDetails.connectionStatus = 'PENDING' THEN 1 -- if received connection request, move it to the top + ELSE 0 + END + ELSE 0 + END AS hasNewActivitiesToShow, + -- draft message + MessageDraft.text AS messageDraftText, + MessageDraft.edit_message_id AS messageDraftEditMessageId, + MessageDraft.quoted_message_id AS messageDraftQuotedMessageId, + MessageDraft.mention_list AS messageDraftMentionList, + -- last message + Message.id AS lastMessageId, + Message.content_type AS lastMessageContentType, + Message.creation_date AS lastMessageDate, + Message.visibility AS lastMessageVisibility, + Message.sender_user_id AS lastMessageSenderUserId, + (Message.expire_after_millis IS NOT NULL) AS lastMessageIsEphemeral, + User.name AS lastMessageSenderName, + User.connection_status AS lastMessageSenderConnectionStatus, + User.deleted AS lastMessageSenderIsDeleted, + (Message.sender_user_id IS NOT NULL AND Message.sender_user_id == ConversationDetails.selfUserId) AS lastMessageIsSelfMessage, + MemberChangeContent.member_change_list AS lastMessageMemberChangeList, + MemberChangeContent.member_change_type AS lastMessageMemberChangeType, + ConversationNameChangedContent.conversation_name AS lastMessageUpdateConversationName, + (Mention.user_id IS NOT NULL) AS lastMessageIsMentioningSelfUser, + TextContent.is_quoting_self AS lastMessageIsQuotingSelfUser, + TextContent.text_body AS lastMessageText, + AssetContent.asset_mime_type AS lastMessageAssetMimeType +FROM ConversationDetails +LEFT JOIN UnreadEventCountsGrouped + ON UnreadEventCountsGrouped.conversationId = ConversationDetails.qualifiedId +LEFT JOIN MessageDraft + ON ConversationDetails.qualifiedId = MessageDraft.conversation_id AND ConversationDetails.archived = 0 -- only return message draft for non-archived conversations +LEFT JOIN LastMessage + ON LastMessage.conversation_id = ConversationDetails.qualifiedId AND ConversationDetails.archived = 0 -- only return last message for non-archived conversations +LEFT JOIN Message + ON LastMessage.message_id = Message.id AND LastMessage.conversation_id = Message.conversation_id +LEFT JOIN User + ON Message.sender_user_id = User.qualified_id +LEFT JOIN MessageMemberChangeContent AS MemberChangeContent + ON LastMessage.message_id = MemberChangeContent.message_id AND LastMessage.conversation_id = MemberChangeContent.conversation_id +LEFT JOIN MessageMention AS Mention + ON LastMessage.message_id == Mention.message_id AND ConversationDetails.selfUserId == Mention.user_id +LEFT JOIN MessageConversationChangedContent AS ConversationNameChangedContent + ON LastMessage.message_id = ConversationNameChangedContent.message_id AND LastMessage.conversation_id = ConversationNameChangedContent.conversation_id +LEFT JOIN MessageAssetContent AS AssetContent + ON LastMessage.message_id = AssetContent.message_id AND LastMessage.conversation_id = AssetContent.conversation_id +LEFT JOIN MessageTextContent AS TextContent + ON LastMessage.message_id = TextContent.message_id AND LastMessage.conversation_id = TextContent.conversation_id +WHERE + ConversationDetails.type IS NOT 'SELF' + AND ( + ConversationDetails.type IS 'GROUP' + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND (ConversationDetails.name IS NOT NULL AND ConversationDetails.otherUserId IS NOT NULL)) -- show 1:1 convos if they have user metadata + OR (ConversationDetails.type IS 'ONE_ON_ONE' AND ConversationDetails.userDeleted = 1) -- show deleted 1:1 convos to maintain prev, logic + OR (ConversationDetails.type IS 'CONNECTION_PENDING' AND ConversationDetails.otherUserId IS NOT NULL) -- show connection requests even without metadata + ) + AND (ConversationDetails.protocol IS 'PROTEUS' OR ConversationDetails.protocol IS 'MIXED' OR (ConversationDetails.protocol IS 'MLS' AND ConversationDetails.mls_group_state IS 'ESTABLISHED')) + AND ConversationDetails.isActive; diff --git a/persistence/src/commonMain/db_user/migrations/91.sqm b/persistence/src/commonMain/db_user/migrations/91.sqm new file mode 100644 index 00000000000..0f8a5a4da56 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/91.sqm @@ -0,0 +1,19 @@ +import com.wire.kalium.persistence.dao.QualifiedIDEntity; +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderTypeEntity; + +CREATE TABLE ConversationFolder ( + id TEXT NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + folder_type TEXT AS ConversationFolderTypeEntity NOT NULL +); + + +CREATE TABLE LabeledConversation ( + conversation_id TEXT AS QualifiedIDEntity NOT NULL, + folder_id TEXT NOT NULL, + + FOREIGN KEY (conversation_id) REFERENCES Conversation(qualified_id) ON DELETE CASCADE ON UPDATE CASCADE, + FOREIGN KEY (folder_id) REFERENCES ConversationFolder(id) ON DELETE CASCADE ON UPDATE CASCADE, + + PRIMARY KEY (folder_id, conversation_id) +); diff --git a/persistence/src/commonMain/db_user/migrations/92.sqm b/persistence/src/commonMain/db_user/migrations/92.sqm new file mode 100644 index 00000000000..3f36fd16814 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/92.sqm @@ -0,0 +1,131 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE + WHEN (SelfUser.id LIKE (Conversation.creator_id || '@%')) THEN 1 + ELSE 0 +END AS isCreator, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; diff --git a/persistence/src/commonMain/db_user/migrations/93.sqm b/persistence/src/commonMain/db_user/migrations/93.sqm new file mode 100644 index 00000000000..30dc8b3fa11 --- /dev/null +++ b/persistence/src/commonMain/db_user/migrations/93.sqm @@ -0,0 +1,127 @@ +DROP VIEW IF EXISTS ConversationDetails; + +CREATE VIEW IF NOT EXISTS ConversationDetails AS +SELECT +Conversation.qualified_id AS qualifiedId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.name + WHEN 'CONNECTION_PENDING' THEN connection_user.name + ELSE Conversation.name +END AS name, +Conversation.type, +Call.status AS callStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.preview_asset_id + WHEN 'CONNECTION_PENDING' THEN connection_user.preview_asset_id +END AS previewAssetId, +Conversation.muted_status AS mutedStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.team + ELSE Conversation.team_id +END AS teamId, +CASE (Conversation.type) + WHEN 'CONNECTION_PENDING' THEN Connection.last_update_date + ELSE Conversation.last_modified_date +END AS lastModifiedDate, +Conversation.last_read_date AS lastReadDate, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_availability_status + WHEN 'CONNECTION_PENDING' THEN connection_user.user_availability_status +END AS userAvailabilityStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.user_type + WHEN 'CONNECTION_PENDING' THEN connection_user.user_type +END AS userType, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.bot_service + WHEN 'CONNECTION_PENDING' THEN connection_user.bot_service +END AS botService, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.deleted + WHEN 'CONNECTION_PENDING' THEN connection_user.deleted +END AS userDeleted, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.defederated + WHEN 'CONNECTION_PENDING' THEN connection_user.defederated +END AS userDefederated, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.supported_protocols + WHEN 'CONNECTION_PENDING' THEN connection_user.supported_protocols +END AS userSupportedProtocols, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.connection_status + WHEN 'CONNECTION_PENDING' THEN connection_user.connection_status +END AS connectionStatus, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.qualified_id + WHEN 'CONNECTION_PENDING' THEN connection_user.qualified_id +END AS otherUserId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.active_one_on_one_conversation_id + WHEN 'CONNECTION_PENDING' THEN connection_user.active_one_on_one_conversation_id +END AS otherUserActiveConversationId, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN coalesce(User.active_one_on_one_conversation_id = Conversation.qualified_id, 0) + ELSE 1 +END AS isActive, +CASE (Conversation.type) + WHEN 'ONE_ON_ONE' THEN User.accent_id + ELSE 0 +END AS accentId, +Conversation.last_notified_date AS lastNotifiedMessageDate, +memberRole. role AS selfRole, +Conversation.protocol, +Conversation.mls_cipher_suite, +Conversation.mls_epoch, +Conversation.mls_group_id, +Conversation.mls_last_keying_material_update_date, +Conversation.mls_group_state, +Conversation.access_list, +Conversation.access_role_list, +Conversation.mls_proposal_timer, +Conversation.muted_time, +Conversation.creator_id, +Conversation.receipt_mode, +Conversation.message_timer, +Conversation.user_message_timer, +Conversation.incomplete_metadata, +Conversation.archived, +Conversation.archived_date_time, +Conversation.verification_status AS mls_verification_status, +Conversation.proteus_verification_status, +Conversation.legal_hold_status, +SelfUser.id AS selfUserId, +CASE + WHEN Conversation.type = 'GROUP' THEN + CASE + WHEN memberRole.role IS NOT NULL THEN 1 + ELSE 0 + END + WHEN Conversation.type = 'ONE_ON_ONE' THEN + CASE + WHEN User.defederated = 1 THEN 0 + WHEN User.deleted = 1 THEN 0 + WHEN User.connection_status = 'BLOCKED' THEN 0 + WHEN Conversation.legal_hold_status = 'DEGRADED' THEN 0 + ELSE 1 + END + ELSE 0 +END AS interactionEnabled, +LabeledConversation.folder_id IS NOT NULL AS isFavorite +FROM Conversation +LEFT JOIN SelfUser +LEFT JOIN Member ON Conversation.qualified_id = Member.conversation + AND Conversation.type IS 'ONE_ON_ONE' + AND Member.user IS NOT SelfUser.id +LEFT JOIN Member AS memberRole ON Conversation.qualified_id = memberRole.conversation + AND memberRole.user IS SelfUser.id +LEFT JOIN User ON User.qualified_id = Member.user +LEFT JOIN Connection ON Connection.qualified_conversation = Conversation.qualified_id + AND (Connection.status = 'SENT' + OR Connection.status = 'PENDING' + OR Connection.status = 'NOT_CONNECTED' + AND Conversation.type IS 'CONNECTION_PENDING') +LEFT JOIN User AS connection_user ON Connection.qualified_to = connection_user.qualified_id +LEFT JOIN Call ON Call.id IS (SELECT id FROM Call WHERE Call.conversation_id = Conversation.qualified_id AND Call.status IS 'STILL_ONGOING' ORDER BY created_at DESC LIMIT 1) +LEFT JOIN ConversationFolder AS FavoriteFolder ON FavoriteFolder.folder_type = 'FAVORITE' +LEFT JOIN LabeledConversation ON LabeledConversation.conversation_id = Conversation.qualified_id AND LabeledConversation.folder_id = FavoriteFolder.id; diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt index b3542fb10f7..017451b22ec 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/config/UserConfigStorage.kt @@ -145,6 +145,11 @@ interface UserConfigStorage { */ fun isConferenceCallingEnabled(): Boolean + /** + * Get a flow of saved flag to know if conference calling is enabled or not + */ + fun isConferenceCallingEnabledFlow(): Flow + fun persistUseSftForOneOnOneCalls(shouldUse: Boolean) fun shouldUseSftForOneOnOneCalls(): Boolean @@ -178,8 +183,6 @@ interface UserConfigStorage { fun getE2EINotificationTime(): Long? fun e2EINotificationTimeFlow(): Flow fun updateE2EINotificationTime(timeStamp: Long) - fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) - fun getShouldFetchE2EITrustAnchorHasRun(): Boolean } @Serializable @@ -327,6 +330,12 @@ class UserConfigStorageImpl( onBufferOverflow = BufferOverflow.DROP_OLDEST ) + private val conferenceCallingEnabledFlow = + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + private val legalHoldRequestFlow = MutableSharedFlow( extraBufferCapacity = 1, @@ -499,6 +508,7 @@ class UserConfigStorageImpl( override fun persistConferenceCalling(enabled: Boolean) { kaliumPreferences.putBoolean(ENABLE_CONFERENCE_CALLING, enabled) + conferenceCallingEnabledFlow.tryEmit(Unit) } override fun isConferenceCallingEnabled(): Boolean = @@ -507,6 +517,10 @@ class UserConfigStorageImpl( DEFAULT_CONFERENCE_CALLING_ENABLED_VALUE ) + override fun isConferenceCallingEnabledFlow(): Flow = conferenceCallingEnabledFlow + .map { isConferenceCallingEnabled() } + .onStart { emit(isConferenceCallingEnabled()) } + override fun persistUseSftForOneOnOneCalls(shouldUse: Boolean) { kaliumPreferences.putBoolean(USE_SFT_FOR_ONE_ON_ONE_CALLS, shouldUse) } @@ -576,13 +590,6 @@ class UserConfigStorageImpl( } } - override fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) { - kaliumPreferences.putBoolean(SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS, shouldFetch) - } - - override fun getShouldFetchE2EITrustAnchorHasRun(): Boolean = - kaliumPreferences.getBoolean(SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS, true) - private companion object { const val FILE_SHARING = "file_sharing" const val GUEST_ROOM_LINK = "guest_room_link" @@ -601,6 +608,5 @@ class UserConfigStorageImpl( const val ENABLE_TYPING_INDICATOR = "enable_typing_indicator" const val APP_LOCK = "app_lock" const val DEFAULT_PROTOCOL = "default_protocol" - const val SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS = "should_fetch_e2ei_trust_anchors" } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt index 2fa8258cce8..c92b50621e0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAO.kt @@ -21,6 +21,7 @@ package com.wire.kalium.persistence.dao import com.wire.kalium.logger.obfuscateDomain import com.wire.kalium.logger.obfuscateId import com.wire.kalium.persistence.dao.ManagedByEntity.WIRE +import com.wire.kalium.persistence.dao.conversation.NameAndHandleEntity import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant import kotlinx.serialization.SerialName @@ -135,6 +136,7 @@ data class UserEntityMinimized( val name: String?, val completeAssetId: UserAssetIdEntity?, val userType: UserTypeEntity, + val accentId: Int ) data class BotIdEntity( @@ -313,4 +315,6 @@ interface UserDAO { suspend fun isAtLeastOneUserATeamMember(userId: List, teamId: String): Boolean suspend fun getOneOnOnConversationId(userId: UserIDEntity): QualifiedIDEntity? suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List + suspend fun getNameAndHandle(userId: UserIDEntity): NameAndHandleEntity? + suspend fun updateTeamId(userId: UserIDEntity, teamId: String) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt index 74f9c332f46..f601dcc701c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/UserDAOImpl.kt @@ -21,6 +21,7 @@ package com.wire.kalium.persistence.dao import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.UsersQueries import com.wire.kalium.persistence.cache.FlowCache +import com.wire.kalium.persistence.dao.conversation.NameAndHandleEntity import com.wire.kalium.persistence.util.mapToList import com.wire.kalium.persistence.util.mapToOneOrNull import kotlinx.coroutines.flow.Flow @@ -145,12 +146,14 @@ class UserMapper { userId: QualifiedIDEntity, name: String?, assetId: QualifiedIDEntity?, - userTypeEntity: UserTypeEntity + userTypeEntity: UserTypeEntity, + accentId: Int ) = UserEntityMinimized( userId, name, assetId, - userTypeEntity + userTypeEntity, + accentId ) } @@ -281,15 +284,15 @@ class UserDAOImpl internal constructor( override suspend fun getUserMinimizedByQualifiedID(qualifiedID: QualifiedIDEntity): UserEntityMinimized? = withContext(queriesContext) { - userQueries.selectMinimizedByQualifiedId(listOf(qualifiedID)) { qualifiedId, name, completeAssetId, userType -> - mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType) + userQueries.selectMinimizedByQualifiedId(listOf(qualifiedID)) { qualifiedId, name, completeAssetId, userType, accentId -> + mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType, accentId) }.executeAsOneOrNull() } override suspend fun getUsersMinimizedByQualifiedIDs(qualifiedIDs: List): List = withContext(queriesContext) { - userQueries.selectMinimizedByQualifiedId(qualifiedIDs) { qualifiedId, name, completeAssetId, userType -> - mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType) + userQueries.selectMinimizedByQualifiedId(qualifiedIDs) { qualifiedId, name, completeAssetId, userType, accentId -> + mapper.toModelMinimized(qualifiedId, name, completeAssetId, userType, accentId) }.executeAsList() } @@ -466,4 +469,12 @@ class UserDAOImpl internal constructor( override suspend fun getOneOnOnConversationId(userId: UserIDEntity): QualifiedIDEntity? = withContext(queriesContext) { userQueries.selectOneOnOnConversationId(userId).executeAsOneOrNull()?.active_one_on_one_conversation_id } + + override suspend fun getNameAndHandle(userId: UserIDEntity): NameAndHandleEntity? = withContext(queriesContext) { + userQueries.selectNamesAndHandle(userId, ::NameAndHandleEntity).executeAsOneOrNull() + } + + override suspend fun updateTeamId(userId: UserIDEntity, teamId: String) { + userQueries.updateTeamId(teamId, userId) + } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt index 16a04553680..464034de37a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAO.kt @@ -31,6 +31,7 @@ data class ProposalTimerEntity( @Suppress("TooManyFunctions") interface ConversationDAO { + val platformExtensions: ConversationExtensions //region Get/Observe by ID suspend fun observeConversationById(qualifiedID: QualifiedIDEntity): Flow @@ -56,7 +57,12 @@ interface ConversationDAO { suspend fun updateConversationReadDate(conversationID: QualifiedIDEntity, date: Instant) suspend fun updateAllConversationsNotificationDate() suspend fun getAllConversations(): Flow> - suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> + suspend fun getAllConversationDetails(fromArchive: Boolean, filter: ConversationFilterEntity): Flow> + suspend fun getAllConversationDetailsWithEvents( + fromArchive: Boolean = false, + onlyInteractionEnabled: Boolean = false, + newActivitiesOnTop: Boolean = false, + ): Flow> suspend fun getConversationIds( type: ConversationEntity.Type, protocol: ConversationEntity.Protocol, @@ -72,7 +78,7 @@ interface ConversationDAO { suspend fun observeOneOnOneConversationWithOtherUser(userId: UserIDEntity): Flow suspend fun getConversationProtocolInfo(qualifiedID: QualifiedIDEntity): ConversationEntity.ProtocolInfo? - suspend fun observeConversationByGroupID(groupID: String): Flow + suspend fun observeConversationDetailsByGroupID(groupID: String): Flow suspend fun getConversationIdByGroupID(groupID: String): QualifiedIDEntity? suspend fun getConversationsByGroupState(groupState: ConversationEntity.GroupState): List suspend fun deleteConversationByQualifiedID(qualifiedID: QualifiedIDEntity) @@ -126,7 +132,7 @@ interface ConversationDAO { suspend fun getConversationsWithoutMetadata(): List suspend fun clearContent(conversationId: QualifiedIDEntity) suspend fun updateMlsVerificationStatus(verificationStatus: ConversationEntity.VerificationStatus, conversationId: QualifiedIDEntity) - suspend fun getConversationByGroupID(groupID: String): ConversationViewEntity? + suspend fun getConversationDetailsByGroupID(groupID: String): ConversationViewEntity? suspend fun observeUnreadArchivedConversationsCount(): Flow suspend fun observeDegradedConversationNotified(conversationId: QualifiedIDEntity): Flow suspend fun updateDegradedConversationNotifiedFlag(conversationId: QualifiedIDEntity, updateFlag: Boolean) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt index 0f20386e2e5..ef7b6ec58a1 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDAOImpl.kt @@ -20,6 +20,8 @@ package com.wire.kalium.persistence.dao.conversation import app.cash.sqldelight.coroutines.asFlow import com.wire.kalium.persistence.ConversationsQueries +import com.wire.kalium.persistence.ConversationDetailsQueries +import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries import com.wire.kalium.persistence.MembersQueries import com.wire.kalium.persistence.UnreadEventsQueries import com.wire.kalium.persistence.cache.FlowCache @@ -49,15 +51,21 @@ internal val MLS_DEFAULT_CIPHER_SUITE = ConversationEntity.CipherSuite.MLS_128_D // TODO: Refactor. We can split this into smaller DAOs. // For example, one for Members, one for Protocol/MLS-related things, etc. // Even if they operate on the same table underneath, these DAOs can represent/do different things. -@Suppress("TooManyFunctions") +@Suppress("TooManyFunctions", "LongParameterList") internal class ConversationDAOImpl internal constructor( private val conversationDetailsCache: FlowCache, private val conversationCache: FlowCache, private val conversationQueries: ConversationsQueries, + private val conversationDetailsQueries: ConversationDetailsQueries, + private val conversationDetailsWithEventsQueries: ConversationDetailsWithEventsQueries, private val memberQueries: MembersQueries, private val unreadEventsQueries: UnreadEventsQueries, private val coroutineContext: CoroutineContext, ) : ConversationDAO { + private val conversationMapper = ConversationMapper + private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + override val platformExtensions: ConversationExtensions = + ConversationExtensionsImpl(conversationDetailsWithEventsQueries, conversationDetailsWithEventsMapper, coroutineContext) // region Get/Observe by ID @@ -78,7 +86,7 @@ internal class ConversationDAOImpl internal constructor( override suspend fun observeConversationDetailsById( conversationId: QualifiedIDEntity ): Flow = conversationDetailsCache.get(conversationId) { - conversationQueries.selectByQualifiedId(conversationId, conversationMapper::fromViewToModel) + conversationDetailsQueries.selectConversationDetailsByQualifiedId(conversationId, conversationMapper::fromViewToModel) .asFlow() .mapToOneOrNull() } @@ -90,7 +98,6 @@ internal class ConversationDAOImpl internal constructor( // endregion - private val conversationMapper = ConversationMapper() override suspend fun getSelfConversationId(protocol: ConversationEntity.Protocol) = withContext(coroutineContext) { conversationQueries.selfConversationId(protocol).executeAsOneOrNull() } @@ -207,13 +214,31 @@ internal class ConversationDAOImpl internal constructor( .flowOn(coroutineContext) } - override suspend fun getAllConversationDetails(fromArchive: Boolean): Flow> { - return conversationQueries.selectAllConversationDetails(fromArchive, conversationMapper::fromViewToModel) + override suspend fun getAllConversationDetails( + fromArchive: Boolean, + filter: ConversationFilterEntity + ): Flow> { + return conversationDetailsQueries.selectAllConversationDetails(fromArchive, filter.toString(), conversationMapper::fromViewToModel) .asFlow() .mapToList() .flowOn(coroutineContext) } + override suspend fun getAllConversationDetailsWithEvents( + fromArchive: Boolean, + onlyInteractionEnabled: Boolean, + newActivitiesOnTop: Boolean, + ): Flow> { + return conversationDetailsWithEventsQueries.selectAllConversationDetailsWithEvents( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + newActivitiesOnTop = newActivitiesOnTop, + mapper = conversationDetailsWithEventsMapper::fromViewToModel + ).asFlow() + .mapToList() + .flowOn(coroutineContext) + } + override suspend fun getConversationIds( type: ConversationEntity.Type, protocol: ConversationEntity.Protocol, @@ -257,15 +282,15 @@ internal class ConversationDAOImpl internal constructor( conversationQueries.selectProtocolInfoByQualifiedId(qualifiedID, conversationMapper::mapProtocolInfo).executeAsOneOrNull() } - override suspend fun observeConversationByGroupID(groupID: String): Flow { - return conversationQueries.selectByGroupId(groupID, conversationMapper::fromViewToModel) + override suspend fun observeConversationDetailsByGroupID(groupID: String): Flow { + return conversationDetailsQueries.selectConversationDetailsByGroupId(groupID, conversationMapper::fromViewToModel) .asFlow() .flowOn(coroutineContext) .mapToOneOrNull() } - override suspend fun getConversationByGroupID(groupID: String): ConversationViewEntity? { - return conversationQueries.selectByGroupId(groupID, conversationMapper::fromViewToModel) + override suspend fun getConversationDetailsByGroupID(groupID: String): ConversationViewEntity? { + return conversationDetailsQueries.selectConversationDetailsByGroupId(groupID, conversationMapper::fromViewToModel) .executeAsOneOrNull() } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt new file mode 100644 index 00000000000..9176a26d5df --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsEntity.kt @@ -0,0 +1,30 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation + +import com.wire.kalium.persistence.dao.message.MessagePreviewEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity +import com.wire.kalium.persistence.dao.unread.ConversationUnreadEventEntity + +data class ConversationDetailsWithEventsEntity( + val conversationViewEntity: ConversationViewEntity, + val lastMessage: MessagePreviewEntity? = null, + val messageDraft: MessageDraftEntity? = null, + val unreadEvents: ConversationUnreadEventEntity = ConversationUnreadEventEntity(conversationViewEntity.id, mapOf()), + val hasNewActivitiesToShow: Boolean = false, +) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt new file mode 100644 index 00000000000..d59c32c6c2f --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationDetailsWithEventsMapper.kt @@ -0,0 +1,211 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation + +import com.wire.kalium.persistence.dao.BotIdEntity +import com.wire.kalium.persistence.dao.ConnectionEntity +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.SupportedProtocolEntity +import com.wire.kalium.persistence.dao.UserAvailabilityStatusEntity +import com.wire.kalium.persistence.dao.UserTypeEntity +import com.wire.kalium.persistence.dao.call.CallEntity +import com.wire.kalium.persistence.dao.member.MemberEntity +import com.wire.kalium.persistence.dao.message.MessageEntity +import com.wire.kalium.persistence.dao.message.MessageMapper +import com.wire.kalium.persistence.dao.message.draft.MessageDraftMapper +import com.wire.kalium.persistence.dao.unread.UnreadEventMapper +import kotlinx.datetime.Instant + +data object ConversationDetailsWithEventsMapper { + // suppressed because the method cannot be shortened and there are unused parameters because sql view returns some duplicated fields + @Suppress("LongParameterList", "LongMethod", "UnusedParameter") + fun fromViewToModel( + qualifiedId: QualifiedIDEntity, + name: String?, + type: ConversationEntity.Type, + callStatus: CallEntity.Status?, + previewAssetId: QualifiedIDEntity?, + mutedStatus: ConversationEntity.MutedStatus, + teamId: String?, + lastModifiedDate: Instant?, + lastReadDate: Instant, + userAvailabilityStatus: UserAvailabilityStatusEntity?, + userType: UserTypeEntity?, + botService: BotIdEntity?, + userDeleted: Boolean?, + userDefederated: Boolean?, + userSupportedProtocols: Set?, + connectionStatus: ConnectionEntity.State?, + otherUserId: QualifiedIDEntity?, + otherUserActiveConversationId: QualifiedIDEntity?, + isActive: Long, + accentId: Int?, + lastNotifiedMessageDate: Instant?, + selfRole: MemberEntity.Role?, + protocol: ConversationEntity.Protocol, + mlsCipherSuite: ConversationEntity.CipherSuite, + mlsEpoch: Long, + mlsGroupId: String?, + mlsLastKeyingMaterialUpdateDate: Instant, + mlsGroupState: ConversationEntity.GroupState, + accessList: List, + accessRoleList: List, + mlsProposalTimer: String?, + mutedTime: Long, + creatorId: String, + receiptMode: ConversationEntity.ReceiptMode, + messageTimer: Long?, + userMessageTimer: Long?, + incompleteMetadata: Boolean, + archived: Boolean, + archivedDateTime: Instant?, + mlsVerificationStatus: ConversationEntity.VerificationStatus, + proteusVerificationStatus: ConversationEntity.VerificationStatus, + legalHoldStatus: ConversationEntity.LegalHoldStatus, + selfUserId: QualifiedIDEntity?, + interactionEnabled: Long, + isFavorite: Boolean, + unreadKnocksCount: Long?, + unreadMissedCallsCount: Long?, + unreadMentionsCount: Long?, + unreadRepliesCount: Long?, + unreadMessagesCount: Long?, + hasNewActivitiesToShow: Long, + messageDraftText: String?, + messageDraftEditMessageId: String?, + messageDraftQuotedMessageId: String?, + messageDraftMentionList: List?, + lastMessageId: String?, + lastMessageContentType: MessageEntity.ContentType?, + lastMessageDate: Instant?, + lastMessageVisibility: MessageEntity.Visibility?, + lastMessageSenderUserId: QualifiedIDEntity?, + lastMessageIsEphemeral: Boolean?, + lastMessageSenderName: String?, + lastMessageSenderConnectionStatus: ConnectionEntity.State?, + lastMessageSenderIsDeleted: Boolean?, + lastMessageIsSelfMessage: Boolean?, + lastMessageMemberChangeList: List?, + lastMessageMemberChangeType: MessageEntity.MemberChangeType?, + lastMessageUpdateConversationName: String?, + lastMessageIsMentioningSelfUser: Boolean?, + lastMessageIsQuotingSelfUser: Boolean?, + lastMessageText: String?, + lastMessageAssetMimeType: String?, + ): ConversationDetailsWithEventsEntity = ConversationDetailsWithEventsEntity( + conversationViewEntity = ConversationMapper.fromViewToModel( + qualifiedId = qualifiedId, + name = name, + type = type, + callStatus = callStatus, + previewAssetId = previewAssetId, + mutedStatus = mutedStatus, + teamId = teamId, + lastModifiedDate = lastModifiedDate, + lastReadDate = lastReadDate, + userAvailabilityStatus = userAvailabilityStatus, + userType = userType, + botService = botService, + userDeleted = userDeleted, + userDefederated = userDefederated, + userSupportedProtocols = userSupportedProtocols, + connectionStatus = connectionStatus, + otherUserId = otherUserId, + otherUserActiveConversationId = otherUserActiveConversationId, + isActive = isActive, + accentId = accentId, + lastNotifiedMessageDate = lastNotifiedMessageDate, + selfRole = selfRole, + protocol = protocol, + mlsCipherSuite = mlsCipherSuite, + mlsEpoch = mlsEpoch, + mlsGroupId = mlsGroupId, + mlsLastKeyingMaterialUpdateDate = mlsLastKeyingMaterialUpdateDate, + mlsGroupState = mlsGroupState, + accessList = accessList, + accessRoleList = accessRoleList, + mlsProposalTimer = mlsProposalTimer, + mutedTime = mutedTime, + creatorId = creatorId, + receiptMode = receiptMode, + messageTimer = messageTimer, + userMessageTimer = userMessageTimer, + incompleteMetadata = incompleteMetadata, + archived = archived, + archivedDateTime = archivedDateTime, + mlsVerificationStatus = mlsVerificationStatus, + proteusVerificationStatus = proteusVerificationStatus, + legalHoldStatus = legalHoldStatus, + selfUserId = selfUserId, + interactionEnabled = interactionEnabled, + isFavorite = isFavorite + ), + unreadEvents = UnreadEventMapper.toConversationUnreadEntity( + conversationId = qualifiedId, + knocksCount = unreadKnocksCount, + missedCallsCount = unreadMissedCallsCount, + mentionsCount = unreadMentionsCount, + repliesCount = unreadRepliesCount, + messagesCount = unreadMessagesCount, + ), + lastMessage = + @Suppress("ComplexCondition") // we need to check all these fields + if ( + lastMessageId != null && lastMessageContentType != null && lastMessageDate != null + && lastMessageVisibility != null && lastMessageSenderUserId != null && lastMessageIsEphemeral != null + && lastMessageIsSelfMessage != null && lastMessageIsMentioningSelfUser != null + ) { + MessageMapper.toPreviewEntity( + id = lastMessageId, + conversationId = qualifiedId, + contentType = lastMessageContentType, + date = lastMessageDate, + visibility = lastMessageVisibility, + senderUserId = lastMessageSenderUserId, + isEphemeral = lastMessageIsEphemeral, + senderName = lastMessageSenderName, + senderConnectionStatus = lastMessageSenderConnectionStatus, + senderIsDeleted = lastMessageSenderIsDeleted, + selfUserId = selfUserId, + isSelfMessage = lastMessageIsSelfMessage, + memberChangeList = lastMessageMemberChangeList, + memberChangeType = lastMessageMemberChangeType, + updatedConversationName = lastMessageUpdateConversationName, + conversationName = name, + isMentioningSelfUser = lastMessageIsMentioningSelfUser, + isQuotingSelfUser = lastMessageIsQuotingSelfUser, + text = lastMessageText, + assetMimeType = lastMessageAssetMimeType, + isUnread = lastMessageDate > lastReadDate, + isNotified = if (lastNotifiedMessageDate?.let { lastMessageDate > it } ?: false) 1 else 0, + mutedStatus = mutedStatus, + conversationType = type, + ) + } else null, + messageDraft = if (!messageDraftText.isNullOrBlank()) { + MessageDraftMapper.toDao( + conversationId = qualifiedId, + text = messageDraftText, + editMessageId = messageDraftEditMessageId, + quotedMessageId = messageDraftQuotedMessageId, + mentionList = messageDraftMentionList ?: emptyList(), + ) + } else null, + hasNewActivitiesToShow = hasNewActivitiesToShow > 0L, + ) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt new file mode 100644 index 00000000000..16330fe8667 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationExtensions.kt @@ -0,0 +1,109 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation + +import app.cash.paging.Pager +import app.cash.paging.PagingConfig +import app.cash.sqldelight.paging3.QueryPagingSource +import com.wire.kalium.persistence.ConversationDetailsWithEventsQueries +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions.QueryConfig +import com.wire.kalium.persistence.dao.message.KaliumPager +import kotlin.coroutines.CoroutineContext + +interface ConversationExtensions { + fun getPagerForConversationDetailsWithEventsSearch( + queryConfig: QueryConfig, + pagingConfig: PagingConfig, + startingOffset: Long = 0, + ): KaliumPager + + data class QueryConfig( + val searchQuery: String = "", + val fromArchive: Boolean = false, + val onlyInteractionEnabled: Boolean = false, + val newActivitiesOnTop: Boolean = false, + val conversationFilter: ConversationFilterEntity = ConversationFilterEntity.ALL, + ) +} + +internal class ConversationExtensionsImpl internal constructor( + private val queries: ConversationDetailsWithEventsQueries, + private val mapper: ConversationDetailsWithEventsMapper, + private val coroutineContext: CoroutineContext, +) : ConversationExtensions { + override fun getPagerForConversationDetailsWithEventsSearch( + queryConfig: QueryConfig, + pagingConfig: PagingConfig, + startingOffset: Long + ): KaliumPager = + KaliumPager( + // We could return a Flow directly, but having the PagingSource is the only way to test this + pager = Pager(pagingConfig) { + pagingSource(queryConfig, startingOffset) + }, + pagingSource = pagingSource(queryConfig, startingOffset), + coroutineContext = coroutineContext, + ) + + private fun pagingSource(queryConfig: QueryConfig, initialOffset: Long) = with(queryConfig) { + QueryPagingSource( + countQuery = + if (searchQuery.isBlank()) { + queries.countConversationDetailsWithEvents( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + ) + } else { + queries.countConversationDetailsWithEventsFromSearch( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + searchQuery = searchQuery + ) + }, + transacter = queries, + context = coroutineContext, + initialOffset = initialOffset, + queryProvider = { limit, offset -> + if (searchQuery.isBlank()) { + queries.selectConversationDetailsWithEvents( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + newActivitiesOnTop = newActivitiesOnTop, + limit = limit, + offset = offset, + mapper = mapper::fromViewToModel, + ) + } else { + queries.selectConversationDetailsWithEventsFromSearch( + fromArchive = fromArchive, + onlyInteractionsEnabled = onlyInteractionEnabled, + conversationFilter = conversationFilter.name, + searchQuery = searchQuery, + newActivitiesOnTop = newActivitiesOnTop, + limit = limit, + offset = offset, + mapper = mapper::fromViewToModel, + ) + } + } + ) + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationFilterEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationFilterEntity.kt new file mode 100644 index 00000000000..4c548b82b67 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationFilterEntity.kt @@ -0,0 +1,25 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation + +enum class ConversationFilterEntity { + ALL, + FAVORITES, + GROUPS, + ONE_ON_ONE +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt index ad1d5ac6e87..4cb050db21d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationMapper.kt @@ -27,8 +27,9 @@ import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.member.MemberEntity import kotlinx.datetime.Instant -internal class ConversationMapper { - @Suppress("LongParameterList", "UnusedParameter", "FunctionParameterNaming") +data object ConversationMapper { + // suppressed because the method cannot be shortened and there are unused parameters because sql view returns some duplicated fields + @Suppress("LongParameterList", "LongMethod", "UnusedParameter") fun fromViewToModel( qualifiedId: QualifiedIDEntity, name: String?, @@ -37,7 +38,7 @@ internal class ConversationMapper { previewAssetId: QualifiedIDEntity?, mutedStatus: ConversationEntity.MutedStatus, teamId: String?, - lastModifiedDate_: Instant?, + lastModifiedDate: Instant?, lastReadDate: Instant, userAvailabilityStatus: UserAvailabilityStatusEntity?, userType: UserTypeEntity?, @@ -48,8 +49,8 @@ internal class ConversationMapper { connectionStatus: ConnectionEntity.State?, otherUserId: QualifiedIDEntity?, otherUserActiveConversationId: QualifiedIDEntity?, - isCreator: Long, isActive: Long, + accentId: Int?, lastNotifiedMessageDate: Instant?, selfRole: MemberEntity.Role?, protocol: ConversationEntity.Protocol, @@ -60,11 +61,9 @@ internal class ConversationMapper { mlsGroupState: ConversationEntity.GroupState, accessList: List, accessRoleList: List, - teamId_: String?, mlsProposalTimer: String?, mutedTime: Long, creatorId: String, - lastModifiedDate: Instant, receiptMode: ConversationEntity.ReceiptMode, messageTimer: Long?, userMessageTimer: Long?, @@ -74,6 +73,9 @@ internal class ConversationMapper { mlsVerificationStatus: ConversationEntity.VerificationStatus, proteusVerificationStatus: ConversationEntity.VerificationStatus, legalHoldStatus: ConversationEntity.LegalHoldStatus, + selfUserId: QualifiedIDEntity?, + interactionEnabled: Long, + isFavorite: Boolean, ): ConversationViewEntity = ConversationViewEntity( id = qualifiedId, name = name, @@ -87,7 +89,6 @@ internal class ConversationMapper { mlsLastKeyingMaterialUpdateDate, mlsCipherSuite ), - isCreator = isCreator, mutedStatus = mutedStatus, mutedTime = mutedTime, creatorId = creatorId, @@ -122,7 +123,9 @@ internal class ConversationMapper { userSupportedProtocols = userSupportedProtocols, userActiveOneOnOneConversationId = otherUserActiveConversationId, proteusVerificationStatus = proteusVerificationStatus, - legalHoldStatus = legalHoldStatus + legalHoldStatus = legalHoldStatus, + accentId = accentId, + isFavorite = isFavorite ) @Suppress("LongParameterList", "UnusedParameter") @@ -158,7 +161,7 @@ internal class ConversationMapper { verificationStatus: ConversationEntity.VerificationStatus, proteusVerificationStatus: ConversationEntity.VerificationStatus, degradedConversationNotified: Boolean, - legalHoldStatus: ConversationEntity.LegalHoldStatus, + legalHoldStatus: ConversationEntity.LegalHoldStatus ) = ConversationEntity( id = qualifiedId, name = name, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt index 4fc9b8ff739..8e5e247226e 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/ConversationViewEntity.kt @@ -47,7 +47,6 @@ data class ConversationViewEntity( val userDefederated: Boolean?, val connectionStatus: ConnectionEntity.State? = ConnectionEntity.State.NOT_CONNECTED, val otherUserId: QualifiedIDEntity?, - val isCreator: Long, val lastNotificationDate: Instant?, val selfRole: MemberEntity.Role?, val protocolInfo: ConversationEntity.ProtocolInfo, @@ -72,7 +71,9 @@ data class ConversationViewEntity( val userSupportedProtocols: Set?, val userActiveOneOnOneConversationId: ConversationIDEntity?, val proteusVerificationStatus: ConversationEntity.VerificationStatus, - val legalHoldStatus: ConversationEntity.LegalHoldStatus + val legalHoldStatus: ConversationEntity.LegalHoldStatus, + val accentId: Int?, + val isFavorite: Boolean, ) { val isMember: Boolean get() = selfRole != null } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt new file mode 100644 index 00000000000..ed1ae82a9f8 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAO.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import kotlinx.coroutines.flow.Flow + +interface ConversationFolderDAO { + suspend fun getFoldersWithConversations(): List + suspend fun observeConversationListFromFolder(folderId: String): Flow> + suspend fun getFavoriteConversationFolder(): ConversationFolderEntity + suspend fun updateConversationFolders(folderWithConversationsList: List) + suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) + suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt new file mode 100644 index 00000000000..3c19bd871b3 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOImpl.kt @@ -0,0 +1,106 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation.folder + +import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.ConversationFoldersQueries +import com.wire.kalium.persistence.GetAllFoldersWithConversations +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper +import com.wire.kalium.persistence.util.mapToList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext + +class ConversationFolderDAOImpl internal constructor( + private val conversationFoldersQueries: ConversationFoldersQueries, + private val coroutineContext: CoroutineContext, +) : ConversationFolderDAO { + private val conversationDetailsWithEventsMapper = ConversationDetailsWithEventsMapper + + override suspend fun getFoldersWithConversations(): List = withContext(coroutineContext) { + val labeledConversationList = conversationFoldersQueries.getAllFoldersWithConversations().executeAsList().map(::toEntity) + + val folderMap = labeledConversationList.groupBy { it.folderId }.mapValues { entry -> + val folderId = entry.key + val firstRow = entry.value.first() + FolderWithConversationsEntity( + id = folderId, + name = firstRow.folderName, + type = firstRow.folderType, + conversationIdList = entry.value.mapNotNull { it.conversationId } + ) + } + + folderMap.values.toList() + } + + private fun toEntity(row: GetAllFoldersWithConversations) = LabeledConversationEntity( + folderId = row.label_id, + folderName = row.label_name, + folderType = row.label_type, + conversationId = row.conversation_id + ) + + override suspend fun observeConversationListFromFolder(folderId: String): Flow> { + return conversationFoldersQueries.getConversationsFromFolder( + folderId, + conversationDetailsWithEventsMapper::fromViewToModel + ) + .asFlow() + .mapToList() + .flowOn(coroutineContext) + } + + override suspend fun getFavoriteConversationFolder(): ConversationFolderEntity { + return conversationFoldersQueries.getFavoriteFolder { id, name, folderType -> + ConversationFolderEntity(id, name, folderType) + } + .executeAsOne() + } + + override suspend fun updateConversationFolders(folderWithConversationsList: List) = + withContext(coroutineContext) { + conversationFoldersQueries.transaction { + conversationFoldersQueries.clearFolders() + folderWithConversationsList.forEach { folderWithConversations -> + conversationFoldersQueries.upsertFolder( + folderWithConversations.id, + folderWithConversations.name, + folderWithConversations.type + ) + folderWithConversations.conversationIdList.forEach { conversationId -> + conversationFoldersQueries.insertLabeledConversation( + conversationId, + folderWithConversations.id + ) + } + } + } + } + + override suspend fun addConversationToFolder(conversationId: QualifiedIDEntity, folderId: String) = withContext(coroutineContext) { + conversationFoldersQueries.insertLabeledConversation(conversationId, folderId) + } + + override suspend fun removeConversationFromFolder(conversationId: QualifiedIDEntity, folderId: String) = withContext(coroutineContext) { + conversationFoldersQueries.deleteLabeledConversation(conversationId, folderId) + } +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt new file mode 100644 index 00000000000..77ceccda8f5 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderEntity.kt @@ -0,0 +1,45 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.dao.QualifiedIDEntity + +data class ConversationFolderEntity( + val id: String, + val name: String, + val type: ConversationFolderTypeEntity +) + +data class FolderWithConversationsEntity( + val id: String, + val name: String, + val type: ConversationFolderTypeEntity, + val conversationIdList: List +) + +data class LabeledConversationEntity( + val folderId: String, + val folderName: String, + val folderType: ConversationFolderTypeEntity, + val conversationId: QualifiedIDEntity? +) + +enum class ConversationFolderTypeEntity { + USER, + FAVORITE +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt index 47c34917679..7ea77f7010a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/member/MemberDAO.kt @@ -26,6 +26,7 @@ import com.wire.kalium.persistence.dao.ConversationIDEntity import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.NameAndHandleEntity import com.wire.kalium.persistence.kaliumLogger import com.wire.kalium.persistence.util.mapToList import com.wire.kalium.persistence.util.mapToOneOrNull @@ -65,6 +66,7 @@ interface MemberDAO { ): Map> suspend fun getOneOneConversationWithFederatedMembers(domain: String): Map + suspend fun selectMembersNameAndHandle(conversationId: QualifiedIDEntity): Map } @Suppress("TooManyFunctions") @@ -121,7 +123,7 @@ internal class MemberDAOImpl internal constructor( override suspend fun insertMembers(memberList: List, groupId: String) { withContext(coroutineContext) { conversationsQueries.selectByGroupId(groupId).executeAsOneOrNull()?.let { - nonSuspendInsertMembersWithQualifiedId(memberList, it.qualifiedId) + nonSuspendInsertMembersWithQualifiedId(memberList, it.qualified_id) } } } @@ -221,4 +223,9 @@ internal class MemberDAOImpl internal constructor( .executeAsList() .associateBy({ it.conversation }, { it.user }) } + + override suspend fun selectMembersNameAndHandle(conversationId: QualifiedIDEntity) = withContext(coroutineContext) { + memberQueries.selectMembersNamesAndHandle(conversationId).executeAsList() + .let { members -> members.associate { it.user to NameAndHandleEntity(it.name, it.handle) } } + } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt index 870e2042528..e37edbceea0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAO.kt @@ -50,7 +50,6 @@ interface MessageDAO { */ suspend fun insertOrIgnoreMessage( message: MessageEntity, - updateConversationReadDate: Boolean = false, updateConversationModifiedDate: Boolean = false ): InsertMessageResult diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt index 46d8da0057e..06d1f66ba3d 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOImpl.kt @@ -89,17 +89,11 @@ internal class MessageDAOImpl internal constructor( override suspend fun insertOrIgnoreMessage( message: MessageEntity, - updateConversationReadDate: Boolean, updateConversationModifiedDate: Boolean ) = withContext(coroutineContext) { queries.transactionWithResult { val messageCreationInstant = message.date - if (updateConversationReadDate) { - conversationsQueries.updateConversationReadDate(messageCreationInstant, message.conversationId) - unreadEventsQueries.deleteUnreadEvents(message.date, message.conversationId) - } - insertInDB(message) val needsToBeNotified = nonSuspendNeedsToBeNotified(message.id, message.conversationId) @@ -380,7 +374,7 @@ internal class MessageDAOImpl internal constructor( messagePreviewQueries.getLastMessages(mapper::toPreviewEntity).asFlow().flowOn(coroutineContext).mapToList() override suspend fun observeConversationsUnreadEvents(): Flow> { - return unreadEventsQueries.getConversationsUnreadEvents(unreadEventMapper::toConversationUnreadEntity) + return unreadEventsQueries.getConversationsUnreadEventCountsGrouped(unreadEventMapper::toConversationUnreadEntity) .asFlow().mapToList() } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt index 5a89b8cc834..6199140d16c 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageEntity.kt @@ -308,6 +308,7 @@ sealed class MessageEntityContent { data class FailedDecryption( val encodedData: ByteArray? = null, + val code: Int?, val isDecryptionResolved: Boolean, val senderUserId: QualifiedIDEntity, val senderClientId: String?, diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt index 1f0566aa080..d98c426cf27 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageInsertExtension.kt @@ -193,6 +193,7 @@ internal class MessageInsertExtensionImpl( message_id = message.id, conversation_id = message.conversationId, unknown_encoded_data = content.encodedData, + error_code = content.code?.toLong() ) is MessageEntityContent.MemberChange -> messagesQueries.insertMemberChangeMessage( diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt index 399ae11c358..b07c3e05472 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/MessageMapper.kt @@ -486,6 +486,7 @@ object MessageMapper { restrictedAssetSize: Long?, restrictedAssetName: String?, failedToDecryptData: ByteArray?, + decryptionErrorCode: Long?, isDecryptionResolved: Boolean?, conversationName: String?, allReactionsJson: String, @@ -576,6 +577,7 @@ object MessageMapper { MessageEntity.ContentType.FAILED_DECRYPTION -> MessageEntityContent.FailedDecryption( encodedData = failedToDecryptData, + code = decryptionErrorCode?.toInt(), isDecryptionResolved = isDecryptionResolved ?: false, senderUserId = senderUserId, senderClientId = senderClientId diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt index e8e79502456..6a57172ac2e 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOImpl.kt @@ -18,10 +18,11 @@ package com.wire.kalium.persistence.dao.message.draft import app.cash.sqldelight.coroutines.asFlow +import com.wire.kalium.persistence.ConversationsQueries import com.wire.kalium.persistence.MessageDraftsQueries +import com.wire.kalium.persistence.MessagesQueries import com.wire.kalium.persistence.dao.ConversationIDEntity -import com.wire.kalium.persistence.dao.QualifiedIDEntity -import com.wire.kalium.persistence.dao.message.MessageEntity +import com.wire.kalium.persistence.dao.message.draft.MessageDraftMapper.toDao import com.wire.kalium.persistence.util.mapToList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn @@ -30,18 +31,50 @@ import kotlin.coroutines.CoroutineContext class MessageDraftDAOImpl internal constructor( private val queries: MessageDraftsQueries, + private val messagesQueries: MessagesQueries, + private val conversationsQueries: ConversationsQueries, private val coroutineContext: CoroutineContext, ) : MessageDraftDAO { override suspend fun upsertMessageDraft(messageDraft: MessageDraftEntity) = withContext(coroutineContext) { - queries.upsertDraft( - conversation_id = messageDraft.conversationId, - text = messageDraft.text, - edit_message_id = messageDraft.editMessageId, - quoted_message_id = messageDraft.quotedMessageId, - mention_list = messageDraft.selectedMentionList - ) + val conversationExists = conversationsQueries.selectConversationByQualifiedId(messageDraft.conversationId) + .executeAsOneOrNull() != null + + if (!conversationExists) { + return@withContext + } + + if (messageDraft.editMessageId != null) { + val messageExists = messagesQueries.getMessage(messageDraft.editMessageId, messageDraft.conversationId) + .executeAsOneOrNull() != null + if (!messageExists) { + return@withContext + } + } + + if (messageDraft.quotedMessageId != null) { + val quotedMessageExists = messagesQueries.getMessage(messageDraft.quotedMessageId, messageDraft.conversationId) + .executeAsOneOrNull() != null + if (!quotedMessageExists) { + return@withContext + } + } + + queries.transaction { + queries.upsertDraft( + conversation_id = messageDraft.conversationId, + text = messageDraft.text, + edit_message_id = messageDraft.editMessageId, + quoted_message_id = messageDraft.quotedMessageId, + mention_list = messageDraft.selectedMentionList + ) + val changes = queries.selectChanges().executeAsOne() + if (changes == 0L) { + // rollback the transaction if no changes were made so that it doesn't notify other queries about changes if not needed + this.rollback() + } + } } override suspend fun getMessageDraft(conversationIDEntity: ConversationIDEntity): MessageDraftEntity? = @@ -57,19 +90,4 @@ class MessageDraftDAOImpl internal constructor( .asFlow() .flowOn(coroutineContext) .mapToList() - - private fun toDao( - conversationId: QualifiedIDEntity, - text: String?, - editMessageId: String?, - quotedMessageId: String?, - mentionList: List - ): MessageDraftEntity = - MessageDraftEntity( - conversationId = conversationId, - text = text.orEmpty(), - editMessageId = editMessageId, - quotedMessageId = quotedMessageId, - selectedMentionList = mentionList - ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt new file mode 100644 index 00000000000..40d1e622ce3 --- /dev/null +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftMapper.kt @@ -0,0 +1,31 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.message.draft + +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.message.MessageEntity + +data object MessageDraftMapper { + fun toDao( + conversationId: QualifiedIDEntity, + text: String?, + editMessageId: String?, + quotedMessageId: String?, + mentionList: List + ): MessageDraftEntity = MessageDraftEntity(conversationId, text.orEmpty(), editMessageId, quotedMessageId, mentionList) +} diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionMapper.kt index bdba6a14519..545fa92cf48 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionMapper.kt @@ -46,7 +46,8 @@ object ReactionMapper { userTypeEntity = userType, deleted = deleted, connectionStatus = connectionStatus, - availabilityStatus = userAvailabilityStatus + availabilityStatus = userAvailabilityStatus, + accentId = accentId ) } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionsEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionsEntity.kt index 227afb0a3df..a6bd98adbd1 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionsEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/reaction/ReactionsEntity.kt @@ -41,7 +41,8 @@ data class MessageReactionEntity( val userTypeEntity: UserTypeEntity, val deleted: Boolean, val connectionStatus: ConnectionEntity.State, - val availabilityStatus: UserAvailabilityStatusEntity + val availabilityStatus: UserAvailabilityStatusEntity, + val accentId: Int ) typealias ReactionsCountEntity = Map diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/DetailedReceiptEntity.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/DetailedReceiptEntity.kt index 48637ea8867..8e64740e2a0 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/DetailedReceiptEntity.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/DetailedReceiptEntity.kt @@ -34,5 +34,6 @@ data class DetailedReceiptEntity( val userType: UserTypeEntity, val isUserDeleted: Boolean, val connectionStatus: ConnectionEntity.State, - val availabilityStatus: UserAvailabilityStatusEntity + val availabilityStatus: UserAvailabilityStatusEntity, + val accentId: Int ) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/ReceiptMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/ReceiptMapper.kt index 8b110bd6797..379ce1281bd 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/ReceiptMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/receipt/ReceiptMapper.kt @@ -40,6 +40,7 @@ internal object ReceiptMapper { isUserDeleted: Boolean, connectionStatus: ConnectionEntity.State, userAvailabilityStatus: UserAvailabilityStatusEntity, + accentId: Int ) = DetailedReceiptEntity( type = type, date = Instant.parse(date), @@ -51,5 +52,6 @@ internal object ReceiptMapper { isUserDeleted = isUserDeleted, connectionStatus = connectionStatus, availabilityStatus = userAvailabilityStatus, + accentId = accentId ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt index 77456973dd9..6cbf40001ac 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/dao/unread/UserConfigDAO.kt @@ -66,6 +66,10 @@ interface UserConfigDAO { suspend fun setPreviousTrackingIdentifier(identifier: String) suspend fun getPreviousTrackingIdentifier(): String? suspend fun deletePreviousTrackingIdentifier() + suspend fun getNextTimeForCallFeedback(): Long? + suspend fun setNextTimeForCallFeedback(timestamp: Long) + suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) + suspend fun getShouldFetchE2EITrustAnchorHasRun(): Boolean } @Suppress("TooManyFunctions") @@ -219,17 +223,31 @@ internal class UserConfigDAOImpl internal constructor( metadataDAO.deleteValue(key = ANALYTICS_TRACKING_IDENTIFIER_PREVIOUS_KEY) } + override suspend fun getNextTimeForCallFeedback(): Long? = metadataDAO.valueByKey(NEXT_TIME_TO_ASK_CALL_FEEDBACK)?.toLong() + + override suspend fun setNextTimeForCallFeedback(timestamp: Long) = + metadataDAO.insertValue(timestamp.toString(), NEXT_TIME_TO_ASK_CALL_FEEDBACK) + + override suspend fun setShouldFetchE2EITrustAnchors(shouldFetch: Boolean) { + metadataDAO.insertValue(value = shouldFetch.toString(), key = SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS) + } + + override suspend fun getShouldFetchE2EITrustAnchorHasRun(): Boolean = + metadataDAO.valueByKey(SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS)?.toBoolean() ?: true + private companion object { private const val DEFAULT_CIPHER_SUITE_KEY = "DEFAULT_CIPHER_SUITE" private const val SELF_DELETING_MESSAGES_KEY = "SELF_DELETING_MESSAGES" private const val SHOULD_NOTIFY_FOR_REVOKED_CERTIFICATE = "should_notify_for_revoked_certificate" private const val MLS_MIGRATION_KEY = "MLS_MIGRATION" private const val SUPPORTED_PROTOCOLS_KEY = "SUPPORTED_PROTOCOLS" + private const val NEXT_TIME_TO_ASK_CALL_FEEDBACK = "next_time_to_ask_for_feedback_about_call" const val LEGAL_HOLD_REQUEST = "legal_hold_request" const val LEGAL_HOLD_CHANGE_NOTIFIED = "legal_hold_change_notified" const val SHOULD_UPDATE_CLIENT_LEGAL_HOLD_CAPABILITY = "should_update_client_legal_hold_capability" private const val ANALYTICS_TRACKING_IDENTIFIER_PREVIOUS_KEY = "analytics_tracking_identifier_previous" private const val ANALYTICS_TRACKING_IDENTIFIER_KEY = "analytics_tracking_identifier" + const val SHOULD_FETCH_E2EI_GET_TRUST_ANCHORS = "should_fetch_e2ei_trust_anchors" } } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt index 707e857eb1c..7774eae162a 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/daokaliumdb/ServerConfigurationDAO.kt @@ -126,6 +126,7 @@ interface ServerConfigurationDAO { fun configById(id: String): ServerConfigEntity? suspend fun configByLinks(links: ServerConfigEntity.Links): ServerConfigEntity? suspend fun updateApiVersion(id: String, commonApiVersion: Int) + suspend fun getCommonApiVersion(domain: String): Int suspend fun updateApiVersionAndDomain(id: String, domain: String, commonApiVersion: Int) suspend fun configForUser(userId: UserIDEntity): ServerConfigEntity? suspend fun setFederationToTrue(id: String) @@ -213,6 +214,10 @@ internal class ServerConfigurationDAOImpl internal constructor( queries.updateApiVersion(commonApiVersion, id) } + override suspend fun getCommonApiVersion(domain: String): Int = withContext(queriesContext) { + queries.getCommonApiVersionByDomain(domain).executeAsOne() + } + override suspend fun updateApiVersionAndDomain(id: String, domain: String, commonApiVersion: Int) = withContext(queriesContext) { queries.updateApiVersionAndDomain(commonApiVersion, domain, id) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt index 67bc60f24c5..f58f4f2b279 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/TableMapper.kt @@ -25,7 +25,10 @@ import com.wire.kalium.persistence.Call import com.wire.kalium.persistence.Client import com.wire.kalium.persistence.Connection import com.wire.kalium.persistence.Conversation +import com.wire.kalium.persistence.ConversationFolder import com.wire.kalium.persistence.ConversationLegalHoldStatusChangeNotified +import com.wire.kalium.persistence.LastMessage +import com.wire.kalium.persistence.LabeledConversation import com.wire.kalium.persistence.Member import com.wire.kalium.persistence.Message import com.wire.kalium.persistence.MessageAssetContent @@ -265,4 +268,17 @@ internal object TableMapper { conversation_idAdapter = QualifiedIDAdapter, mention_listAdapter = MentionListAdapter() ) + + val lastMessageAdapter = LastMessage.Adapter( + conversation_idAdapter = QualifiedIDAdapter, + creation_dateAdapter = InstantTypeAdapter, + ) + + val labeledConversationAdapter = LabeledConversation.Adapter( + conversation_idAdapter = QualifiedIDAdapter + ) + + val conversationFolderAdapter = ConversationFolder.Adapter( + folder_typeAdapter = EnumColumnAdapter() + ) } diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt index 06602796798..7c75ee447b6 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/db/UserDatabaseBuilder.kt @@ -58,6 +58,8 @@ import com.wire.kalium.persistence.dao.conversation.ConversationEntity import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAO import com.wire.kalium.persistence.dao.conversation.ConversationMetaDataDAOImpl import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAO +import com.wire.kalium.persistence.dao.conversation.folder.ConversationFolderDAOImpl import com.wire.kalium.persistence.dao.member.MemberDAO import com.wire.kalium.persistence.dao.member.MemberDAOImpl import com.wire.kalium.persistence.dao.member.MemberEntity @@ -157,7 +159,10 @@ class UserDatabaseBuilder internal constructor( TableMapper.messageConversationProtocolChangedDuringACAllContentAdapter, ConversationLegalHoldStatusChangeNotifiedAdapter = TableMapper.conversationLegalHoldStatusChangeNotifiedAdapter, MessageAssetTransferStatusAdapter = TableMapper.messageAssetTransferStatusAdapter, - MessageDraftAdapter = TableMapper.messageDraftsAdapter + MessageDraftAdapter = TableMapper.messageDraftsAdapter, + LastMessageAdapter = TableMapper.lastMessageAdapter, + LabeledConversationAdapter = TableMapper.labeledConversationAdapter, + ConversationFolderAdapter = TableMapper.conversationFolderAdapter ) init { @@ -193,11 +198,19 @@ class UserDatabaseBuilder internal constructor( conversationDetailsCache, conversationCache, database.conversationsQueries, + database.conversationDetailsQueries, + database.conversationDetailsWithEventsQueries, database.membersQueries, database.unreadEventsQueries, queriesContext, ) + val conversationFolderDAO: ConversationFolderDAO + get() = ConversationFolderDAOImpl( + database.conversationFoldersQueries, + queriesContext + ) + private val conversationMembersCache = FlowCache>(databaseScope) @@ -256,6 +269,8 @@ class UserDatabaseBuilder internal constructor( val messageDraftDAO = MessageDraftDAOImpl( database.messageDraftsQueries, + database.messagesQueries, + database.conversationsQueries, queriesContext ) diff --git a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt index 6d0ed44cc61..dc306760bab 100644 --- a/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt +++ b/persistence/src/commonMain/kotlin/com/wire/kalium/persistence/model/LogoutReason.kt @@ -38,5 +38,10 @@ enum class LogoutReason { /** * Session Expired. */ - SESSION_EXPIRED; + SESSION_EXPIRED, + + /** + * The migration to CC failed. + */ + MIGRATION_TO_CC_FAILED } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigDAOTest.kt index 8a95cc96f37..a1902160ce8 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/config/UserConfigDAOTest.kt @@ -123,4 +123,21 @@ class UserConfigDAOTest : BaseDatabaseTest() { assertEquals(thirdExpectedValue, thirdValue) } } + + @Test + fun givenNoValueStoredForShouldFetchE2EITrustAnchorHasRun_whenCalled_thenReturnTrue() = runTest { + assertTrue(userConfigDAO.getShouldFetchE2EITrustAnchorHasRun()) + } + + @Test + fun givenShouldFetchE2EITrustAnchorHasRunIsSetToFalse_whenCalled_thenReturnFalse() = runTest { + userConfigDAO.setShouldFetchE2EITrustAnchors(false) + assertFalse(userConfigDAO.getShouldFetchE2EITrustAnchorHasRun()) + } + + @Test + fun givenShouldFetchE2EITrustAnchorHasRunIsSetToTrue_whenCalled_thenReturnTrue() = runTest { + userConfigDAO.setShouldFetchE2EITrustAnchors(true) + assertTrue(userConfigDAO.getShouldFetchE2EITrustAnchorHasRun()) + } } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt new file mode 100644 index 00000000000..d01cd2e2e38 --- /dev/null +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/conversation/ConversationExtensionsTest.kt @@ -0,0 +1,200 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.kalium.persistence.conversation + +import app.cash.paging.PagingConfig +import app.cash.paging.PagingSource +import app.cash.paging.PagingSourceLoadParamsAppend +import app.cash.paging.PagingSourceLoadParamsRefresh +import app.cash.paging.PagingSourceLoadResultPage +import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.dao.ConnectionDAO +import com.wire.kalium.persistence.dao.ConversationIDEntity +import com.wire.kalium.persistence.dao.UserDAO +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDAO +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsEntity +import com.wire.kalium.persistence.dao.conversation.ConversationDetailsWithEventsMapper +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationExtensions +import com.wire.kalium.persistence.dao.conversation.ConversationExtensionsImpl +import com.wire.kalium.persistence.dao.member.MemberDAO +import com.wire.kalium.persistence.dao.message.KaliumPager +import com.wire.kalium.persistence.dao.message.MessageDAO +import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO +import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newUserEntity +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Instant +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class ConversationExtensionsTest : BaseDatabaseTest() { + private lateinit var conversationExtensions: ConversationExtensions + private lateinit var messageDAO: MessageDAO + private lateinit var messageDraftDAO: MessageDraftDAO + private lateinit var conversationDAO: ConversationDAO + private lateinit var connectionDAO: ConnectionDAO + private lateinit var memberDAO: MemberDAO + private lateinit var userDAO: UserDAO + private val selfUserId = UserIDEntity("selfValue", "selfDomain") + + @BeforeTest + fun setUp() { + deleteDatabase(selfUserId) + val db = createDatabase(selfUserId, encryptedDBSecret, true) + val queries = db.database.conversationDetailsWithEventsQueries + messageDAO = db.messageDAO + messageDraftDAO = db.messageDraftDAO + conversationDAO = db.conversationDAO + connectionDAO = db.connectionDAO + memberDAO = db.memberDAO + userDAO = db.userDAO + conversationExtensions = ConversationExtensionsImpl(queries, ConversationDetailsWithEventsMapper, dispatcher) + } + + @AfterTest + fun tearDown() { + deleteDatabase(selfUserId) + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenItShouldContainTheCorrectCountBeforeAndAfter() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched, itemsAfter should be the remaining ones + assertEquals(CONVERSATION_COUNT - PAGE_SIZE, result.itemsAfter) + assertEquals(0, result.itemsBefore) // No items before the first page + } + + @Test + fun givenInsertedConversations_whenGettingFirstSearchedPage_thenItShouldContainTheCorrectCountBeforeAndAfter() = runTest(dispatcher) { + populateData() + val searchQuery = "conversation 1" + val result = getPager(searchQuery = searchQuery).pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched containing only 11 results ("conversation 1" and "conversation 10" to "conversation 19") + assertEquals(0, result.itemsAfter) // Since the page has fewer elements than PAGE_SIZE, there should be no items after this page + assertEquals(0, result.itemsBefore) // No items before the first page + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenTheNextKeyShouldBeTheFirstItemOfTheNextPage() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + assertEquals(PAGE_SIZE, result.nextKey) // First page fetched, second page starts at the end of the first one + } + + @Test + fun givenInsertedConversations_whenGettingFirstPage_thenItShouldContainTheFirstPageOfItems() = runTest(dispatcher) { + populateData() + val result = getPager().pagingSource.refresh() + assertIs>(result) + result.data.forEachIndexed { index, conversation -> + assertEquals("$CONVERSATION_ID_PREFIX$index", conversation.conversationViewEntity.id.value) + assertEquals(false, conversation.conversationViewEntity.archived) + } + } + + @Test + fun givenInsertedConversations_whenGettingSecondPage_thenShouldContainTheCorrectItems() = runTest(dispatcher) { + populateData() + val pagingSource = getPager().pagingSource + val secondPageResult = pagingSource.nextPageForOffset(PAGE_SIZE) + assertIs>(secondPageResult) + assertFalse { secondPageResult.data.isEmpty() } + assertTrue { secondPageResult.data.size <= PAGE_SIZE } + secondPageResult.data.forEachIndexed { index, conversation -> + assertEquals("$CONVERSATION_ID_PREFIX${index + PAGE_SIZE}", conversation.conversationViewEntity.id.value) + } + } + + @Test + fun givenInsertedConversations_whenGettingFirstPageOfArchivedConversations_thenItShouldContainArchivedItems() = runTest(dispatcher) { + populateData(archived = false, count = CONVERSATION_COUNT, conversationIdPrefix = CONVERSATION_ID_PREFIX) + populateData(archived = true, count = CONVERSATION_COUNT, conversationIdPrefix = ARCHIVED_CONVERSATION_ID_PREFIX) + val result = getPager(fromArchive = true).pagingSource.refresh() + assertIs>(result) + result.data.forEachIndexed { index, conversation -> + assertEquals("$ARCHIVED_CONVERSATION_ID_PREFIX$index", conversation.conversationViewEntity.id.value) + assertEquals(true, conversation.conversationViewEntity.archived) + } + } + + @Test + fun givenInsertedConversations_whenGettingFirstSearchedPage_thenShouldContainTheCorrectItems() = runTest(dispatcher) { + populateData() + val searchQuery = "conversation 1" + val result = getPager(searchQuery = searchQuery).pagingSource.refresh() + assertIs>(result) + // Assuming the first page was fetched containing only 11 results ["conversation 1" and "conversation 10" to "conversation 19"] + assertEquals(11, result.data.size) + result.data.forEachIndexed { index, conversation -> + assertEquals(true, conversation.conversationViewEntity.name?.contains(searchQuery) ?: false) + } + } + + private fun getPager(searchQuery: String = "", fromArchive: Boolean = false): KaliumPager = + conversationExtensions.getPagerForConversationDetailsWithEventsSearch( + pagingConfig = PagingConfig(PAGE_SIZE), + queryConfig = ConversationExtensions.QueryConfig(searchQuery = searchQuery, fromArchive = fromArchive), + ) + + private suspend fun PagingSource.refresh() = + load(PagingSourceLoadParamsRefresh(null, PAGE_SIZE, false)) + + private suspend fun PagingSource.nextPageForOffset(key: Int) = + load(PagingSourceLoadParamsAppend(key, PAGE_SIZE, true)) + + private suspend fun populateData( + archived: Boolean = false, + count: Int = CONVERSATION_COUNT, + conversationIdPrefix: String = CONVERSATION_ID_PREFIX, + ) { + userDAO.upsertUser(newUserEntity(qualifiedID = UserIDEntity("user", "domain"))) + repeat(count) { + // Ordered by date - Inserting with decreasing date is important to assert pagination + val lastModified = Instant.fromEpochSeconds(CONVERSATION_COUNT - it.toLong()) + val lastRead = lastModified.minus(1.seconds) // if message needs to be unread, then lastRead should be before lastModified + val conversation = newConversationEntity(ConversationIDEntity("$conversationIdPrefix$it", "domain")).copy( + name = "conversation $it", + type = ConversationEntity.Type.GROUP, + lastModifiedDate = lastModified, + lastReadDate = lastRead, + archived = archived, + ) + conversationDAO.insertConversation(conversation) + } + } + + private companion object { + const val CONVERSATION_COUNT = 100 + const val CONVERSATION_ID_PREFIX = "conversation_" + const val ARCHIVED_CONVERSATION_ID_PREFIX = "archived_conversation_" + const val PAGE_SIZE = 20 + } +} diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt index 6ca8c5826ff..28333dfa8c5 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConnectionDaoTest.kt @@ -20,7 +20,6 @@ package com.wire.kalium.persistence.dao import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.db.UserDatabaseBuilder -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlinx.datetime.toInstant @@ -28,7 +27,6 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals -@OptIn(ExperimentalCoroutinesApi::class) class ConnectionDaoTest : BaseDatabaseTest() { private val connection1 = connectionEntity("1") diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt index 432b4c4f99a..a000851a0d6 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/ConversationDAOTest.kt @@ -22,10 +22,13 @@ import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.asset.AssetDAO import com.wire.kalium.persistence.dao.asset.AssetEntity +import com.wire.kalium.persistence.dao.call.CallDAO +import com.wire.kalium.persistence.dao.call.CallEntity import com.wire.kalium.persistence.dao.client.ClientDAO import com.wire.kalium.persistence.dao.client.InsertClientParam import com.wire.kalium.persistence.dao.conversation.ConversationDAO import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.conversation.ConversationFilterEntity import com.wire.kalium.persistence.dao.conversation.ConversationGuestLinkEntity import com.wire.kalium.persistence.dao.conversation.ConversationViewEntity import com.wire.kalium.persistence.dao.conversation.E2EIConversationClientInfoEntity @@ -38,8 +41,11 @@ import com.wire.kalium.persistence.dao.member.MemberEntity import com.wire.kalium.persistence.dao.message.MessageDAO import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent +import com.wire.kalium.persistence.dao.message.draft.MessageDraftDAO +import com.wire.kalium.persistence.dao.unread.UnreadEventTypeEntity import com.wire.kalium.persistence.utils.IgnoreIOS import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newDraftMessageEntity import com.wire.kalium.persistence.utils.stubs.newRegularMessageEntity import com.wire.kalium.persistence.utils.stubs.newSystemMessageEntity import com.wire.kalium.persistence.utils.stubs.newUserEntity @@ -69,10 +75,12 @@ class ConversationDAOTest : BaseDatabaseTest() { private lateinit var clientDao: ClientDAO private lateinit var connectionDAO: ConnectionDAO private lateinit var messageDAO: MessageDAO + private lateinit var messageDraftDAO: MessageDraftDAO private lateinit var userDAO: UserDAO private lateinit var teamDAO: TeamDAO private lateinit var memberDAO: MemberDAO private lateinit var assertDAO: AssetDAO + private lateinit var callDAO: CallDAO private val selfUserId = UserIDEntity("selfValue", "selfDomain") @BeforeTest @@ -83,14 +91,16 @@ class ConversationDAOTest : BaseDatabaseTest() { clientDao = db.clientDAO connectionDAO = db.connectionDAO messageDAO = db.messageDAO + messageDraftDAO = db.messageDraftDAO userDAO = db.userDAO teamDAO = db.teamDAO memberDAO = db.memberDAO assertDAO = db.assetDAO + callDAO = db.callDAO } @Test - fun givenConversationIsInserted_whenFetchingById_thenConversationIsReturned() = runTest { + fun givenConversationIsInserted_whenFetchingById_thenConversationIsReturned() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val result = conversationDAO.getConversationDetailsById(conversationEntity1.id) @@ -98,7 +108,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenListOfConversations_ThenMultipleConversationsCanBeInsertedAtOnce() = runTest { + fun givenListOfConversations_ThenMultipleConversationsCanBeInsertedAtOnce() = runTest(dispatcher) { conversationDAO.insertConversations(listOf(conversationEntity1, conversationEntity2)) insertTeamUserAndMember(team, user1, conversationEntity1.id) insertTeamUserAndMember(team, user2, conversationEntity2.id) @@ -109,7 +119,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationCanBeDeleted() = runTest { + fun givenExistingConversation_ThenConversationCanBeDeleted() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) conversationDAO.deleteConversationByQualifiedID(conversationEntity1.id) val result = try { @@ -121,7 +131,21 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationCanBeUpdated() = runTest { + fun givenExistingConversation_WhenReinserting_ThenGroupStateIsUpdated() = runTest(dispatcher) { + conversationDAO.insertConversation(conversationEntity2) + conversationDAO.insertConversation( + conversationEntity2.copy( + protocolInfo = mlsProtocolInfo1.copy( + groupState = ConversationEntity.GroupState.PENDING_JOIN + ) + ) + ) + val result = conversationDAO.getConversationById(conversationEntity2.id) + assertEquals(ConversationEntity.GroupState.PENDING_JOIN, (result?.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupState) + } + + @Test + fun givenExistingConversation_ThenConversationCanBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val updatedConversation1Entity = conversationEntity1.copy(name = "Updated conversation1") @@ -134,9 +158,9 @@ class ConversationDAOTest : BaseDatabaseTest() { fun givenExistingConversation_ThenConversationCanBeRetrievedByGroupID() = runTest { conversationDAO.insertConversation(conversationEntity2) insertTeamUserAndMember(team, user2, conversationEntity2.id) - val result = - conversationDAO.observeConversationByGroupID((conversationEntity2.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupId) - .first() + val result = conversationDAO.observeConversationDetailsByGroupID( + (conversationEntity2.protocolInfo as ConversationEntity.ProtocolInfo.MLS).groupId + ).first() assertEquals(conversationEntity2.toViewEntity(user2), result) } @@ -254,7 +278,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationGroupStateCanBeUpdated() = runTest { + fun givenExistingConversation_ThenConversationGroupStateCanBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateConversationGroupState( ConversationEntity.GroupState.PENDING_WELCOME_MESSAGE, @@ -267,7 +291,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationGroupStateCanBeUpdatedToEstablished() = runTest { + fun givenExistingConversation_ThenConversationGroupStateCanBeUpdatedToEstablished() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateMlsGroupStateAndCipherSuite( ConversationEntity.GroupState.PENDING_WELCOME_MESSAGE, @@ -285,7 +309,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_ThenConversationIsUpdatedOnInsert() = runTest { + fun givenExistingConversation_ThenConversationIsUpdatedOnInsert() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity1) insertTeamUserAndMember(team, user1, conversationEntity1.id) val updatedConversation1Entity = conversationEntity1.copy(name = "Updated conversation1") @@ -295,7 +319,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAnExistingConversation_WhenUpdatingTheMutingStatus_ThenConversationShouldBeUpdated() = runTest { + fun givenAnExistingConversation_WhenUpdatingTheMutingStatus_ThenConversationShouldBeUpdated() = runTest(dispatcher) { conversationDAO.insertConversation(conversationEntity2) conversationDAO.updateConversationMutedStatus( conversationId = conversationEntity2.id, @@ -310,7 +334,7 @@ class ConversationDAOTest : BaseDatabaseTest() { @Test @IgnoreIOS - fun givenConversation_whenInsertingStoredConversation_thenLastChangesTimeIsNotChanged() = runTest { + fun givenConversation_whenInsertingStoredConversation_thenLastChangesTimeIsNotChanged() = runTest(dispatcher) { val convStored = conversationEntity1.copy( lastNotificationDate = "2022-04-30T15:36:00.000Z".toInstant(), lastModifiedDate = "2022-03-30T15:36:00.000Z".toInstant(), @@ -335,7 +359,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenConversation_whenUpdatingAccessInfo_thenItsUpdated() = runTest { + fun givenConversation_whenUpdatingAccessInfo_thenItsUpdated() = runTest(dispatcher) { val convStored = conversationEntity1.copy( accessRole = listOf(ConversationEntity.AccessRole.TEAM_MEMBER), access = listOf(ConversationEntity.Access.INVITE) ) @@ -358,7 +382,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenExistingConversation_whenUpdatingTheConversationLastReadDate_ThenTheConversationHasTheDate() = runTest { + fun givenExistingConversation_whenUpdatingTheConversationLastReadDate_ThenTheConversationHasTheDate() = runTest(dispatcher) { // given val expectedLastReadDate = Instant.fromEpochMilliseconds(1648654560000) @@ -376,7 +400,7 @@ class ConversationDAOTest : BaseDatabaseTest() { @Test fun givenExistingConversation_whenUpdatingTheConversationSeenDate_thenEmitTheNewConversationStateWithTheUpdatedSeenDate() = - runTest { + runTest(dispatcher) { // given val expectedConversationSeenDate = Instant.fromEpochMilliseconds(1648654560000) @@ -440,7 +464,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenNewValue_whenUpdatingProtocol_thenItsUpdatedAndReportedAsChanged() = runTest { + fun givenNewValue_whenUpdatingProtocol_thenItsUpdatedAndReportedAsChanged() = runTest(dispatcher) { val conversation = conversationEntity5 val groupId = "groupId" val updatedCipherSuite = ConversationEntity.CipherSuite.MLS_256_DHKEMP521_AES256GCM_SHA512_P521 @@ -484,7 +508,7 @@ class ConversationDAOTest : BaseDatabaseTest() { // when conversationDAO.updateKeyingMaterial(conversationProtocolInfo.groupId, newUpdate) // then - assertEquals(expected, conversationDAO.observeConversationByGroupID(conversationProtocolInfo.groupId).first()?.protocolInfo) + assertEquals(expected, conversationDAO.observeConversationDetailsByGroupID(conversationProtocolInfo.groupId).first()?.protocolInfo) } @Test @@ -787,7 +811,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAConversation_whenChangingTheName_itReturnsTheUpdatedName() = runTest { + fun givenAConversation_whenChangingTheName_itReturnsTheUpdatedName() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) insertTeamUserAndMember(team, user1, conversationEntity3.id) @@ -821,7 +845,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenAConversation_whenUpdatingReceiptMode_itReturnsTheUpdatedValue() = runTest { + fun givenAConversation_whenUpdatingReceiptMode_itReturnsTheUpdatedValue() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity1.copy(receiptMode = ConversationEntity.ReceiptMode.ENABLED)) @@ -834,7 +858,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenSelfUserIsNotMemberOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest { + fun givenSelfUserIsNotMemberOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) teamDAO.insertTeam(team) @@ -849,7 +873,7 @@ class ConversationDAOTest : BaseDatabaseTest() { } @Test - fun givenConversation_whenUpdatingMessageTimer_itReturnsCorrectTimer() = runTest { + fun givenConversation_whenUpdatingMessageTimer_itReturnsCorrectTimer() = runTest(dispatcher) { // given conversationDAO.insertConversation(conversationEntity3) val messageTimer = 60000L @@ -862,21 +886,6 @@ class ConversationDAOTest : BaseDatabaseTest() { assertEquals(messageTimer, result?.messageTimer) } - @Test - fun givenSelfUserIsCreatorOfConversation_whenGettingConversationDetails_itReturnsCorrectDetails() = runTest { - // given - conversationDAO.insertConversation(conversationEntity3.copy(creatorId = selfUserId.value)) - teamDAO.insertTeam(team) - userDAO.upsertUser(user2) - insertTeamUserAndMember(team, user2, conversationEntity3.id) - - // when - val result = conversationDAO.getConversationDetailsById(conversationEntity3.id) - - // then - assertEquals(1L, result?.isCreator) - } - @Test fun givenMixedConversation_whenGettingConversationProtocolInfo_itReturnsCorrectInfo() = runTest { // given @@ -977,13 +986,20 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversation) connectionDAO.insertConnection(connectionEntity) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(1, it.size) val result = it.first() assertEquals(conversationId, result.id) assertEquals(ConversationEntity.Type.CONNECTION_PENDING, result.type) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(1, it.size) + val result = it.first() + + assertEquals(conversationId, result.conversationViewEntity.id) + assertEquals(ConversationEntity.Type.CONNECTION_PENDING, result.conversationViewEntity.type) + } } @Test @@ -1005,7 +1021,10 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversation) connectionDAO.insertConnection(connectionEntity) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { + assertEquals(1, it.size) + } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { assertEquals(1, it.size) } } @@ -1022,10 +1041,14 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity1.id) - conversationDAO.getAllConversationDetails(fromArchive).first().let { + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(1, it.size) + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } } @Test @@ -1041,9 +1064,12 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - val result = conversationDAO.getAllConversationDetails(fromArchive).first() - - assertEquals(2, result.size) + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { + assertEquals(2, it.size) + } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(2, it.size) + } } @Test @@ -1059,9 +1085,12 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMember(member1, conversationEntity1.id) memberDAO.insertMember(member2, conversationEntity2.id) - val result = conversationDAO.getAllConversationDetails(fromArchive).first() - - assertEquals(2, result.size) + conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first().let { + assertEquals(2, it.size) + } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first().let { + assertEquals(2, it.size) + } } @Test @@ -1073,10 +1102,13 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then - assertEquals(conversation.toViewEntity(user1), result.firstOrNull { it.id == conversation.id }) + val expected = conversation.toViewEntity(user1).copy(accentId = 0) + assertEquals(expected, result.firstOrNull { it.id == conversation.id }) + assertEquals(expected, resultWithEvents.firstOrNull { it.conversationViewEntity.id == conversation.id }?.conversationViewEntity) } @Test @@ -1088,10 +1120,12 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then - assertNull(result.firstOrNull { it.id == conversation.id }) + assertEquals(null, result.firstOrNull { it.id == conversation.id }) + assertEquals(null, resultWithEvents.firstOrNull { it.conversationViewEntity.id == conversation.id }) } @Test @@ -1119,11 +1153,14 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation2.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then assertEquals(conversation2.id, result[0].id) + assertEquals(conversation2.id, resultWithEvents[0].conversationViewEntity.id) assertEquals(conversation1.id, result[1].id) + assertEquals(conversation1.id, resultWithEvents[1].conversationViewEntity.id) } @Test @@ -1153,12 +1190,16 @@ class ConversationDAOTest : BaseDatabaseTest() { insertTeamUserAndMember(team, user1, conversation2.id) // when - val result = conversationDAO.getAllConversationDetails(fromArchive).first() + val result = conversationDAO.getAllConversationDetails(fromArchive, ConversationFilterEntity.ALL).first() + val resultWithEvents = conversationDAO.getAllConversationDetailsWithEvents(fromArchive).first() // then assertTrue(result.size == 1) + assertTrue(resultWithEvents.size == 1) assertTrue(!result[0].archived) + assertTrue(!resultWithEvents[0].conversationViewEntity.archived) assertEquals(conversation2.id, result[0].id) + assertEquals(conversation2.id, resultWithEvents[0].conversationViewEntity.id) } @Test @@ -1225,10 +1266,319 @@ class ConversationDAOTest : BaseDatabaseTest() { memberDAO.insertMembersWithQualifiedId(listOf(member1, member2), conversationEntity1.id) memberDAO.insertMembersWithQualifiedId(listOf(member1, member2), conversationEntity2.id) - conversationDAO.getAllConversationDetails(fromArchive = false).first().let { + conversationDAO.getAllConversationDetails( + fromArchive = false, + filter = ConversationFilterEntity.ALL + ).first().let { assertEquals(1, it.size) assertEquals(conversationEntity1.id, it.first().id) } + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(1, it.size) + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } + } + + @Test + fun givenConversationWithUnreadMessageAndDraft_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy(lastReadDate = Instant.fromEpochMilliseconds(0)) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity.id, + senderUserId = user1.id, + date = conversationEntity.lastReadDate.plus(1.seconds) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + + val draftMessageEntity = newDraftMessageEntity(conversationId = conversationEntity.id) + messageDraftDAO.upsertMessageDraft(draftMessageEntity) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(1, it.first().unreadEvents.unreadEvents[UnreadEventTypeEntity.MESSAGE]) + assertEquals(draftMessageEntity.text, it.first().messageDraft?.text) + assertEquals(messageEntity.id, it.first().lastMessage?.id) + } + } + + @Test + fun givenConversationWithoutEvents_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy(lastReadDate = Instant.fromEpochMilliseconds(0)) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = false).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(0, it.first().unreadEvents.unreadEvents.size) + assertEquals(null, it.first().messageDraft) + assertEquals(null, it.first().lastMessage) + } + } + + @Test + fun givenArchiveConversationWithUnreadMessageAndDraft_whenGettingAllConversationsWithEvents_thenShouldReturnCorrectValues() = runTest { + val conversationEntity = conversationEntity1.copy( + archived = true, + lastReadDate = Instant.fromEpochMilliseconds(0L) + ) + conversationDAO.insertConversation(conversationEntity) + + userDAO.upsertUser(user1.copy(activeOneOnOneConversationId = conversationEntity.id)) + + memberDAO.insertMember(member1, conversationEntity.id) + memberDAO.insertMember(member2, conversationEntity.id) + + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity.id, + senderUserId = user1.id, + date = conversationEntity.lastReadDate.plus(1.seconds) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + + val draftMessageEntity = newDraftMessageEntity(conversationId = conversationEntity.id) + messageDraftDAO.upsertMessageDraft(draftMessageEntity) + + conversationDAO.getAllConversationDetailsWithEvents(fromArchive = true).first().let { + assertEquals(conversationEntity.id, it.first().conversationViewEntity.id) + assertEquals(1, it.first().unreadEvents.unreadEvents[UnreadEventTypeEntity.MESSAGE]) + assertEquals(null, it.first().messageDraft) // do not return draft for archived conversation + assertEquals(null, it.first().lastMessage) // do not return last message for archived conversation + } + } + + @Test + fun givenConversationWithStillOngoingCall_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, + lastModifiedDate = Instant.fromEpochMilliseconds(2) // conversation 1 is more recent than conversation 2 + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, + lastModifiedDate = Instant.fromEpochMilliseconds(1) // conversation 2 is less recent than conversation 1 + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val callEntity = CallEntity( + conversationId = conversationEntity1.id, + id = "call_id", + status = CallEntity.Status.STILL_ONGOING, + callerId = "callerId", + conversationType = ConversationEntity.Type.GROUP, + type = CallEntity.Type.CONFERENCE + ) + callDAO.insertCall(callEntity.copy(conversationId = conversationEntity2.id)) // but conversation 2 has ongoing call + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with ongoing call + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEvents_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(2) // but it's also already read + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(0), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0) // but it's still unread + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with unread event + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEventsButMuted_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = + runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(0), + mutedStatus = ConversationEntity.MutedStatus.ONLY_MENTIONS_AND_REPLIES_ALLOWED, // but new messages are muted + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(1), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0), + mutedStatus = ConversationEntity.MutedStatus.ALL_ALLOWED, // but new messages are not muted + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity1 = newRegularMessageEntity( + id = "unread_message_id_1", + conversationId = conversationEntity1.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + val messageEntity2 = newRegularMessageEntity( + id = "unread_message_id_2", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity1) + messageDAO.insertOrIgnoreMessage(messageEntity2) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with not muted new message + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConversationWithUnreadEvents_whenGettingAllConversationsWithEventsAndNotNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + lastReadDate = Instant.fromEpochMilliseconds(2) // but it's also already read + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(0), // conversation 2 is less recent than conversation 1 + lastReadDate = Instant.fromEpochMilliseconds(0) // but it's still unread + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + val messageEntity = newRegularMessageEntity( + id = "unread_message_id", + conversationId = conversationEntity2.id, + senderUserId = user1.id, + date = Instant.fromEpochMilliseconds(1) // message received after last read date so it should be unread + ) + messageDAO.insertOrIgnoreMessage(messageEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = false).first().let { + assertEquals(conversationEntity1.id, it[0].conversationViewEntity.id) // first is the more recent one even if it's already read + assertEquals(conversationEntity2.id, it[1].conversationViewEntity.id) // second is the other one even if it has unread events + } + } + + @Test + fun givenReceivedConnectionRequest_whenGettingAllConversationsWithEventsAndNewActivitiesOnTop_thenReturnRightOrder() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + ) + val conversationEntity2 = conversationEntity1.copy( + id = ConversationIDEntity("conversation2", "domain"), + type = ConversationEntity.Type.CONNECTION_PENDING, + lastModifiedDate = Instant.fromEpochMilliseconds(1), // conversation 1 is more recent than conversation 2 + ) + val userEntity = user1.copy(connectionStatus = ConnectionEntity.State.PENDING) + val connectionEntity = ConnectionEntity( + conversationId = conversationEntity2.id.value, + from = userEntity.id.value, + lastUpdateDate = Instant.fromEpochMilliseconds(1), + qualifiedConversationId = conversationEntity2.id, + qualifiedToId = userEntity.id, + status = ConnectionEntity.State.PENDING, + toId = userEntity.id.value, + ) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + connectionDAO.insertConnection(connectionEntity) + userDAO.upsertUser(userEntity) + conversationDAO.getAllConversationDetailsWithEvents(newActivitiesOnTop = true).first().let { + assertEquals(conversationEntity2.id, it[0].conversationViewEntity.id) // first is the one with received connection request + assertEquals(conversationEntity1.id, it[1].conversationViewEntity.id) // second is the other one even if it is more recent + } + } + + @Test + fun givenConnectionRequest_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.CONNECTION_PENDING, + lastModifiedDate = Instant.fromEpochMilliseconds(2), // conversation 1 is more recent than conversation 2 + ) + val userEntity = user1.copy(connectionStatus = ConnectionEntity.State.PENDING) + val connectionEntity = ConnectionEntity( + conversationId = conversationEntity1.id.value, + from = userEntity.id.value, + lastUpdateDate = Instant.fromEpochMilliseconds(1), + qualifiedConversationId = conversationEntity1.id, + qualifiedToId = userEntity.id, + status = ConnectionEntity.State.PENDING, + toId = userEntity.id.value, + ) + conversationDAO.insertConversation(conversationEntity1) + connectionDAO.insertConnection(connectionEntity) + userDAO.upsertUser(userEntity) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(0, it.size) + } + } + + @Test + fun givenAGroupConvWhichSelfUserLeft_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.GROUP, + ) + val conversationEntity2 = conversationEntity1.copy(id = ConversationIDEntity("conversation2", "domain")) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1) + memberDAO.insertMember(MemberEntity(selfUserId, MemberEntity.Role.Member), conversationEntity1.id) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(1, it.size) // self user is a member of only conversation1 + assertEquals(conversationEntity1.id, it.first().conversationViewEntity.id) + } + } + + @Test + fun givenAOneOneConvWithDeletedUser_whenGettingAllConversationsWithEventsAndOnlyEnabledInteractions_thenDoNotReturnIt() = runTest { + val conversationEntity1 = conversationEntity1.copy( + id = ConversationIDEntity("conversation1", "domain"), + type = ConversationEntity.Type.ONE_ON_ONE, + ) + val conversationEntity2 = conversationEntity1.copy(id = ConversationIDEntity("conversation2", "domain")) + conversationDAO.insertConversation(conversationEntity1) + conversationDAO.insertConversation(conversationEntity2) + userDAO.upsertUser(user1.copy(deleted = true)) + memberDAO.insertMember(MemberEntity(selfUserId, MemberEntity.Role.Member), conversationEntity1.id) + memberDAO.insertMember(MemberEntity(user1.id, MemberEntity.Role.Member), conversationEntity1.id) + conversationDAO.getAllConversationDetailsWithEvents(onlyInteractionEnabled = true).first().let { + assertEquals(0, it.size) + } } @Test @@ -1881,7 +2231,7 @@ class ConversationDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversationEntity4) // when - val result = conversationDAO.getConversationByGroupID("call_subconversation_groupid") + val result = conversationDAO.getConversationDetailsByGroupID("call_subconversation_groupid") // then assertEquals( @@ -1993,7 +2343,6 @@ class ConversationDAOTest : BaseDatabaseTest() { userDeleted = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.deleted else null, connectionStatus = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.connectionStatus else null, otherUserId = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.id else null, - isCreator = 0L, lastNotificationDate = lastNotificationDate, protocolInfo = protocolInfo, accessList = access, @@ -2018,7 +2367,9 @@ class ConversationDAOTest : BaseDatabaseTest() { proteusVerificationStatus = ConversationEntity.VerificationStatus.DEGRADED, userSupportedProtocols = if (type == ConversationEntity.Type.ONE_ON_ONE) userEntity?.supportedProtocols else null, userActiveOneOnOneConversationId = null, - legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED + legalHoldStatus = ConversationEntity.LegalHoldStatus.DISABLED, + accentId = 1, + isFavorite = false ) } @@ -2043,6 +2394,21 @@ class ConversationDAOTest : BaseDatabaseTest() { isMLSCapable = false ) + val mlsProtocolInfo1 = ConversationEntity.ProtocolInfo.MLS( + "group2", + ConversationEntity.GroupState.ESTABLISHED, + 0UL, + Instant.parse("2021-03-30T15:36:00.000Z"), + cipherSuite = ConversationEntity.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + val mlsProtocolInfo2 = ConversationEntity.ProtocolInfo.MLS( + "group3", + ConversationEntity.GroupState.PENDING_JOIN, + 0UL, + Instant.parse("2021-03-30T15:36:00.000Z"), + cipherSuite = ConversationEntity.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 + ) + val team = TeamEntity(teamId, "teamName", "") val conversationEntity1 = ConversationEntity( @@ -2072,13 +2438,7 @@ class ConversationDAOTest : BaseDatabaseTest() { "conversation2", ConversationEntity.Type.ONE_ON_ONE, null, - ConversationEntity.ProtocolInfo.MLS( - "group2", - ConversationEntity.GroupState.ESTABLISHED, - 0UL, - Instant.parse("2021-03-30T15:36:00.000Z"), - cipherSuite = ConversationEntity.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - ), + protocolInfo = mlsProtocolInfo1, creatorId = "someValue", lastNotificationDate = null, lastModifiedDate = "2021-03-30T15:36:00.000Z".toInstant(), @@ -2101,13 +2461,7 @@ class ConversationDAOTest : BaseDatabaseTest() { "conversation3", ConversationEntity.Type.GROUP, null, - ConversationEntity.ProtocolInfo.MLS( - "group3", - ConversationEntity.GroupState.PENDING_JOIN, - 0UL, - Instant.parse("2021-03-30T15:36:00.000Z"), - cipherSuite = ConversationEntity.CipherSuite.MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519 - ), + protocolInfo = mlsProtocolInfo2, creatorId = "someValue", // This conversation was modified after the last time the user was notified about it lastNotificationDate = "2021-03-30T15:30:00.000Z".toInstant(), diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt new file mode 100644 index 00000000000..c55255dd733 --- /dev/null +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/conversation/folder/ConversationFolderDAOTest.kt @@ -0,0 +1,223 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.kalium.persistence.dao.conversation.folder + +import com.wire.kalium.persistence.BaseDatabaseTest +import com.wire.kalium.persistence.dao.QualifiedIDEntity +import com.wire.kalium.persistence.dao.UserIDEntity +import com.wire.kalium.persistence.dao.conversation.ConversationEntity +import com.wire.kalium.persistence.dao.member.MemberEntity +import com.wire.kalium.persistence.db.UserDatabaseBuilder +import com.wire.kalium.persistence.utils.stubs.newConversationEntity +import com.wire.kalium.persistence.utils.stubs.newUserEntity +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ConversationFolderDAOTest : BaseDatabaseTest() { + + private val conversationEntity1 = newConversationEntity("Test1").copy(type = ConversationEntity.Type.GROUP) + private val userEntity1 = newUserEntity("userEntity1") + val member1 = MemberEntity(userEntity1.id, MemberEntity.Role.Admin) + + lateinit var db: UserDatabaseBuilder + private val selfUserId = UserIDEntity("selfValue", "selfDomain") + + @BeforeTest + fun setUp() { + deleteDatabase(selfUserId) + db = createDatabase(selfUserId, encryptedDBSecret, true) + } + + @Test + fun givenFolderWithConversationId_WhenObservingThenConversationShouldBeReturned() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + + val conversationFolderEntity = folderWithConversationsEntity( + id = folderId, + name = "folderName", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id)) + + db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) + val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first().first() + + assertEquals(conversationEntity1.id, result.conversationViewEntity.id) + } + + @Test + fun givenFavoriteFolderWithConversationId_WhenObservingThenFavoriteConversationShouldBeReturned() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + + val conversationFolderEntity = folderWithConversationsEntity( + id = folderId, + name = "", + type = ConversationFolderTypeEntity.FAVORITE, + conversationIdList = listOf(conversationEntity1.id)) + + db.conversationFolderDAO.updateConversationFolders(listOf(conversationFolderEntity)) + val result = db.conversationFolderDAO.getFavoriteConversationFolder() + + assertEquals(folderId, result.id) + } + + @Test + fun givenMultipleFolders_whenRetrievingFolders_shouldReturnCorrectData() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folder1 = folderWithConversationsEntity( + id = "folderId1", + name = "Folder 1", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + val folder2 = folderWithConversationsEntity( + id = "folderId2", + name = "Folder 2", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf() + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder1, folder2)) + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertEquals(2, result.size) + assertTrue(result.any { it.id == "folderId1" && it.name == "Folder 1" }) + assertTrue(result.any { it.id == "folderId2" && it.name == "Folder 2" }) + } + + @Test + fun givenFolderWithConversation_whenRemovingConversation_thenFolderShouldBeEmpty() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folderId = "folderId1" + val folder = folderWithConversationsEntity( + id = folderId, + name = "Test Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + db.conversationFolderDAO.removeConversationFromFolder(conversationEntity1.id, folderId) + + val result = db.conversationFolderDAO.observeConversationListFromFolder(folderId).first() + + assertTrue(result.isEmpty()) + } + + @Test + fun givenFolderWithConversations_whenDeletingFolder_thenFolderShouldBeRemoved() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val folder = folderWithConversationsEntity( + id = "folderId1", + name = "Folder 1", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + db.conversationFolderDAO.updateConversationFolders(listOf()) // Clear folders + + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertTrue(result.isEmpty()) + } + + @Test + fun givenEmptyFolder_whenAddingToDatabase_thenShouldBeRetrievable() = runTest { + val folder = folderWithConversationsEntity( + id = "folderId1", + name = "Empty Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf() + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(folder)) + + val result = db.conversationFolderDAO.getFoldersWithConversations() + + assertEquals(1, result.size) + assertEquals("folderId1", result.first().id) + assertTrue(result.first().conversationIdList.isEmpty()) + } + + @Test + fun givenConversationAddedToUserAndFavoriteFolders_whenRetrievingFolders_thenShouldBeInBothFolders() = runTest { + db.conversationDAO.insertConversation(conversationEntity1) + db.userDAO.upsertUser(userEntity1) + db.memberDAO.insertMember(member1, conversationEntity1.id) + + val userFolder = folderWithConversationsEntity( + id = "userFolderId", + name = "User Folder", + type = ConversationFolderTypeEntity.USER, + conversationIdList = listOf(conversationEntity1.id) + ) + + val favoriteFolder = folderWithConversationsEntity( + id = "favoriteFolderId", + name = "Favorites", + type = ConversationFolderTypeEntity.FAVORITE, + conversationIdList = listOf(conversationEntity1.id) + ) + + db.conversationFolderDAO.updateConversationFolders(listOf(userFolder, favoriteFolder)) + + val userFolderResult = db.conversationFolderDAO.observeConversationListFromFolder("userFolderId").first() + assertEquals(1, userFolderResult.size) + assertEquals(conversationEntity1.id, userFolderResult.first().conversationViewEntity.id) + + val favoriteFolderResult = db.conversationFolderDAO.observeConversationListFromFolder("favoriteFolderId").first() + assertEquals(1, favoriteFolderResult.size) + assertEquals(conversationEntity1.id, favoriteFolderResult.first().conversationViewEntity.id) + } + + companion object { + fun folderWithConversationsEntity( + id: String = "folderId", + type: ConversationFolderTypeEntity = ConversationFolderTypeEntity.FAVORITE, + name: String = "", + conversationIdList: List = listOf(QualifiedIDEntity("conversationId", "domain")) + ) = FolderWithConversationsEntity( + id = id, + type = type, + name = name, + conversationIdList = conversationIdList + ) + } +} diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt index ae89c137b02..da4724ba739 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageDAOTest.kt @@ -979,7 +979,7 @@ class MessageDAOTest : BaseDatabaseTest() { conversationId = conversationId, senderUserId = userEntity1.id, senderClientId = "someClient", - content = MessageEntityContent.FailedDecryption(null, false, userEntity1.id, "someClient") + content = MessageEntityContent.FailedDecryption(null, 333, false, userEntity1.id, "someClient") ), newRegularMessageEntity( id = messageId2, @@ -987,7 +987,7 @@ class MessageDAOTest : BaseDatabaseTest() { conversationId = conversationId2, senderUserId = userEntity1.id, senderClientId = "someClient", - content = MessageEntityContent.FailedDecryption(null, false, userEntity1.id, "someClient") + content = MessageEntityContent.FailedDecryption(null, 333, false, userEntity1.id, "someClient") ) ) ) @@ -1000,6 +1000,7 @@ class MessageDAOTest : BaseDatabaseTest() { assertTrue((updatedMessage?.content as MessageEntityContent.FailedDecryption).isDecryptionResolved) val updatedMessage2 = messageDAO.getMessageById(messageId2, conversationId2) assertTrue((updatedMessage2?.content as MessageEntityContent.FailedDecryption).isDecryptionResolved) + assertEquals(333, (updatedMessage2.content as MessageEntityContent.FailedDecryption).code) } @Test @@ -1945,15 +1946,6 @@ class MessageDAOTest : BaseDatabaseTest() { fun givenMessagesAreInserted_whenGettingLastMessagesByConversations_thenOnlyLastMessagesForEachConversationAreReturned() = runTest { // given insertInitialData() - fun createMessage(id: String, conversationId: QualifiedIDEntity, date: Instant) = newRegularMessageEntity( - id = id, - conversationId = conversationId, - date = date, - senderUserId = userEntity1.id, - senderName = userEntity1.name!!, - sender = userDetailsEntity1 - ) - val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") val messages = listOf( createMessage(id = "1A", conversationId = conversationEntity1.id, date = baseInstant), @@ -1971,6 +1963,124 @@ class MessageDAOTest : BaseDatabaseTest() { assertEquals(null, result[conversationEntity3.id]) } + + @Test + fun givenNewMessageIsInserted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessages(listOf(newMessage)) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(newMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastMessageIsDeleted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val olderMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val lastMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(olderMessage, lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.deleteMessage(lastMessage.id, conversationEntity1.id) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(olderMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastMessageIsMovedToAnotherConversation_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessageConversation1 = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val lastMessageConversation2 = createMessage(id = "2", conversationId = conversationEntity2.id, date = baseInstant + 1.seconds) + messageDAO.insertOrIgnoreMessages(listOf(lastMessageConversation1, lastMessageConversation2)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id, conversationEntity2.id)) + messageDAO.moveMessages(conversationEntity2.id, conversationEntity1.id) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id, conversationEntity2.id)) + // then + assertEquals(lastMessageConversation1.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(lastMessageConversation2.id, resultBefore[conversationEntity2.id]?.id) + + assertEquals(lastMessageConversation2.id, resultAfter[conversationEntity1.id]?.id) + assertEquals(null, resultAfter[conversationEntity2.id]?.id) // conversation 2 should be empty - all messages are moved to 1 + } + + @Test + fun givenLastMessageIsEdited_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val lastMessageTextContent = MessageEntityContent.Text("message") + val lastMessage = createMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds) + .copy(content = lastMessageTextContent) + val editedLastMessageId = lastMessage.id + "_edit" + messageDAO.insertOrIgnoreMessages(listOf(lastMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.updateTextMessageContent( + editInstant = baseInstant + 2.seconds, + conversationId = conversationEntity1.id, + currentMessageId = lastMessage.id, + newTextContent = lastMessageTextContent.copy(messageBody = "edited"), + newMessageId = editedLastMessageId, + ) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(lastMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(editedLastMessageId, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenNewAssetMessageWithIncompleteDataIsInserted_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val currentLastVisibleMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newerAssetMessageIncompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = false) + messageDAO.insertOrIgnoreMessages(listOf(currentLastVisibleMessage)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessage(newerAssetMessageIncompleteData) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(currentLastVisibleMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(currentLastVisibleMessage.id, resultAfter[conversationEntity1.id]?.id) + } + + @Test + fun givenLastAssetMessageRemoteDataUpdated_whenGettingLastMessagesByConversations_thenReturnProperLastMessages() = runTest { + // given + insertInitialData() + val baseInstant = Instant.parse("2022-01-01T00:00:00.000Z") + val currentLastVisibleMessage = createMessage(id = "1", conversationId = conversationEntity1.id, date = baseInstant) + val newerAssetMessageIncompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = false) + val newerAssetMessageCompleteData = + createImageAssetMessage(id = "2", conversationId = conversationEntity1.id, date = baseInstant + 1.seconds, isComplete = true) + messageDAO.insertOrIgnoreMessages(listOf(currentLastVisibleMessage, newerAssetMessageIncompleteData)) + // when + val resultBefore = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + messageDAO.insertOrIgnoreMessage(newerAssetMessageCompleteData) + val resultAfter = messageDAO.getLastMessagesByConversations(listOf(conversationEntity1.id)) + // then + assertEquals(currentLastVisibleMessage.id, resultBefore[conversationEntity1.id]?.id) + assertEquals(newerAssetMessageCompleteData.id, resultAfter[conversationEntity1.id]?.id) + } + @Test fun givenUnverifiedWarningMessageIsInserted_whenInsertingSuchMessageAgain_thenOnlyIdIsUpdatedNoNewMessages() = runTest { // given @@ -2031,8 +2141,17 @@ class MessageDAOTest : BaseDatabaseTest() { status = MessageEntity.Status.SENT, senderName = userEntity1.name!!, ) - val expectedMessages = listOf(alreadyEndedEphemeralMessage) - val allMessages = expectedMessages + listOf(pendingEphemeralMessage, nonEphemeralMessage) + val notYetStartedEphemeralMessage = newRegularMessageEntity( + "4", + conversationId = conversationEntity1.id, + senderUserId = userEntity1.id, + status = MessageEntity.Status.SENT, + senderName = userEntity1.name!!, + selfDeletionEndDate = null, + expireAfterMs = 1.seconds.inWholeSeconds + ) + val expectedMessages = listOf(pendingEphemeralMessage) + val allMessages = expectedMessages + listOf(alreadyEndedEphemeralMessage, nonEphemeralMessage, notYetStartedEphemeralMessage) messageDAO.insertOrIgnoreMessages(allMessages) @@ -2235,4 +2354,39 @@ class MessageDAOTest : BaseDatabaseTest() { conversationDAO.insertConversation(conversationEntity2) conversationDAO.insertConversation(conversationEntity3) } + + private fun createMessage(id: String, conversationId: QualifiedIDEntity, date: Instant) = newRegularMessageEntity( + id = id, + conversationId = conversationId, + date = date, + senderUserId = userEntity1.id, + senderName = userEntity1.name!!, + sender = userDetailsEntity1, + ) + + private fun createImageAssetMessage(id: String, conversationId: QualifiedIDEntity, date: Instant, isComplete: Boolean) = + newRegularMessageEntity( + id = id, + conversationId = conversationId, + date = date, + senderUserId = userEntity1.id, + senderName = userEntity1.name!!, + sender = userDetailsEntity1, + content = MessageEntityContent.Asset( + assetSizeInBytes = if (isComplete) 100000L else 0L, + assetName = "test name", + assetMimeType = "JPG", + assetId = if (isComplete) "assetId" else "", + assetOtrKey = if (isComplete) byteArrayOf(1) else byteArrayOf(), + assetSha256Key = if (isComplete) byteArrayOf(1) else byteArrayOf(), + assetToken = "", + assetDomain = "domain", + assetEncryptionAlgorithm = "", + assetWidth = if (isComplete) 100 else null, + assetHeight = if (isComplete) 100 else null, + assetDurationMs = null, + assetNormalizedLoudness = null, + ), + visibility = if (isComplete) MessageEntity.Visibility.VISIBLE else MessageEntity.Visibility.HIDDEN + ) } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt index 9609b2152df..998bb343c09 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageMapperTest.kt @@ -160,6 +160,7 @@ class MessageMapperTest { restrictedAssetSize: Long? = null, restrictedAssetName: String? = null, failedToDecryptData: ByteArray? = null, + decryptionErrorCode: Long? = null, isDecryptionResolved: Boolean? = null, conversationName: String? = null, allReactionsJson: String = "{}", @@ -251,6 +252,7 @@ class MessageMapperTest { restrictedAssetSize, restrictedAssetName, failedToDecryptData, + decryptionErrorCode, isDecryptionResolved, conversationName, allReactionsJson, diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageTextEditTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageTextEditTest.kt index 9fa8b7fd8da..e2d5fb9a3e6 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageTextEditTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/MessageTextEditTest.kt @@ -38,7 +38,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) class MessageTextEditTest : BaseMessageTest() { @Test @@ -247,7 +246,7 @@ class MessageTextEditTest : BaseMessageTest() { } @Test - fun givenTextWasInsertedAndIsRead_whenUpdatingContentWithSelfMention_thenUnreadEventShouldNotChange() = runTest { + fun givenTextWasInsertedAndIsNotRead_whenUpdatingContentWithSelfMention_thenUnreadEventShouldNotChange() = runTest { // Given val initMentions = listOf( MessageEntity.Mention(0, 1, SELF_USER_ID), @@ -255,8 +254,7 @@ class MessageTextEditTest : BaseMessageTest() { ) insertInitialDataWithMentions( - mentions = initMentions, - updateConversationReadDate = true + mentions = initMentions ) // When @@ -276,20 +274,19 @@ class MessageTextEditTest : BaseMessageTest() { val unreadEvents = messageDAO.observeUnreadEvents() .first()[CONVERSATION_ID] - assertTrue(unreadEvents.isNullOrEmpty()) + assertNotNull(unreadEvents) + assertEquals(1, unreadEvents.size) } private suspend fun insertInitialDataWithMentions( mentions: List, - updateConversationReadDate: Boolean = false ) { super.insertInitialData() messageDAO.insertOrIgnoreMessage( ORIGINAL_MESSAGE.copy( content = ORIGINAL_CONTENT.copy(mentions = mentions) - ), - updateConversationReadDate = updateConversationReadDate + ) ) } diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOTest.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOTest.kt index c1eedfc9ae8..5484ed6a4de 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOTest.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/dao/message/draft/MessageDraftDAOTest.kt @@ -18,6 +18,7 @@ package com.wire.kalium.persistence.dao.message.draft +import app.cash.turbine.test import com.wire.kalium.persistence.BaseDatabaseTest import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserDAO @@ -32,6 +33,8 @@ import kotlinx.coroutines.test.runTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull @Suppress("LargeClass") class MessageDraftDAOTest : BaseDatabaseTest() { @@ -68,7 +71,7 @@ class MessageDraftDAOTest : BaseDatabaseTest() { } @Test - fun givenAlreadyExistingMessageDraft_whenUpserting_thenItShouldBeProperlyUpdatedInDb() = runTest { + fun givenAlreadyExistingMessageDraft_whenUpsertingTextChange_thenItShouldBeProperlyUpdatedInDb() = runTest { // Given insertInitialData() messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(text = "@John I need")) @@ -81,6 +84,98 @@ class MessageDraftDAOTest : BaseDatabaseTest() { assertEquals(MESSAGE_DRAFT, result) } + @Test + fun givenAlreadyExistingMessageDraft_whenUpsertingDifferentQuotedMessageId_thenItShouldBeProperlyUpdatedInDb() = runTest { + // given + val newQuotedMessageId = "newQuotedMessageId" + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(quotedMessageId = newQuotedMessageId)) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertEquals(newQuotedMessageId, result.quotedMessageId) + } + + @Test + fun givenAlreadyExistingMessageDraft_whenUpsertingNullQuotedMessageId_thenItShouldBeProperlyUpdatedInDb() = runTest { + // given + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(quotedMessageId = null)) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertNull(result.quotedMessageId) + } + + @Test + fun givenAlreadyExistingMessageDraftWithoutQuotedMessageId_whenUpsertingQuotedMessageId_thenItShouldBeProperlyUpdatedInDb() = runTest{ + // given + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(quotedMessageId = null)) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertEquals(MESSAGE_DRAFT.quotedMessageId, result.quotedMessageId) + } + + @Test + fun givenAlreadyExistingMessageDraft_whenUpsertingDifferentEditMessageId_thenItShouldBeProperlyUpdatedInDb() = runTest { + // given + val newEditMessageId = "newEditMessageId" + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(editMessageId = newEditMessageId)) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertEquals(newEditMessageId, result.editMessageId) + } + + @Test + fun givenAlreadyExistingMessageDraft_whenUpsertingNullEditMessageId_thenItShouldBeProperlyUpdatedInDb() = runTest { + // given + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(editMessageId = null)) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertNull(result.editMessageId) + } + + @Test + fun givenAlreadyExistingMessageDraft_whenUpsertingEmptyMentionList_thenItShouldBeProperlyUpdatedInDb() = runTest { + // given + insertInitialData() + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // when + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT.copy(selectedMentionList = emptyList())) + + // then + val result = messageDraftDAO.getMessageDraft(conversationEntity1.id) + assertNotNull(result) + assertEquals(emptyList(), result.selectedMentionList) + } + @Test fun givenAlreadyExistingMessageDraft_whenDeletingIt_thenItShouldBeProperlyRemovedInDb() = runTest { // Given @@ -113,6 +208,89 @@ class MessageDraftDAOTest : BaseDatabaseTest() { assertEquals(null, removedResult) } + @Test + fun givenMessageIsRemoved_whenUpsertingDraft_thenItShouldIgnore() = runTest { + // Given + insertInitialData() + messageDAO.deleteMessage("editMessageId", conversationEntity1.id) + + // When + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // Then + val result = messageDraftDAO.getMessageDraft(MESSAGE_DRAFT.conversationId) + assertEquals(null, result) + } + + @Test + fun givenConversationIsRemoved_whenUpsertingDraft_thenItShouldIgnore() = runTest { + // Given + insertInitialData() + conversationDAO.deleteConversationByQualifiedID(conversationEntity1.id) + + // When + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // Then + val result = messageDraftDAO.getMessageDraft(MESSAGE_DRAFT.conversationId) + assertEquals(null, result) + } + + @Test + fun givenQuotedMessageIsRemoved_whenUpsertingDraft_thenItShouldIgnore() = runTest { + // Given + insertInitialData() + messageDAO.deleteMessage("quotedMessageId", conversationEntity1.id) + + // When + messageDraftDAO.upsertMessageDraft(MESSAGE_DRAFT) + + // Then + val result = messageDraftDAO.getMessageDraft(MESSAGE_DRAFT.conversationId) + assertEquals(null, result) + } + + @Test + fun givenSavedDraft_whenUpsertingTheSameExactDraft_thenItShouldIgnoreAndNotNotifyOtherQueries() = runTest { + // Given + insertInitialData() + val draft = MESSAGE_DRAFT + messageDraftDAO.upsertMessageDraft(draft) + + messageDraftDAO.observeMessageDrafts().test { + val initialValue = awaitItem() + assertEquals(listOf(draft), initialValue) + + // When + messageDraftDAO.upsertMessageDraft(draft) // the same exact draft is being saved again + + // Then + expectNoEvents() // other query should not be notified + } + } + + @Test + fun givenSavedDraft_whenUpsertingUpdatedDraft_thenItShouldBeSavedAndOtherQueriesShouldBeUpdated() = runTest { + // Given + insertInitialData() + val draft = MESSAGE_DRAFT + val updatedDraft = MESSAGE_DRAFT.copy(text = MESSAGE_DRAFT.text + " :)") + messageDraftDAO.upsertMessageDraft(draft) + + messageDraftDAO.observeMessageDrafts().test { + val initialValue = awaitItem() + assertEquals(listOf(draft), initialValue) + + // When + messageDraftDAO.upsertMessageDraft(updatedDraft) // the same exact draft is being saved again + + // Then + val updatedValue = awaitItem() // other query should be notified + assertEquals(listOf(updatedDraft), updatedValue) + } + + } + private suspend fun insertInitialData() { userDAO.upsertUsers(listOf(userEntity1)) conversationDAO.insertConversation(conversationEntity1) @@ -123,6 +301,13 @@ class MessageDraftDAOTest : BaseDatabaseTest() { senderUserId = userEntity1.id ) ) + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "newEditMessageId", + conversationId = conversationEntity1.id, + senderUserId = userEntity1.id + ) + ) messageDAO.insertOrIgnoreMessage( newRegularMessageEntity( id = "quotedMessageId", @@ -130,6 +315,13 @@ class MessageDraftDAOTest : BaseDatabaseTest() { senderUserId = userEntity1.id ) ) + messageDAO.insertOrIgnoreMessage( + newRegularMessageEntity( + id = "newQuotedMessageId", + conversationId = conversationEntity1.id, + senderUserId = userEntity1.id + ) + ) } companion object { diff --git a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt index 4116e4d6d43..5c2760274a7 100644 --- a/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt +++ b/persistence/src/commonTest/kotlin/com/wire/kalium/persistence/utils/stubs/MessageStubs.kt @@ -23,6 +23,7 @@ import com.wire.kalium.persistence.dao.UserDetailsEntity import com.wire.kalium.persistence.dao.UserIDEntity import com.wire.kalium.persistence.dao.message.MessageEntity import com.wire.kalium.persistence.dao.message.MessageEntityContent +import com.wire.kalium.persistence.dao.message.draft.MessageDraftEntity import kotlinx.datetime.Instant @Suppress("LongParameterList") @@ -84,3 +85,11 @@ fun newSystemMessageEntity( selfDeletionEndDate = null, readCount = 0 ) + +fun newDraftMessageEntity( + conversationId: QualifiedIDEntity = QualifiedIDEntity("convId", "convDomain"), + text: String = "draft text", + editMessageId: String? = null, + quotedMessageId: String? = null, + selectedMentionList: List = emptyList() +) = MessageDraftEntity(conversationId, text, editMessageId, quotedMessageId, selectedMentionList) diff --git a/protobuf-codegen/src/main/proto/messages.proto b/protobuf-codegen/src/main/proto/messages.proto index c205faa4989..efd576ac5b9 100644 --- a/protobuf-codegen/src/main/proto/messages.proto +++ b/protobuf-codegen/src/main/proto/messages.proto @@ -46,6 +46,7 @@ message GenericMessage { ButtonActionConfirmation buttonActionConfirmation = 22; DataTransfer dataTransfer = 23; // client-side synchronization across devices of the same user } + optional UnknownStrategy unknownStrategy = 24 [default = IGNORE]; } message QualifiedUserId { @@ -360,3 +361,9 @@ enum LegalHoldStatus { DISABLED = 1; ENABLED = 2; } + +enum UnknownStrategy { + IGNORE = 0; // Ignore the message completely. + DISCARD_AND_WARN = 1; // Warn the user, but discard the message, as it may not be helpful in the future + WARN_USER_ALLOW_RETRY = 2; // Warn the user. Client has freedom to store it and retry in the future. +} diff --git a/scripts/generate_new_api_version.sh b/scripts/generate_new_api_version.sh new file mode 100755 index 00000000000..5b2a6c2875a --- /dev/null +++ b/scripts/generate_new_api_version.sh @@ -0,0 +1,112 @@ +#!/bin/bash + +# Makes all paths relative to project root so it can be run from anywhere +parent_path=$( + cd "$(dirname "${BASH_SOURCE[0]}")" || exit + pwd -P +) +cd "$parent_path/.." || exit + +if [ "$#" -ne 3 ]; then + echo "Usage: $0 " + echo "Example: $0 5 6 7" + exit 1 +fi + +# Validate that all parameters are integers +if ! [[ "$1" =~ ^[0-9]+$ && "$2" =~ ^[0-9]+$ && "$3" =~ ^[0-9]+$ ]]; then + echo "Error: All parameters must be integers." + exit 1 +fi + +# sometimes we have lower case (i.e. imports) sometimes upper case (i.e. class names) +previousApiVersionLower="v$1" +currentApiVersionLower="v$2" +newApiVersionLower="v$3" + +previousApiVersionUpper="V$1" +currentApiVersionUpper="V$2" +newApiVersionUpper="V$3" + +copy_api_files() { + local source_dir=$1 + local target_dir=$2 + + mkdir -p "$target_dir" + + for file in "$source_dir"/*.kt; do + if [[ -f "$file" ]]; then + content=$(cat "$file") + # package name changed from previous to current version + new_content=$(echo "$content" | sed "s/com\.wire\.kalium\.network\.api\.$currentApiVersionLower\./com.wire.kalium.network.api.$newApiVersionLower./g") + # class name changed from previous to current version + new_content=$(echo "$new_content" | sed "s/\(class .*\)$currentApiVersionUpper/\1$newApiVersionUpper/g") + # imports changed from previous to current version + new_content=$(echo "$new_content" | sed "s/com\.wire\.kalium\.network\.api\.$previousApiVersionLower\./com.wire.kalium.network.api.$currentApiVersionLower./g") + # imports class names changed from previous to current version + new_content=$(echo "$new_content" | sed "s/\(import com\.wire\.kalium\.network\.api\.$currentApiVersionLower\.\)\(.*\)$previousApiVersionUpper/\1\2$currentApiVersionUpper/g") + # class names in extension definition changed from previous to current version + new_content=$(echo "$new_content" | sed "s/\(: \)\(.*\)$previousApiVersionUpper/\1\2$currentApiVersionUpper/g") + # class names in inheritance changed from previous to current version + new_content=$(echo "$new_content" | sed "s/\(: \)\(.*\)$previousApiVersionUpper/\1\2$currentApiVersionUpper/g") + # Make class definitions empty inside {} + new_content=$(echo "$new_content" | perl -0777 -pe "s|({[\W\w]*\})|\2|g") + # Remove all private val or val + new_content=$(echo "$new_content" | sed "s/\(private \)*val //g") + # New file name with newApiVersion + new_filename=$(basename "$file" | sed "s/$currentApiVersionUpper/$newApiVersionUpper/g") + echo "$new_content" >"$target_dir/$new_filename" + echo "Created $new_filename" + fi + done +} + +SOURCE_DIR_UNAUTH="network/src/commonMain/kotlin/com/wire/kalium/network/api/$currentApiVersionLower/unauthenticated" +TARGET_DIR_UNAUTH="network/src/commonMain/kotlin/com/wire/kalium/network/api/$newApiVersionLower/unauthenticated" + +SOURCE_DIR_AUTH="network/src/commonMain/kotlin/com/wire/kalium/network/api/$currentApiVersionLower/authenticated" +TARGET_DIR_AUTH="network/src/commonMain/kotlin/com/wire/kalium/network/api/$newApiVersionLower/authenticated" + +copy_api_files "$SOURCE_DIR_UNAUTH" "$TARGET_DIR_UNAUTH" +copy_api_files "$SOURCE_DIR_AUTH" "$TARGET_DIR_AUTH" + +copy_container_files() { + local source_file="$1" + local target_file="${source_file//$currentApiVersionUpper/$newApiVersionUpper}" + target_file="${target_file//$currentApiVersionLower/$newApiVersionLower}" + if [[ -f "$source_file" ]]; then + mkdir -p "$(dirname "$target_file")" + + # Read the content of the file + content=$(cat "$source_file") + + # Perform replacements on the content + new_content="${content//$currentApiVersionUpper/$newApiVersionUpper}" + new_content="${new_content//$currentApiVersionLower/$newApiVersionLower}" + + # Save the modified content back to the target file + echo "$new_content" >"$target_file" + + echo "Created $target_file" + else + exit 1 + fi +} + +copy_container_files "network/src/commonMain/kotlin/com/wire/kalium/network/api/$currentApiVersionLower/authenticated/networkContainer/AuthenticatedNetworkContainer$currentApiVersionUpper.kt" +copy_container_files "network/src/commonMain/kotlin/com/wire/kalium/network/api/$currentApiVersionLower/unauthenticated/networkContainer/UnauthenticatedNetworkContainer$currentApiVersionUpper.kt" + +echo + +# Add the new API version to DevelopmentApiVersions if it does not already contain it +if ! grep -q "$3" network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt; then + sed -i '' "s/\(val DevelopmentApiVersions = setOf(.*\))/\1, $3)/" network/src/commonMain/kotlin/com/wire/kalium/network/BackendMetaDataUtil.kt + echo "Added $3 to DevelopmentApiVersions in BackendMetaDataUtil.kt" +else + echo "$3 is already in DevelopmentApiVersions in BackendMetaDataUtil.kt" +fi + +echo "!!!!!!!" +echo "You must add the new API version to the list of supported API versions in the AuthenticatedNetworkContainer.create() and UnauthenticatedNetworkContainer.create() methods." +echo "Check the generated files for unused parameters." +echo "!!!!!!!" diff --git a/tango-tests/build.gradle.kts b/tango-tests/build.gradle.kts index fbf70768876..9354918570d 100644 --- a/tango-tests/build.gradle.kts +++ b/tango-tests/build.gradle.kts @@ -32,6 +32,7 @@ kotlin { implementation(project(":logic")) implementation(project(":persistence")) implementation(project(":mocks")) + implementation(project(":cryptography")) implementation(libs.kotlin.test) implementation(libs.settings.kmpTest) diff --git a/tango-tests/src/integrationTest/kotlin/PocIntegrationTest.kt b/tango-tests/src/integrationTest/kotlin/PocIntegrationTest.kt index 442797addb7..270ea80e43e 100644 --- a/tango-tests/src/integrationTest/kotlin/PocIntegrationTest.kt +++ b/tango-tests/src/integrationTest/kotlin/PocIntegrationTest.kt @@ -21,20 +21,38 @@ import action.ClientActions import action.LoginActions import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.server.ServerConfig +import com.wire.kalium.logic.data.event.EventGenerator +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.QualifiedClientID import com.wire.kalium.logic.data.logout.LogoutReason +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.UserSessionScope import com.wire.kalium.logic.feature.auth.AuthenticationScope import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase import com.wire.kalium.logic.featureFlags.KaliumConfigs +import com.wire.kalium.logic.functional.getOrFail +import com.wire.kalium.logic.util.TimeLogger +import com.wire.kalium.mocks.mocks.conversation.ConversationMocks import com.wire.kalium.mocks.requests.ACMERequests import com.wire.kalium.mocks.requests.ClientRequests +import com.wire.kalium.mocks.requests.ConnectionRequests +import com.wire.kalium.mocks.requests.ConversationRequests import com.wire.kalium.mocks.requests.FeatureConfigRequests import com.wire.kalium.mocks.requests.LoginRequests +import com.wire.kalium.mocks.requests.NotificationRequests +import com.wire.kalium.mocks.requests.PreKeyRequests import com.wire.kalium.network.NetworkState +import com.wire.kalium.network.api.authenticated.notification.NotificationResponse import com.wire.kalium.network.api.unbound.configuration.ServerConfigDTO import com.wire.kalium.network.utils.TestRequestHandler import com.wire.kalium.network.utils.TestRequestHandler.Companion.TEST_BACKEND_CONFIG +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest +import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json import org.junit.Ignore import org.junit.Test @@ -68,13 +86,16 @@ class PocIntegrationTest { } } - @Ignore("needs to be checked and fix") @Test fun givenEmailAndPasswordWhenLoggingInThenRegisterClientAndLogout() = runTest { val mockedRequests = mutableListOf().apply { addAll(LoginRequests.loginRequestResponseSuccess) addAll(ClientRequests.clientRequestResponseSuccess) addAll(FeatureConfigRequests.responseSuccess) + addAll(NotificationRequests.notificationsRequestResponseSuccess) + addAll(ConversationRequests.conversationsRequestResponseSuccess) + addAll(ConnectionRequests.connectionRequestResponseSuccess()) + addAll(PreKeyRequests.preKeyRequestResponseSuccess) } val coreLogic = createCoreLogic(mockedRequests) @@ -91,19 +112,92 @@ class PocIntegrationTest { authScope = authScope ) - val userSessionScope = ClientActions.registerClient( + val userSession = ClientActions.registerClient( password = USER_PASSWORD, userId = loginAuthToken.userId, coreLogic = coreLogic ) - val x = userSessionScope.client.fetchSelfClients() - println(x.toString()) + userSession.logout.invoke(LogoutReason.SELF_SOFT_LOGOUT) + } + } - userSessionScope.logout.invoke(LogoutReason.SELF_SOFT_LOGOUT) + @Test + fun givenUserWhenHandlingTextMessagesThenProcessShouldSucceed() = runTest { + val mockedRequests = mutableListOf().apply { + addAll(LoginRequests.loginRequestResponseSuccess) + addAll(ClientRequests.clientRequestResponseSuccess) + addAll(FeatureConfigRequests.responseSuccess) + addAll(NotificationRequests.notificationsRequestResponseSuccess) + addAll(ConversationRequests.conversationsRequestResponseSuccess) + addAll(ConnectionRequests.connectionRequestResponseSuccess()) + addAll(PreKeyRequests.preKeyRequestResponseSuccess) + } + + TestNetworkStateObserver.DEFAULT_TEST_NETWORK_STATE_OBSERVER.updateNetworkState(NetworkState.ConnectedWithInternet) + + launch { + val userSession = initUserSession(createCoreLogic(mockedRequests)) + + val selfUserId = userSession.users.getSelfUser().first().id + val selfClientId = userSession.clientIdProvider().getOrFail { throw IllegalStateException("No self client is registered") } + val targetUserId = UserId(value = selfUserId.value, domain = selfUserId.domain) + + userSession.debug.breakSession(selfUserId, selfClientId) + + userSession.debug.establishSession( + userId = targetUserId, + clientId = selfClientId + ) + val generator = EventGenerator( + selfClient = QualifiedClientID( + clientId = selfClientId, + userId = selfUserId + ), + targetClient = QualifiedClientID( + clientId = selfClientId, + userId = targetUserId + ), + proteusClient = userSession.proteusClientProvider.getOrCreate() + ) + + val events = generator.generateEvents( + limit = 1000, + conversationId = ConversationId(ConversationMocks.conversationId.value, ConversationMocks.conversationId.domain), + ) + + val response = NotificationResponse( + time = Clock.System.now().toString(), + hasMore = false, + notifications = events.toList() + ) + + val encodedData = json.encodeToString(response) + + val logger = TimeLogger("entire process") + logger.start() + userSession.debug.synchronizeExternalData(encodedData) + logger.finish() } } + private suspend fun initUserSession(coreLogic: CoreLogic): UserSessionScope { + val authScope = getAuthScope(coreLogic, TEST_BACKEND_CONFIG.links) + + val loginAuthToken = LoginActions.loginAndAddAuthenticatedUser( + email = USER_EMAIL, + password = USER_PASSWORD, + coreLogic = coreLogic, + authScope = authScope + ) + + return ClientActions.registerClient( + password = USER_PASSWORD, + userId = loginAuthToken.userId, + coreLogic = coreLogic + ) + } + private suspend fun getAuthScope(coreLogic: CoreLogic, backend: ServerConfigDTO.Links): AuthenticationScope { val result = coreLogic.versionedAuthenticationScope( ServerConfig.Links( @@ -128,16 +222,20 @@ class PocIntegrationTest { private val HOME_DIRECTORY: String = System.getProperty("user.home") private val USER_EMAIL = "user@domain.com" private val USER_PASSWORD = "password" + private var json = Json { + prettyPrint = true + } fun createCoreLogic(mockedRequests: List) = CoreLogic( rootPath = "$HOME_DIRECTORY/.kalium/accounts-test", kaliumConfigs = KaliumConfigs( developmentApiEnabled = true, encryptProteusStorage = true, - isMLSSupportEnabled = true, wipeOnDeviceRemoval = true, mockedRequests = mockedRequests, - mockNetworkStateObserver = TestNetworkStateObserver.DEFAULT_TEST_NETWORK_STATE_OBSERVER + mockNetworkStateObserver = TestNetworkStateObserver.DEFAULT_TEST_NETWORK_STATE_OBSERVER, + enableCalling = false, + mockedWebSocket = true ), userAgent = "Wire Integration Tests" ) diff --git a/testservice/Jenkinsfile b/testservice/Jenkinsfile index 660b2c3f763..3b2520fa9b4 100644 --- a/testservice/Jenkinsfile +++ b/testservice/Jenkinsfile @@ -16,7 +16,7 @@ pipeline { } } steps { - withMaven(jdk: 'AdoptiumJDK17', maven: 'M3', mavenLocalRepo: '.repository') { + withMaven(jdk: 'JDK17', maven: 'M3', mavenLocalRepo: '.repository') { sh 'PATH=$HOME/.cargo/bin:$PATH make' sh 'sudo cp $WORKSPACE/native/libs/lib* /usr/lib/' sh './gradlew clean' @@ -25,7 +25,7 @@ pipeline { } stage('Build') { steps { - withMaven(jdk: 'AdoptiumJDK17', maven: 'M3', mavenLocalRepo: '.repository') { + withMaven(jdk: 'JDK17', maven: 'M3', mavenLocalRepo: '.repository') { withCredentials([usernamePassword(credentialsId: 'READ_PACKAGES_GITHUB_TOKEN', passwordVariable: 'GITHUB_TOKEN', usernameVariable: 'GITHUB_USER')]) { sh './gradlew :testservice:shadowJar' } diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/api/v1/ConversationResources.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/api/v1/ConversationResources.kt index ea413f9aa2a..48f95e424f4 100644 --- a/testservice/src/main/kotlin/com/wire/kalium/testservice/api/v1/ConversationResources.kt +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/api/v1/ConversationResources.kt @@ -429,7 +429,8 @@ class ConversationResources(private val instanceService: InstanceService) { mentions, messageTimer, quotedMessageId, - buttons + buttons, + legalHoldStatus ) } } diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/managed/ConversationRepository.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/managed/ConversationRepository.kt index 3e717d1ffe5..af5b3c85721 100644 --- a/testservice/src/main/kotlin/com/wire/kalium/testservice/managed/ConversationRepository.kt +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/managed/ConversationRepository.kt @@ -260,13 +260,21 @@ sealed class ConversationRepository { mentions: List, messageTimer: Int?, quotedMessageId: String?, - buttons: List = listOf() + buttons: List = listOf(), + legalHoldStatus: Int? ): Response = instance.coreLogic.globalScope { return when (val session = session.currentSession()) { is CurrentSessionResult.Success -> { instance.coreLogic.sessionScope(session.accountInfo.userId) { if (text != null) { setMessageTimer(instance, conversationId, messageTimer) + legalHoldStatus.let { + if (legalHoldStatus == 2) { + log.info("Instance ${instance.instanceId}: Approve legal hold request and don't block sending") + approveLegalHoldRequest(instance.password) + conversations.setNotifiedAboutConversationUnderLegalHold(conversationId) + } + } log.info("Instance ${instance.instanceId}: Send text message '$text'") val result = if (buttons.isEmpty()) { val previews = mapLinkPreviews(linkPreviews) diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetMimeTypeUseCase.kt b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt similarity index 51% rename from logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetMimeTypeUseCase.kt rename to util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt index c8a4ab19c4b..3d6507cf8ff 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/asset/ValidateAssetMimeTypeUseCase.kt +++ b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/LenientJsonSerializer.kt @@ -15,22 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.kalium.logic.feature.asset +package com.wire.kalium.util.serialization + +import kotlinx.serialization.json.Json /** - * Returns true if the mime type is allowed and false otherwise. - * @param mimeType the mime type to validate. - * @param allowedExtension the list of allowed extension. + * The json serializer for shared usage. */ -interface ValidateAssetMimeTypeUseCase { - operator fun invoke(mimeType: String, allowedExtension: List): Boolean -} +object LenientJsonSerializer { -internal class ValidateAssetMimeTypeUseCaseImpl : ValidateAssetMimeTypeUseCase { - override operator fun invoke(mimeType: String, allowedExtension: List): Boolean { - val extension = mimeType.split("/").last().lowercase() - return allowedExtension.any { - it.lowercase() == extension - } + val json = Json { + isLenient = true + encodeDefaults = true + ignoreUnknownKeys = true } } diff --git a/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt index 67f1ab54c8f..39c4def02b8 100644 --- a/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt +++ b/util/src/commonMain/kotlin/com.wire.kalium.util/serialization/SerializationUtils.kt @@ -18,11 +18,20 @@ package com.wire.kalium.util.serialization +import kotlinx.serialization.KSerializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.floatOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull // See: https://github.com/Kotlin/kotlinx.serialization/issues/746#issuecomment-737000705 @@ -60,3 +69,29 @@ fun Map<*, *>.toJsonObject(): JsonObject { } return JsonObject(map) } + +private fun JsonElement.toAnyOrNull(): Any? { + return when (this) { + is JsonNull -> null + is JsonPrimitive -> toAnyValue() + is JsonObject -> this.map { it.key to it.value.toAnyOrNull() }.toMap() + is JsonArray -> this.map { it.toAnyOrNull() } + } +} + +private fun JsonPrimitive.toAnyValue(): Any? { + return this.booleanOrNull ?: this.intOrNull ?: this.longOrNull ?: this.floatOrNull ?: this.doubleOrNull ?: this.contentOrNull +} + +object AnyPrimitiveValueSerializer : KSerializer { + private val delegateSerializer = JsonElement.serializer() + override val descriptor = delegateSerializer.descriptor + override fun serialize(encoder: Encoder, value: Any) { + encoder.encodeSerializableValue(delegateSerializer, value.toJsonElement()) + } + + override fun deserialize(decoder: Decoder): Any { + val jsonPrimitive = decoder.decodeSerializableValue(delegateSerializer) + return requireNotNull(jsonPrimitive.toAnyOrNull()) { "value cannot be null" } + } +}