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 da51187c40a..91d8ce94843 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 @@ -493,6 +493,7 @@ internal class MLSConversationDataSource( allowPartialMemberList: Boolean = false, ): Either = withContext(serialDispatcher) { commitPendingProposals(groupID).flatMap { + kaliumLogger.d("adding $userIdList to MLS group: $groupID") produceAndSendCommitWithRetryAndResult(groupID, retryOnStaleMessage = retryOnStaleMessage) { keyPackageRepository.claimKeyPackages(userIdList, cipherSuite).flatMap { result -> if (result.usersWithoutKeyPackagesAvailable.isNotEmpty() && !allowPartialMemberList) { @@ -619,6 +620,7 @@ internal class MLSConversationDataSource( externalSenders: ByteArray, allowPartialMemberList: Boolean = false, ): Either = withContext(serialDispatcher) { + kaliumLogger.d("establish MLS group: $groupID") mlsClientProvider.getMLSClient().flatMap { mlsClient -> wrapMLSRequest { mlsClient.createConversation( 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 344700b26ee..7ca5d84b94f 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 @@ -34,6 +34,7 @@ import com.wire.kalium.protobuf.encodeToByteArray import com.wire.kalium.protobuf.messages.Asset import com.wire.kalium.protobuf.messages.Button import com.wire.kalium.protobuf.messages.ButtonAction +import com.wire.kalium.protobuf.messages.ButtonActionConfirmation import com.wire.kalium.protobuf.messages.Calling import com.wire.kalium.protobuf.messages.Cleared import com.wire.kalium.protobuf.messages.ClientAction @@ -132,7 +133,7 @@ class ProtoContentMapperImpl( is MessageContent.Composite -> packComposite(readableContent, expectsReadConfirmation, legalHoldStatus) is MessageContent.ButtonAction -> packButtonAction(readableContent) - is MessageContent.ButtonActionConfirmation -> TODO() + is MessageContent.ButtonActionConfirmation -> packButtonActionConfirmation(readableContent) is MessageContent.Location -> packLocation(readableContent, expectsReadConfirmation, legalHoldStatus) } } @@ -165,6 +166,16 @@ class ProtoContentMapperImpl( ) ) + private fun packButtonActionConfirmation( + readableContent: MessageContent.ButtonActionConfirmation + ): GenericMessage.Content.ButtonActionConfirmation = + GenericMessage.Content.ButtonActionConfirmation( + ButtonActionConfirmation( + buttonId = readableContent.buttonId, + referenceMessageId = readableContent.referencedMessageId + ) + ) + private fun packComposite( readableContent: MessageContent.Composite, expectsReadConfirmation: Boolean, 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 5162b084d9b..da135841990 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 @@ -18,6 +18,7 @@ package com.wire.kalium.logic.feature.client +import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.data.client.ClientRepository import com.wire.kalium.logic.data.conversation.ClientId @@ -28,6 +29,7 @@ import com.wire.kalium.logic.feature.featureConfig.SyncFeatureConfigsUseCase import com.wire.kalium.logic.feature.session.UpgradeCurrentSessionUseCase import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.nullableFold +import com.wire.kalium.logic.kaliumLogger /** * This use case is responsible for getting the client. @@ -74,6 +76,7 @@ internal class GetOrRegisterClientUseCaseImpl( when (result) { is RegisterClientResult.E2EICertificateRequired -> { + kaliumLogger.i("Client registration blocked because E2EI certificate required") clientRepository.setClientRegistrationBlockedByE2EI() upgradeCurrentSessionAndPersistClient(result.client.id) } @@ -86,7 +89,9 @@ internal class GetOrRegisterClientUseCaseImpl( } private suspend fun upgradeCurrentSessionAndPersistClient(clientId: ClientId) { + kaliumLogger.i("Upgrade current session for client ${clientId.value.obfuscateId()}") upgradeCurrentSessionUseCase(clientId).flatMap { + kaliumLogger.i("Persist client ${clientId.value.obfuscateId()}") clientRepository.persistClientId(clientId) } } 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 c2873502d28..05f9bb05ac1 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 @@ -56,7 +56,9 @@ import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCa import com.wire.kalium.logic.feature.asset.UpdateAssetMessageDownloadStatusUseCaseImpl import com.wire.kalium.logic.feature.asset.UpdateAssetMessageUploadStatusUseCase import com.wire.kalium.logic.feature.asset.UpdateAssetMessageUploadStatusUseCaseImpl +import com.wire.kalium.logic.feature.message.composite.SendButtonActionConfirmationMessageUseCase 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.ephemeral.DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessageForSelfUserAsSenderUseCaseImpl import com.wire.kalium.logic.feature.message.ephemeral.DeleteEphemeralMessagesAfterEndDateUseCase @@ -353,6 +355,14 @@ class MessageScope internal constructor( val resetSession: ResetSessionUseCase get() = ResetSessionUseCaseImpl(proteusClientProvider, sessionResetSender, messageRepository) + val sendButtonActionConfirmationMessage: SendButtonActionConfirmationMessageUseCase + get() = SendButtonActionConfirmationMessageUseCase( + syncManager = syncManager, + messageSender = messageSender, + selfUserId = selfUserId, + currentClientIdProvider = currentClientIdProvider + ) + val sendButtonActionMessage: SendButtonActionMessageUseCase get() = SendButtonActionMessageUseCase( syncManager = syncManager, @@ -361,6 +371,19 @@ class MessageScope internal constructor( currentClientIdProvider = currentClientIdProvider, messageMetadataRepository = messageMetadataRepository ) + + val sendButtonMessage: SendButtonMessageUseCase + get() = SendButtonMessageUseCase( + persistMessage = persistMessage, + selfUserId = selfUserId, + provideClientId = currentClientIdProvider, + slowSyncRepository = slowSyncRepository, + messageSender = messageSender, + messageSendFailureHandler = messageSendFailureHandler, + userPropertyRepository = userPropertyRepository, + scope = scope + ) + private val deleteEphemeralMessageForSelfUserAsReceiver: DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl get() = DeleteEphemeralMessageForSelfUserAsReceiverUseCaseImpl( messageRepository = messageRepository, diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageUseCase.kt new file mode 100644 index 00000000000..a617cba75a5 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageUseCase.kt @@ -0,0 +1,79 @@ +/* + * 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.composite + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logic.CoreFailure +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.data.user.UserId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.data.message.MessageTarget +import com.wire.kalium.logic.functional.flatMap +import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.sync.SyncManager +import com.wire.kalium.util.DateTimeUtil + +/** + * Use case for sending a button action message. + * @param conversationId The conversation id. + * @param messageId The id of the message that contains the button. + * @param buttonId The id of the button. + * + * the action message is sent only to the message original sender. + */ +class SendButtonActionConfirmationMessageUseCase internal constructor( + private val messageSender: MessageSender, + private val syncManager: SyncManager, + private val currentClientIdProvider: CurrentClientIdProvider, + private val selfUserId: UserId +) { + suspend operator fun invoke( + conversationId: ConversationId, + messageId: String, + buttonId: String, + userIds: List + ): Result = syncManager.waitUntilLiveOrFailure().flatMap { + currentClientIdProvider().flatMap { currentClientId -> + val regularMessage = Message.Signaling( + id = uuid4().toString(), + content = MessageContent.ButtonActionConfirmation( + referencedMessageId = messageId, + buttonId = buttonId + ), + conversationId = conversationId, + date = DateTimeUtil.currentIsoDateTimeString(), + senderUserId = selfUserId, + senderClientId = currentClientId, + status = Message.Status.Pending, + isSelfMessage = true, + expirationData = null + ) + messageSender.sendMessage(regularMessage, messageTarget = MessageTarget.Users(userIds)) + } + }.fold(Result::Failure, { Result.Success }) + + sealed interface Result { + data object Success : Result + data class Failure( + val error: CoreFailure + ) : Result + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageUseCase.kt new file mode 100644 index 00000000000..e635c6cd764 --- /dev/null +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageUseCase.kt @@ -0,0 +1,122 @@ +/* + * 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.composite + +import com.benasher44.uuid.uuid4 +import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.id.QualifiedID +import com.wire.kalium.logic.data.message.Message +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.message.PersistMessageUseCase +import com.wire.kalium.logic.data.message.mention.MessageMention +import com.wire.kalium.logic.data.properties.UserPropertyRepository +import com.wire.kalium.logic.data.sync.SlowSyncRepository +import com.wire.kalium.logic.data.sync.SlowSyncStatus +import com.wire.kalium.logic.feature.message.MessageSendFailureHandler +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.util.DateTimeUtil +import com.wire.kalium.util.KaliumDispatcher +import com.wire.kalium.util.KaliumDispatcherImpl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.first + +@Suppress("LongParameterList") +/** + * @sample samples.logic.MessageUseCases.sendingBasicTextMessage + * @sample samples.logic.MessageUseCases.sendingTextMessageWithMentions + */ +class SendButtonMessageUseCase internal constructor( + private val persistMessage: PersistMessageUseCase, + private val selfUserId: QualifiedID, + private val provideClientId: CurrentClientIdProvider, + private val slowSyncRepository: SlowSyncRepository, + private val messageSender: MessageSender, + private val messageSendFailureHandler: MessageSendFailureHandler, + private val userPropertyRepository: UserPropertyRepository, + private val dispatchers: KaliumDispatcher = KaliumDispatcherImpl, + private val scope: CoroutineScope +) { + + suspend operator fun invoke( + conversationId: ConversationId, + text: String, + mentions: List = emptyList(), + quotedMessageId: String? = null, + buttons: List = listOf() + ): Either = scope.async(dispatchers.io) { + slowSyncRepository.slowSyncStatus.first { + it is SlowSyncStatus.Complete + } + + val generatedMessageUuid = uuid4().toString() + val expectsReadConfirmation = userPropertyRepository.getReadReceiptsStatus() + + provideClientId().flatMap { clientId -> + val textContent = MessageContent.Text( + value = text, + mentions = mentions, + quotedMessageReference = quotedMessageId?.let { quotedMessageId -> + MessageContent.QuoteReference( + quotedMessageId = quotedMessageId, + quotedMessageSha256 = null, + isVerified = true + ) + } + ) + + val transform: (String) -> MessageContent.Composite.Button = { MessageContent.Composite.Button(it, it, false) } + val buttonContent = buttons.map(transform) + val content = MessageContent.Composite(textContent, buttonContent) + + val message = Message.Regular( + id = generatedMessageUuid, + content = content, + expectsReadConfirmation = expectsReadConfirmation, + conversationId = conversationId, + date = DateTimeUtil.currentIsoDateTimeString(), + senderUserId = selfUserId, + senderClientId = clientId, + status = Message.Status.Pending, + editStatus = Message.EditStatus.NotEdited, + // According to proto Ephemeral it is not possible to send a Composite message with timer + expirationData = null, + isSelfMessage = true + ) + persistMessage(message).flatMap { + messageSender.sendMessage(message) + } + }.onFailure { + messageSendFailureHandler.handleFailureAndUpdateMessageStatus( + failure = it, + conversationId = conversationId, + messageId = generatedMessageUuid, + messageType = TYPE + ) + } + }.await() + + companion object { + const val TYPE = "Text" + } +} diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigrator.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigrator.kt index 0577055b6b3..228e7c5ec5e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigrator.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/mlsmigration/MLSMigrator.kt @@ -25,15 +25,16 @@ import com.wire.kalium.logic.data.conversation.Conversation.Protocol import com.wire.kalium.logic.data.conversation.ConversationRepository import com.wire.kalium.logic.data.conversation.MLSConversationRepository import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.id.SelfTeamIdProvider import com.wire.kalium.logic.data.message.SystemMessageInserter import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.data.user.UserRepository -import com.wire.kalium.logic.data.id.SelfTeamIdProvider 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.fold import com.wire.kalium.logic.functional.foldToEitherWhileRight +import com.wire.kalium.logic.functional.right import com.wire.kalium.logic.kaliumLogger import kotlinx.coroutines.flow.first @@ -107,6 +108,7 @@ internal class MLSMigratorImpl( ) } } + kaliumLogger.i("migrating ${conversationId.toLogString()} to mls") establishConversation(conversationId) }.flatMapLeft { kaliumLogger.w("failed to migrate ${conversationId.toLogString()} to mixed: $it") @@ -135,16 +137,10 @@ internal class MLSMigratorImpl( .flatMap { protocolInfo -> when (protocolInfo) { is Conversation.ProtocolInfo.Mixed -> { - mlsConversationRepository.establishMLSGroup(protocolInfo.groupId, emptyList()) - .flatMap { - conversationRepository.getConversationMembers(conversationId).flatMap { members -> - mlsConversationRepository.addMemberToMLSGroup( - protocolInfo.groupId, - members, - protocolInfo.cipherSuite - ) - } - } + conversationRepository.getConversationMembers(conversationId).flatMap { members -> + mlsConversationRepository.establishMLSGroup(protocolInfo.groupId, members) + } + Unit.right() } else -> Either.Right(Unit) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt index e1ad327c057..6bcc986f571 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/SendTextMessageCaseTest.kt @@ -25,6 +25,7 @@ import com.wire.kalium.logic.data.properties.UserPropertyRepository import com.wire.kalium.logic.data.sync.SlowSyncRepository import com.wire.kalium.logic.data.sync.SlowSyncStatus import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.message.MessageContent import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase import com.wire.kalium.logic.data.message.SelfDeletionTimer import com.wire.kalium.logic.framework.TestClient @@ -38,6 +39,7 @@ import io.mockative.any import io.mockative.classOf import io.mockative.configure import io.mockative.given +import io.mockative.matching import io.mockative.mock import io.mockative.once import io.mockative.verify @@ -73,11 +75,14 @@ class SendTextMessageCaseTest { .wasInvoked(once) verify(arrangement.persistMessage) .suspendFunction(arrangement.persistMessage::invoke) - .with(any()) + .with(matching { message -> message.content is MessageContent.Text }) .wasInvoked(once) verify(arrangement.messageSender) .suspendFunction(arrangement.messageSender::sendMessage) - .with(any(), any()) + .with( + matching { message -> message.content is MessageContent.Text }, + any() + ) .wasInvoked(once) verify(arrangement.messageSendFailureHandler) .suspendFunction(arrangement.messageSendFailureHandler::handleFailureAndUpdateMessageStatus) @@ -149,24 +154,28 @@ class SendTextMessageCaseTest { .whenInvokedWith(any(), any()) .thenReturn(Either.Right(Unit)) } + fun withSendMessageFailure() = apply { given(messageSender) .suspendFunction(messageSender::sendMessage) .whenInvokedWith(any(), any()) .thenReturn(Either.Left(NetworkFailure.NoNetworkConnection(null))) } + fun withCurrentClientProviderSuccess(clientId: ClientId = TestClient.CLIENT_ID) = apply { given(currentClientIdProvider) .suspendFunction(currentClientIdProvider::invoke) .whenInvoked() .thenReturn(Either.Right(clientId)) } + fun withPersistMessageSuccess() = apply { given(persistMessage) .suspendFunction(persistMessage::invoke) .whenInvokedWith(any()) .thenReturn(Either.Right(Unit)) } + fun withSlowSyncStatusComplete() = apply { val stateFlow = MutableStateFlow(SlowSyncStatus.Complete).asStateFlow() given(slowSyncRepository) @@ -174,6 +183,7 @@ class SendTextMessageCaseTest { .whenInvoked() .thenReturn(stateFlow) } + fun withToggleReadReceiptsStatus(enabled: Boolean = false) = apply { given(userPropertyRepository) .suspendFunction(userPropertyRepository::getReadReceiptsStatus) @@ -181,7 +191,7 @@ class SendTextMessageCaseTest { .thenReturn(enabled) } - fun withMessageTimer(result: SelfDeletionTimer) = apply { + fun withMessageTimer(result: SelfDeletionTimer) = apply { given(observeSelfDeletionTimerSettingsForConversation) .suspendFunction(observeSelfDeletionTimerSettingsForConversation::invoke) .whenInvokedWith(any()) diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageTest.kt new file mode 100644 index 00000000000..feef43c059a --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonActionConfirmationMessageTest.kt @@ -0,0 +1,100 @@ +/* + * 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.composite + +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.message.MessageTarget +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.util.arrangement.MessageSenderArrangement +import com.wire.kalium.logic.util.arrangement.MessageSenderArrangementImpl +import com.wire.kalium.logic.util.arrangement.SyncManagerArrangement +import com.wire.kalium.logic.util.arrangement.SyncManagerArrangementImpl +import com.wire.kalium.logic.util.arrangement.provider.CurrentClientIdProviderArrangement +import com.wire.kalium.logic.util.arrangement.provider.CurrentClientIdProviderArrangementImpl +import io.mockative.any +import io.mockative.matching +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertIs + +class SendButtonActionConfirmationMessageTest { + + @Test + fun givenMessageSendingSuccess_thenMessageIsSentOnlyToOriginalSenderOfTheButtonAction() = runTest { + val convId = ConversationId("conversation-id", "conversation-domain") + val buttonActionSender = UserId("action-sender-id", "action-sender-domain") + val (arrangement, useCase) = Arrangement() + .arrange { + withWaitUntilLiveOrFailure(Either.Right(Unit)) + withCurrentClientIdSuccess(ClientId("client-id")) + withSendMessageSucceed() + } + + val result = useCase( + conversationId = convId, + messageId = "message-id", + buttonId = "button-id", + userIds = listOf(buttonActionSender) + ) + + assertIs(result) + + verify(arrangement.messageSender) + .suspendFunction(arrangement.messageSender::sendMessage) + .with(any(), matching { + it is MessageTarget.Users && it.userId == listOf(buttonActionSender) + }) + .wasInvoked(exactly = once) + + verify(arrangement.currentClientIdProvider) + .suspendFunction(arrangement.currentClientIdProvider::invoke) + .wasInvoked(exactly = once) + + verify(arrangement.syncManager) + .suspendFunction(arrangement.syncManager::waitUntilLiveOrFailure) + .wasInvoked(exactly = once) + } + + private companion object { + val SELF_USER_ID: UserId = UserId("self-user-id", "self-user-domain") + } + + private class Arrangement : + MessageSenderArrangement by MessageSenderArrangementImpl(), + SyncManagerArrangement by SyncManagerArrangementImpl(), + CurrentClientIdProviderArrangement by CurrentClientIdProviderArrangementImpl() { + + private lateinit var useCase: SendButtonActionConfirmationMessageUseCase + + fun arrange(block: Arrangement.() -> Unit): Pair { + apply(block) + useCase = SendButtonActionConfirmationMessageUseCase( + messageSender = messageSender, + syncManager = syncManager, + currentClientIdProvider = currentClientIdProvider, + selfUserId = SELF_USER_ID, + ) + + return this to useCase + } + } +} diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageCaseTest.kt new file mode 100644 index 00000000000..ee7b8a880f8 --- /dev/null +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/message/composite/SendButtonMessageCaseTest.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.feature.message.composite + +import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.id.CurrentClientIdProvider +import com.wire.kalium.logic.data.message.MessageContent +import com.wire.kalium.logic.data.message.PersistMessageUseCase +import com.wire.kalium.logic.data.properties.UserPropertyRepository +import com.wire.kalium.logic.data.sync.SlowSyncRepository +import com.wire.kalium.logic.data.sync.SlowSyncStatus +import com.wire.kalium.logic.feature.message.MessageSendFailureHandler +import com.wire.kalium.logic.feature.message.MessageSender +import com.wire.kalium.logic.feature.selfDeletingMessages.ObserveSelfDeletionTimerSettingsForConversationUseCase +import com.wire.kalium.logic.framework.TestClient +import com.wire.kalium.logic.framework.TestConversation +import com.wire.kalium.logic.framework.TestUser +import com.wire.kalium.logic.functional.Either +import com.wire.kalium.logic.util.shouldSucceed +import io.mockative.Mock +import io.mockative.any +import io.mockative.classOf +import io.mockative.configure +import io.mockative.given +import io.mockative.matching +import io.mockative.mock +import io.mockative.once +import io.mockative.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.test.runTest +import kotlin.test.Test + +class SendButtonMessageCaseTest { + + @Test + fun givenATextMessageContainsButtons_whenSendingIt_thenShouldBeCompositeAndReturnASuccessResult() = runTest { + // Given + val (arrangement, sendTextMessage) = SendButtonMessageCaseTest.Arrangement(this) + .withToggleReadReceiptsStatus() + .withCurrentClientProviderSuccess() + .withPersistMessageSuccess() + .withSlowSyncStatusComplete() + .withSendMessageSuccess() + .arrange() + val buttons = listOf("OK", "Cancel") + + // When + val result = sendTextMessage.invoke(TestConversation.ID, "some-text", listOf(), null, buttons) + + // Then + result.shouldSucceed() + + verify(arrangement.userPropertyRepository) + .suspendFunction(arrangement.userPropertyRepository::getReadReceiptsStatus) + .wasInvoked(once) + verify(arrangement.persistMessage) + .suspendFunction(arrangement.persistMessage::invoke) + .with(matching { message -> message.content is MessageContent.Composite }) + .wasInvoked(once) + verify(arrangement.messageSender) + .suspendFunction(arrangement.messageSender::sendMessage) + .with( + matching { message -> message.content is MessageContent.Composite }, + any() + ) + .wasInvoked(once) + verify(arrangement.messageSendFailureHandler) + .suspendFunction(arrangement.messageSendFailureHandler::handleFailureAndUpdateMessageStatus) + .with(any(), any(), any(), any(), any()) + .wasNotInvoked() + } + + private class Arrangement(private val coroutineScope: CoroutineScope) { + + @Mock + val persistMessage = mock(classOf()) + + @Mock + val currentClientIdProvider = mock(classOf()) + + @Mock + val slowSyncRepository = mock(classOf()) + + @Mock + val messageSender = mock(classOf()) + + @Mock + val userPropertyRepository = mock(classOf()) + + @Mock + val messageSendFailureHandler = configure(mock(classOf())) { stubsUnitByDefault = true } + + fun withSendMessageSuccess() = apply { + given(messageSender) + .suspendFunction(messageSender::sendMessage) + .whenInvokedWith(any(), any()) + .thenReturn(Either.Right(Unit)) + } + + fun withSendMessageFailure() = apply { + given(messageSender) + .suspendFunction(messageSender::sendMessage) + .whenInvokedWith(any(), any()) + .thenReturn(Either.Left(NetworkFailure.NoNetworkConnection(null))) + } + + fun withCurrentClientProviderSuccess(clientId: ClientId = TestClient.CLIENT_ID) = apply { + given(currentClientIdProvider) + .suspendFunction(currentClientIdProvider::invoke) + .whenInvoked() + .thenReturn(Either.Right(clientId)) + } + + fun withPersistMessageSuccess() = apply { + given(persistMessage) + .suspendFunction(persistMessage::invoke) + .whenInvokedWith(any()) + .thenReturn(Either.Right(Unit)) + } + + fun withSlowSyncStatusComplete() = apply { + val stateFlow = MutableStateFlow(SlowSyncStatus.Complete).asStateFlow() + given(slowSyncRepository) + .getter(slowSyncRepository::slowSyncStatus) + .whenInvoked() + .thenReturn(stateFlow) + } + + fun withToggleReadReceiptsStatus(enabled: Boolean = false) = apply { + given(userPropertyRepository) + .suspendFunction(userPropertyRepository::getReadReceiptsStatus) + .whenInvoked() + .thenReturn(enabled) + } + + fun arrange() = this to SendButtonMessageUseCase( + persistMessage, + TestUser.SELF.id, + currentClientIdProvider, + slowSyncRepository, + messageSender, + messageSendFailureHandler, + userPropertyRepository, + scope = coroutineScope + ) + } +} 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 43d5ac4fd6a..8050b10abd5 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 @@ -19,6 +19,7 @@ package com.wire.kalium.logic.feature.mlsmigration import com.wire.kalium.logic.CoreFailure import com.wire.kalium.logic.NetworkFailure +import com.wire.kalium.logic.StorageFailure import com.wire.kalium.logic.data.call.CallRepository import com.wire.kalium.logic.data.conversation.Conversation import com.wire.kalium.logic.data.conversation.ConversationRepository @@ -34,6 +35,8 @@ import com.wire.kalium.logic.framework.TestConversation import com.wire.kalium.logic.framework.TestTeam import com.wire.kalium.logic.framework.TestUser 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.shouldSucceed @@ -68,7 +71,7 @@ class MLSMigratorTest { .withFetchConversationSucceeding() .withGetConversationProtocolInfoReturning(Arrangement.MIXED_PROTOCOL_INFO) .withEstablishGroupSucceeds(Arrangement.SUCCESSFUL_ADDITION_RESULT) - .withGetConversationMembersReturning(Arrangement.MEMBERS) + .withGetConversationMembersReturning(Arrangement.MEMBERS.right()) .withAddMembersSucceeds() .withoutAnyEstablishedCall() .arrange() @@ -102,7 +105,7 @@ class MLSMigratorTest { .withFetchConversationSucceeding() .withGetConversationProtocolInfoReturning(Arrangement.MIXED_PROTOCOL_INFO) .withEstablishGroupSucceeds(Arrangement.SUCCESSFUL_ADDITION_RESULT) - .withGetConversationMembersReturning(Arrangement.MEMBERS) + .withGetConversationMembersReturning(Arrangement.MEMBERS.right()) .withAddMembersSucceeds() .withEstablishedCall() .arrange() @@ -143,6 +146,7 @@ class MLSMigratorTest { .withUpdateProtocolReturns() .withFetchConversationSucceeding() .withGetConversationProtocolInfoReturning(Arrangement.MIXED_PROTOCOL_INFO) + .withGetConversationMembersReturning(StorageFailure.DataNotFound.left()) .withEstablishGroupFails() .withoutAnyEstablishedCall() .arrange() @@ -244,11 +248,11 @@ class MLSMigratorTest { .thenReturn(Either.Right(protocolInfo)) } - fun withGetConversationMembersReturning(members: List) = apply { + fun withGetConversationMembersReturning(result: Either>) = apply { given(conversationRepository) .suspendFunction(conversationRepository::getConversationMembers) .whenInvokedWith(anything()) - .thenReturn(Either.Right(members)) + .thenReturn(result) } fun withFetchConversationSucceeding() = apply { 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 e18e4016325..573aa554469 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 @@ -21,13 +21,18 @@ package com.wire.kalium.testservice.api.v1 import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.mention.MessageMention +import com.wire.kalium.logic.data.message.receipt.DetailedReceipt import com.wire.kalium.logic.data.message.receipt.ReceiptType import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.testservice.managed.ConversationRepository import com.wire.kalium.testservice.managed.InstanceService import com.wire.kalium.testservice.models.ClearConversationRequest import com.wire.kalium.testservice.models.DeleteMessageRequest +import com.wire.kalium.testservice.models.GetMessageReceiptsRequest import com.wire.kalium.testservice.models.GetMessagesRequest +import com.wire.kalium.testservice.models.NewConversationRequest +import com.wire.kalium.testservice.models.SendButtonActionConfirmationRequest +import com.wire.kalium.testservice.models.SendButtonActionRequest import com.wire.kalium.testservice.models.SendConfirmationReadRequest import com.wire.kalium.testservice.models.SendEphemeralConfirmationDeliveredRequest import com.wire.kalium.testservice.models.SendFileRequest @@ -36,6 +41,7 @@ import com.wire.kalium.testservice.models.SendLocationRequest import com.wire.kalium.testservice.models.SendPingRequest import com.wire.kalium.testservice.models.SendReactionRequest import com.wire.kalium.testservice.models.SendTextRequest +import com.wire.kalium.testservice.models.SendTypingRequest import com.wire.kalium.testservice.models.UpdateTextRequest import io.swagger.v3.oas.annotations.Operation import kotlinx.coroutines.runBlocking @@ -131,6 +137,42 @@ class ConversationResources(private val instanceService: InstanceService) { } } + @POST + @Path("/instance/{id}/getMessageReadReceipts") + @Operation(summary = "Get all read receipts of a specific message") + @Consumes(MediaType.APPLICATION_JSON) + fun getMessageReadReceipts(@PathParam("id") id: String, @Valid request: GetMessageReceiptsRequest): List { + val instance = instanceService.getInstanceOrThrow(id) + with(request) { + return runBlocking { + ConversationRepository.getMessageReceipts( + instance, + ConversationId(conversationId, conversationDomain), + messageId, + ReceiptType.READ + ) + } + } + } + + @POST + @Path("/instance/{id}/getMessageDeliveryReceipts") + @Operation(summary = "Get all delivery receipts of a specific message") + @Consumes(MediaType.APPLICATION_JSON) + fun getMessageDeliveryReceipts(@PathParam("id") id: String, @Valid request: GetMessageReceiptsRequest): List { + val instance = instanceService.getInstanceOrThrow(id) + with(request) { + return runBlocking { + ConversationRepository.getMessageReceipts( + instance, + ConversationId(conversationId, conversationDomain), + messageId, + ReceiptType.DELIVERED + ) + } + } + } + // POST /api/v1/instance/{instanceId}/mute // Mute a conversation. @@ -250,8 +292,8 @@ class ConversationResources(private val instanceService: InstanceService) { ConversationId(conversationId, conversationDomain), data, type, - height, width, + height, messageTimer ) } @@ -295,11 +337,42 @@ class ConversationResources(private val instanceService: InstanceService) { } } - // POST /api/v1/instance/{instanceId}/sendButtonAction - // Send a button action to a poll. + @POST + @Path("/instance/{id}/sendButtonAction") + @Operation(summary = "Send a button action to a poll.") + @Consumes(MediaType.APPLICATION_JSON) + fun sendButtonActionConfirmation(@PathParam("id") id: String, @Valid request: SendButtonActionRequest): Response { + val instance = instanceService.getInstanceOrThrow(id) + return with(request) { + runBlocking { + ConversationRepository.sendButtonAction( + instance, + ConversationId(conversationId, conversationDomain), + referenceMessageId, + buttonId + ) + } + } + } - // POST /api/v1/instance/{instanceId}/sendButtonActionConfirmation - // Send a confirmation to a button action. + @POST + @Path("/instance/{id}/sendButtonActionConfirmation") + @Operation(summary = "Send a confirmation to a button action.") + @Consumes(MediaType.APPLICATION_JSON) + fun sendButtonActionConfirmation(@PathParam("id") id: String, @Valid request: SendButtonActionConfirmationRequest): Response { + val instance = instanceService.getInstanceOrThrow(id) + return with(request) { + runBlocking { + ConversationRepository.sendButtonActionConfirmation( + instance, + ConversationId(conversationId, conversationDomain), + referenceMessageId, + buttonId, + userIds.map { UserId(it, conversationDomain) } + ) + } + } + } @POST @Path("/instance/{id}/sendReaction") @@ -328,7 +401,7 @@ class ConversationResources(private val instanceService: InstanceService) { @Consumes(MediaType.APPLICATION_JSON) fun sendText(@PathParam("id") id: String, @Valid sendTextRequest: SendTextRequest): Response { val instance = instanceService.getInstanceOrThrow(id) - // TODO Implement buttons and link previews here + // TODO Implement link previews here val quotedMessageId = sendTextRequest.quote?.quotedMessageId val mentions = when (sendTextRequest.mentions.size) { 0 -> emptyList() @@ -351,7 +424,8 @@ class ConversationResources(private val instanceService: InstanceService) { text, mentions, messageTimer, - quotedMessageId + quotedMessageId, + buttons ) } } @@ -391,6 +465,44 @@ class ConversationResources(private val instanceService: InstanceService) { } } - // POST /api/v1/instance/{instanceId}/sendTyping - // Send a typing indicator to a conversation. + @POST + @Path("/instance/{id}/sendTyping") + @Operation(summary = "Send a typing indicator to a conversation") + @Consumes(MediaType.APPLICATION_JSON) + fun sendTyping(@PathParam("id") id: String, @Valid request: SendTypingRequest): Response { + val instance = instanceService.getInstanceOrThrow(id) + return with(request) { + runBlocking { + ConversationRepository.sendTyping( + instance, + ConversationId(conversationId, conversationDomain), + status + ) + } + } + } + + @POST + @Path("/instance/{id}/conversation") + @Operation(summary = "Create a new conversation") + @Consumes(MediaType.APPLICATION_JSON) + fun createConversation(@PathParam("id") id: String, @Valid request: NewConversationRequest): Response { + val instance = instanceService.getInstanceOrThrow(id) + return with(request) { + val users = userIds.map { + if (it.contains("@")) { + UserId(it.split("@")[0], it.split("@")[1]) + } else { + UserId(it, "staging.zinfra.io") + } + } + runBlocking { + ConversationRepository.createConversation( + instance, + name, + users + ) + } + } + } } 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 e757f80598a..e03ea2a3d7e 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 @@ -19,6 +19,8 @@ package com.wire.kalium.testservice.managed import com.wire.kalium.logic.StorageFailure +import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.conversation.ConversationOptions import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.message.Message import com.wire.kalium.logic.data.message.mention.MessageMention @@ -28,6 +30,11 @@ import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCas import com.wire.kalium.logic.feature.debug.BrokenState import com.wire.kalium.logic.feature.debug.SendBrokenAssetMessageResult import com.wire.kalium.logic.data.message.SelfDeletionTimer +import com.wire.kalium.logic.data.message.receipt.DetailedReceipt +import com.wire.kalium.logic.data.user.UserId +import com.wire.kalium.logic.feature.conversation.CreateGroupConversationUseCase +import com.wire.kalium.logic.feature.message.composite.SendButtonActionConfirmationMessageUseCase +import com.wire.kalium.logic.feature.message.composite.SendButtonActionMessageUseCase import com.wire.kalium.logic.feature.session.CurrentSessionResult import com.wire.kalium.logic.functional.fold import com.wire.kalium.logic.functional.onFailure @@ -48,6 +55,7 @@ import kotlin.time.toDuration sealed class ConversationRepository { + @Suppress("TooManyFunctions") companion object { private val log = LoggerFactory.getLogger(ConversationRepository::class.java.name) @@ -99,6 +107,40 @@ sealed class ConversationRepository { } } + suspend fun createConversation( + instance: Instance, + name: String, + userIds: List + ): Response = instance.coreLogic.globalScope { + when (val session = session.currentSession()) { + is CurrentSessionResult.Success -> { + instance.coreLogic.sessionScope(session.accountInfo.userId) { + log.info("Instance ${instance.instanceId}: Create conversation \"$name\" with ${ + userIds.joinToString { user -> user.value + "@" + user.domain } + }") + when (val result = conversations.createGroupConversation( + name, + userIds, + ConversationOptions(protocol = ConversationOptions.Protocol.MLS) + )) { + is CreateGroupConversationUseCase.Result.Success -> { + Response.status(Response.Status.OK).build() + } + + else -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Instance ${instance.instanceId}: $result").build() + } + } + } + } + + is CurrentSessionResult.Failure -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Session failure").build() + } + } + } + suspend fun sendConfirmation(instance: Instance, conversationId: ConversationId, type: ReceiptType, messageId: String): Response = instance.coreLogic.globalScope { when (val session = session.currentSession()) { @@ -120,6 +162,67 @@ sealed class ConversationRepository { } } + suspend fun sendButtonAction( + instance: Instance, + conversationId: ConversationId, + referenceMessageId: String, + buttonId: String + ): Response = instance.coreLogic.globalScope { + when (val session = session.currentSession()) { + is CurrentSessionResult.Success -> { + instance.coreLogic.sessionScope(session.accountInfo.userId) { + log.info("Instance ${instance.instanceId}: Send button action for button $buttonId") + when (val result = messages.sendButtonActionMessage(conversationId, referenceMessageId, buttonId)) { + is SendButtonActionMessageUseCase.Result.Failure -> + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(result).build() + + else -> { + Response.status(Response.Status.OK) + .entity(SendTextResponse(instance.instanceId, "", "")).build() + } + } + } + } + + is CurrentSessionResult.Failure -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Session failure").build() + } + } + } + + suspend fun sendButtonActionConfirmation( + instance: Instance, + conversationId: ConversationId, + referenceMessageId: String, + buttonId: String, + userIds: List + ): Response = instance.coreLogic.globalScope { + when (val session = session.currentSession()) { + is CurrentSessionResult.Success -> { + instance.coreLogic.sessionScope(session.accountInfo.userId) { + log.info("Instance ${instance.instanceId}: Send button action confirmation for button $buttonId") + when (val result = messages.sendButtonActionConfirmationMessage( + conversationId, + referenceMessageId, + buttonId, + userIds + )) { + is SendButtonActionConfirmationMessageUseCase.Result.Failure -> Response + .status(Response.Status.INTERNAL_SERVER_ERROR).entity(result).build() + + else -> { + Response.status(Response.Status.OK).entity(SendTextResponse(instance.instanceId, "", "")).build() + } + } + } + } + + is CurrentSessionResult.Failure -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Session failure").build() + } + } + } + suspend fun sendReaction( instance: Instance, conversationId: ConversationId, @@ -152,7 +255,8 @@ sealed class ConversationRepository { text: String?, mentions: List, messageTimer: Int?, - quotedMessageId: String? + quotedMessageId: String?, + buttons: List = listOf() ): Response = instance.coreLogic.globalScope { return when (val session = session.currentSession()) { is CurrentSessionResult.Success -> { @@ -160,9 +264,16 @@ sealed class ConversationRepository { if (text != null) { setMessageTimer(instance, conversationId, messageTimer) log.info("Instance ${instance.instanceId}: Send text message '$text'") - messages.sendTextMessage( - conversationId, text, mentions, quotedMessageId - ).fold({ + val result = if (buttons.isEmpty()) { + messages.sendTextMessage( + conversationId, text, mentions, quotedMessageId + ) + } else { + messages.sendButtonMessage( + conversationId, text, mentions, quotedMessageId, buttons + ) + } + result.fold({ Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(it).build() }, { Response.status(Response.Status.OK) @@ -215,6 +326,30 @@ sealed class ConversationRepository { } } + suspend fun sendTyping( + instance: Instance, + conversationId: ConversationId, + status: String + ): Response = instance.coreLogic.globalScope { + when (val session = session.currentSession()) { + is CurrentSessionResult.Success -> { + instance.coreLogic.sessionScope(session.accountInfo.userId) { + log.info("Instance ${instance.instanceId}: $status typing") + if (status.equals("started")) { + conversations.sendTypingEvent(conversationId, Conversation.TypingIndicatorMode.STARTED) + } else { + conversations.sendTypingEvent(conversationId, Conversation.TypingIndicatorMode.STOPPED) + } + Response.status(Response.Status.OK).build() + } + } + + is CurrentSessionResult.Failure -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Session failure").build() + } + } + } + @Suppress("LongParameterList") suspend fun sendLocation( instance: Instance, @@ -284,6 +419,30 @@ sealed class ConversationRepository { throw WebApplicationException("Instance ${instance.instanceId}: Could not get recent messages") } + suspend fun getMessageReceipts( + instance: Instance, + conversationId: ConversationId, + messageId: String, + type: ReceiptType + ): List { + instance.coreLogic.globalScope { + when (val session = session.currentSession()) { + is CurrentSessionResult.Success -> { + instance.coreLogic.sessionScope(session.accountInfo.userId) { + log.info("Instance ${instance.instanceId}: Get receipts...") + val receipts = messages.observeMessageReceipts(conversationId, messageId, type).first() + return receipts + } + } + + is CurrentSessionResult.Failure -> { + Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("Session failure").build() + } + } + } + throw WebApplicationException("Instance ${instance.instanceId}: Could not get receipts from message") + } + @Suppress("LongParameterList", "LongMethod", "ThrowsCount") suspend fun sendFile( instance: Instance, diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/models/GetMessageReceiptsRequest.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/GetMessageReceiptsRequest.kt new file mode 100644 index 00000000000..46a948b4eb8 --- /dev/null +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/GetMessageReceiptsRequest.kt @@ -0,0 +1,24 @@ +/* + * 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.testservice.models + +data class GetMessageReceiptsRequest( + val conversationDomain: String = "staging.zinfra.io", + val conversationId: String = "", + val messageId: String = "", +) diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/models/NewConversationRequest.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/NewConversationRequest.kt new file mode 100644 index 00000000000..7e4e2c2a93b --- /dev/null +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/NewConversationRequest.kt @@ -0,0 +1,23 @@ +/* + * 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.testservice.models + +data class NewConversationRequest( + val name: String = "", + val userIds: List = emptyList() +) diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionConfirmationRequest.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionConfirmationRequest.kt new file mode 100644 index 00000000000..a9b6060e28e --- /dev/null +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionConfirmationRequest.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.testservice.models + +data class SendButtonActionConfirmationRequest( + val conversationDomain: String = "staging.zinfra.io", + val conversationId: String = "", + val referenceMessageId: String = "", + val buttonId: String = "", + val userIds: List = emptyList() +) diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionRequest.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionRequest.kt new file mode 100644 index 00000000000..015a148b672 --- /dev/null +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendButtonActionRequest.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.testservice.models + +data class SendButtonActionRequest( + val conversationDomain: String = "staging.zinfra.io", + val conversationId: String = "", + val buttonId: String = "", + val referenceMessageId: String = "" +) diff --git a/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendTypingRequest.kt b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendTypingRequest.kt new file mode 100644 index 00000000000..534994fc29c --- /dev/null +++ b/testservice/src/main/kotlin/com/wire/kalium/testservice/models/SendTypingRequest.kt @@ -0,0 +1,24 @@ +/* + * 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.testservice.models + +data class SendTypingRequest( + val conversationDomain: String = "staging.zinfra.io", + val conversationId: String = "", + val status: String = "" +)