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..bbd0df3dfc3 --- /dev/null +++ b/cli/src/commonMain/kotlin/com/wire/kalium/cli/commands/GenerateEventsCommand.kt @@ -0,0 +1,90 @@ +/* + * 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.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.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 targetUserId = UserId(value = targetUserId, domain = selfUserId.domain) + val targetClientId = ClientId(targetClientId) + + userSession.debug.establishSession( + userId = targetUserId, + clientId = targetClientId + ) + val generator = EventGenerator( + selfUserID = 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/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/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cf097d9cd3..da24c4aacad 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -66,6 +66,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 @@ -102,6 +103,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/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..41f77485974 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/event/EventGenerator.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.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 selfUserID: UserId, targetClient: QualifiedClientID, val proteusClient: ProteusClient) { + + private val protoContentMapper: ProtoContentMapper = MapperProvider.protoContentMapper(selfUserID) + private val sessionId = CryptoSessionId(targetClient.userId.toCrypto(), CryptoClientId(targetClient.clientId.value)) + + fun generateEvents( + limit: Int, + conversationId: ConversationId, + ): Flow { + return flow { + repeat(limit) { count -> + val protobuf = generateProtoContent(generateTextContent(count)) + val message = encryptMessage(protobuf, proteusClient, sessionId, sessionId) + val event = generateNewMessageDTO(selfUserID, 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.value, + recipient = recipient.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(), + 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/feature/debug/DebugScope.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/debug/DebugScope.kt index 7e51df74b7a..d86bd11b89b 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 @@ -92,6 +92,9 @@ class DebugScope internal constructor( internal val dispatcher: KaliumDispatcher = KaliumDispatcherImpl, ) { + val establishSession: EstablishSessionUseCase + get() = EstablishSessionUseCaseImpl(sessionEstablisher) + val breakSession: BreakSessionUseCase get() = BreakSessionUseCaseImpl(proteusClientProvider) 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 + }) + } +}