diff --git a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/HistoryClientDAO.kt b/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/HistoryClientDAO.kt index f645791700f..ccf501b3b6a 100644 --- a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/HistoryClientDAO.kt +++ b/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/HistoryClientDAO.kt @@ -17,7 +17,7 @@ */ package com.wire.kalium.conversation.history.data -import com.wire.kalium.conversation.history.HistoryClient +import com.wire.kalium.logic.data.history.HistoryClient import com.wire.kalium.persistence.dao.QualifiedIDEntity import kotlinx.coroutines.flow.Flow import kotlinx.datetime.Instant diff --git a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAO.kt b/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAO.kt index d001248f405..0b2ae0b7ed9 100644 --- a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAO.kt +++ b/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAO.kt @@ -19,8 +19,7 @@ package com.wire.kalium.conversation.history.data import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList -import com.wire.kalium.conversation.history.HistoryClient -import com.wire.kalium.logic.data.id.ConversationId +import com.wire.kalium.logic.data.history.HistoryClient import com.wire.kalium.persistence.HistoryClientQueries import com.wire.kalium.persistence.dao.QualifiedIDEntity import kotlinx.coroutines.Dispatchers @@ -44,12 +43,10 @@ internal class SQLiteHistoryClientDAO internal constructor( * Maps a database entity to a domain model. */ private fun mapToHistoryClient( - conversationId: QualifiedIDEntity, id: String, secret: ByteArray, creationDate: Instant ): HistoryClient = HistoryClient( - conversationId = ConversationId(conversationId.value, conversationId.domain), id = id, creationTime = creationDate, secret = HistoryClient.Secret(secret) diff --git a/conversation-history/src/commonTest/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAOTest.kt b/conversation-history/src/commonTest/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAOTest.kt index 7fc5c109399..6fb9147d37f 100644 --- a/conversation-history/src/commonTest/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAOTest.kt +++ b/conversation-history/src/commonTest/kotlin/com/wire/kalium/conversation/history/data/SQLiteHistoryClientDAOTest.kt @@ -18,7 +18,6 @@ package com.wire.kalium.conversation.history.data import app.cash.turbine.test -import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.persistence.TestUserDatabase import com.wire.kalium.persistence.dao.QualifiedIDEntity import com.wire.kalium.persistence.dao.UserIDEntity @@ -121,7 +120,6 @@ class SQLiteHistoryClientDAOTest { // Then assertEquals(1, result.size) with(result.first()) { - assertEquals(ConversationId(conversationId.value, conversationId.domain), this.conversationId) assertEquals(clientId, this.id) assertEquals(creationDate, this.creationTime) assertTrue(this.secret.value.contentEquals(secret)) @@ -166,12 +164,10 @@ class SQLiteHistoryClientDAOTest { assertEquals(1, result2.size) with(result1.first()) { - assertEquals(ConversationId(conversationId1.value, conversationId1.domain), this.conversationId) assertEquals(clientId1, this.id) } with(result2.first()) { - assertEquals(ConversationId(conversationId2.value, conversationId2.domain), this.conversationId) assertEquals(clientId2, this.id) } } diff --git a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/HistoryClient.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/history/HistoryClient.kt similarity index 85% rename from conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/HistoryClient.kt rename to data/src/commonMain/kotlin/com/wire/kalium/logic/data/history/HistoryClient.kt index 97f878dac6c..ce613237361 100644 --- a/conversation-history/src/commonMain/kotlin/com/wire/kalium/conversation/history/HistoryClient.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/history/HistoryClient.kt @@ -15,14 +15,12 @@ * 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.conversation.history +package com.wire.kalium.logic.data.history -import com.wire.kalium.logic.data.id.ConversationId import kotlinx.datetime.Instant import kotlin.time.ExperimentalTime -public data class HistoryClient @OptIn(ExperimentalTime::class) constructor( - val conversationId: ConversationId, +data class HistoryClient @OptIn(ExperimentalTime::class) constructor( val id: String, val creationTime: Instant, val secret: Secret, @@ -45,6 +43,5 @@ public data class HistoryClient @OptIn(ExperimentalTime::class) constructor( override fun hashCode(): Int { return value.contentHashCode() } - } } 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 90a344a1a84..34bc2dcaef1 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 @@ -260,6 +260,19 @@ sealed interface Message { is MessageContent.CompositeEdited -> mutableMapOf( typeKey to "compositeEdited" ) + + MessageContent.History.ClientsRequest -> mutableMapOf( + typeKey to "historyClientsRequest", + ) + + is MessageContent.History.ClientsResponse -> mutableMapOf( + typeKey to "historyClientsResponse", + "count" to content.clients.size + ) + + is MessageContent.History.NewClientAvailable -> mutableMapOf( + typeKey to "historyNewClientAvailable", + ) } val standardProperties = mapOf( 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 66320bbb38d..150f871f227 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 @@ -21,6 +21,7 @@ package com.wire.kalium.logic.data.message import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.data.conversation.ClientId import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.history.HistoryClient import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.MessageButtonId import com.wire.kalium.logic.data.id.MessageId @@ -401,6 +402,12 @@ sealed interface MessageContent { val emojis: Map ) : Signaling + sealed interface History : Signaling { + data class NewClientAvailable(val client: HistoryClient) : History + data class ClientsResponse(val clients: List) : History + data object ClientsRequest : History + } + data class Multipart( val value: String?, val linkPreviews: List = emptyList(), @@ -421,7 +428,7 @@ sealed interface MessageContent { * @return A string representing the type of content. * Useful for logging. Plain strings must be used, otherwise it may be affected by code minification. */ -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongMethod") fun MessageContent?.getType() = when (this) { is MessageContent.Asset -> "Asset" is MessageContent.FailedDecryption -> "FailedDecryption" @@ -478,6 +485,9 @@ fun MessageContent?.getType() = when (this) { is MessageContent.InCallEmoji -> "InCallEmoji" is MessageContent.Multipart -> "Multipart" is MessageContent.CompositeEdited -> "CompositeEdited" + MessageContent.History.ClientsRequest -> "History.ClientsRequest" + is MessageContent.History.ClientsResponse -> "History.ClientsResponse" + is MessageContent.History.NewClientAvailable -> "History.NewClientAvailable" null -> "null" } diff --git a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt index 3dd2f60edd7..cdaa24c4d9b 100644 --- a/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt +++ b/data/src/commonMain/kotlin/com/wire/kalium/logic/data/message/MessageContentLogging.kt @@ -44,4 +44,7 @@ inline fun MessageContent.FromProto.typeDescription(): String = when (this) { is MessageContent.InCallEmoji -> "InCallEmoji" is MessageContent.Multipart -> "Multipart" is MessageContent.CompositeEdited -> "CompositeEdited" + MessageContent.History.ClientsRequest -> "History.ClientsRequest" + is MessageContent.History.ClientsResponse -> "History.ClientsResponse" + is MessageContent.History.NewClientAvailable -> "History.NewClientAvailable" } 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 060fe5cda73..3815ab97461 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 @@ -131,6 +131,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.DataTransfer -> false is MessageContent.InCallEmoji -> false is MessageContent.Multipart -> true + is MessageContent.History -> false } @Suppress("ComplexMethod") @@ -188,6 +189,7 @@ internal class PersistMessageUseCaseImpl( is MessageContent.MemberChange.RemovedFromTeam, is MessageContent.TeamMemberRemoved, is MessageContent.DataTransfer, - is MessageContent.InCallEmoji -> false + is MessageContent.InCallEmoji, + is MessageContent.History -> false } } 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 ced986275dc..0593675ff72 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 @@ -25,6 +25,7 @@ import com.wire.kalium.logic.data.asset.AssetTransferStatus import com.wire.kalium.logic.data.asset.toModel import com.wire.kalium.logic.data.asset.toProto import com.wire.kalium.logic.data.conversation.Conversation +import com.wire.kalium.logic.data.history.HistoryClient import com.wire.kalium.logic.data.id.ConversationId import com.wire.kalium.logic.data.id.IdMapper import com.wire.kalium.logic.data.message.composite.CompositeButton @@ -51,7 +52,12 @@ import com.wire.kalium.protobuf.messages.DataTransfer import com.wire.kalium.protobuf.messages.Ephemeral import com.wire.kalium.protobuf.messages.External import com.wire.kalium.protobuf.messages.GenericMessage +import com.wire.kalium.protobuf.messages.GenericMessage.Content.Availability +import com.wire.kalium.protobuf.messages.GenericMessage.Content.Deleted import com.wire.kalium.protobuf.messages.GenericMessage.UnknownStrategy +import com.wire.kalium.protobuf.messages.HistoryClientAvailable +import com.wire.kalium.protobuf.messages.HistoryClientRequest +import com.wire.kalium.protobuf.messages.HistoryClientResponse import com.wire.kalium.protobuf.messages.InCallEmoji import com.wire.kalium.protobuf.messages.Knock import com.wire.kalium.protobuf.messages.LastRead @@ -66,9 +72,11 @@ 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.util.DateTimeUtil.toIsoDateTimeString import io.mockative.Mockable import kotlinx.datetime.Instant import pbandk.ByteArr +import com.wire.kalium.protobuf.messages.HistoryClient as ProtoHistoryClient @Mockable interface ProtoContentMapper { @@ -130,9 +138,9 @@ class ProtoContentMapperImpl( is MessageContent.Calling -> packCalling(readableContent) is MessageContent.Asset -> packAsset(readableContent, expectsReadConfirmation, legalHoldStatus) is MessageContent.Knock -> packKnock(readableContent, legalHoldStatus) - is MessageContent.DeleteMessage -> GenericMessage.Content.Deleted(MessageDelete(messageId = readableContent.messageId)) + is MessageContent.DeleteMessage -> Deleted(MessageDelete(messageId = readableContent.messageId)) is MessageContent.DeleteForMe -> packHidden(readableContent) - is MessageContent.Availability -> GenericMessage.Content.Availability( + is MessageContent.Availability -> Availability( availabilityMapper.fromModelAvailabilityToProto( readableContent.status ) @@ -161,6 +169,27 @@ class ProtoContentMapperImpl( is MessageContent.DataTransfer -> packDataTransfer(readableContent) is MessageContent.InCallEmoji -> packInCallEmoji(readableContent) is MessageContent.Multipart -> packMultipart(readableContent, expectsReadConfirmation, legalHoldStatus) + is MessageContent.History -> packHistoryMessage(readableContent) + } + } + + private fun packHistoryMessage(readableContent: MessageContent.History): GenericMessage.Content { + fun mapHistoryClientToProto(historyClient: HistoryClient): ProtoHistoryClient { + return ProtoHistoryClient( + clientId = historyClient.id, + createdAt = historyClient.creationTime.toIsoDateTimeString(), + secret = ByteArr(historyClient.secret.value) + ) + } + return when (readableContent) { + MessageContent.History.ClientsRequest -> GenericMessage.Content.HistoryClientRequest(HistoryClientRequest()) + is MessageContent.History.ClientsResponse -> GenericMessage.Content.HistoryClientResponse( + HistoryClientResponse(readableContent.clients.map { mapHistoryClientToProto(it) }) + ) + + is MessageContent.History.NewClientAvailable -> GenericMessage.Content.HistoryClientAvailable( + HistoryClientAvailable(mapHistoryClientToProto(readableContent.client)) + ) } } @@ -337,6 +366,7 @@ class ProtoContentMapperImpl( is MessageContent.CompositeEdited, is MessageContent.DataTransfer, is MessageContent.InCallEmoji, + is MessageContent.History, is MessageContent.Multipart -> throw IllegalArgumentException( "Unexpected message content type: ${readableContent.getType()}" ) @@ -349,7 +379,9 @@ class ProtoContentMapperImpl( External( ByteArr(protoContent.otrKey), protoContent.sha256?.let { ByteArr(it) }, - protoContent.encryptionAlgorithm?.let { encryptionAlgorithmMapper.toProtoBufModel(it) } + protoContent.encryptionAlgorithm?.let { + encryptionAlgorithmMapper.toProtoBufModel(it) + } ) ) @@ -450,6 +482,10 @@ class ProtoContentMapperImpl( is GenericMessage.Content.InCallEmoji -> unpackInCallEmoji(protoContent) + is GenericMessage.Content.HistoryClientAvailable -> unpackHistoryClientAvailable(protoContent) + is GenericMessage.Content.HistoryClientRequest -> unpackHistoryClientRequest() + is GenericMessage.Content.HistoryClientResponse -> unpackHistoryClientResponse(protoContent) + null -> { kaliumLogger.w( "Null content when parsing protobuf. Message UUID = ${genericMessage.messageId.obfuscateId()}" + @@ -889,6 +925,21 @@ class ProtoContentMapperImpl( ) } + private fun unpackHistoryClientRequest(): MessageContent.History = + MessageContent.History.ClientsRequest + + private fun protoHistoryClientToHistoryClient(protoClient: ProtoHistoryClient) = HistoryClient( + id = protoClient.clientId, + secret = HistoryClient.Secret(protoClient.secret.array), + creationTime = Instant.parse(protoClient.createdAt), + ) + + private fun unpackHistoryClientResponse(protoContent: GenericMessage.Content.HistoryClientResponse): MessageContent.History = + MessageContent.History.ClientsResponse(protoContent.value.clients.map(::protoHistoryClientToHistoryClient)) + + private fun unpackHistoryClientAvailable(protoContent: GenericMessage.Content.HistoryClientAvailable): MessageContent.History = + MessageContent.History.NewClientAvailable(protoHistoryClientToHistoryClient(protoContent.value.client)) + private fun extractConversationId( qualifiedConversationID: QualifiedConversationId?, unqualifiedConversationID: String diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt index a467f4cf3a8..c0a9230b45e 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/sync/receiver/conversation/message/ApplicationMessageHandler.kt @@ -184,23 +184,7 @@ internal class ApplicationMessageHandlerImpl( userRepository.updateOtherUserAvailabilityStatus(signaling.senderUserId, content.status) } - is MessageContent.ClientAction -> { - logger.i(message = "ClientAction status update received: ") - - val message = Message.System( - id = signaling.id, - content = MessageContent.CryptoSessionReset, - conversationId = signaling.conversationId, - date = signaling.date, - senderUserId = signaling.senderUserId, - status = signaling.status, - senderUserName = signaling.senderUserName, - expirationData = null - ) - - logger.i(message = "Persisting crypto session reset system message..") - persistMessage(message) - } + is MessageContent.ClientAction -> handleClientAction(signaling) is MessageContent.Reaction -> persistReaction(content, signaling.conversationId, signaling.senderUserId, signaling.date) is MessageContent.DeleteMessage -> deleteMessageHandler(content, signaling.conversationId, signaling.senderUserId) @@ -240,9 +224,31 @@ internal class ApplicationMessageHandlerImpl( ) is MessageContent.CompositeEdited -> messageCompositeEditHandler.handle(signaling, content) + + is MessageContent.History -> TODO("HISTORY CLIENTS ARE NOT HANDLED YET") } } + private suspend fun handleClientAction( + signaling: Message.Signaling, + ) { + logger.i(message = "ClientAction status update received: ") + + val message = Message.System( + id = signaling.id, + content = MessageContent.CryptoSessionReset, + conversationId = signaling.conversationId, + date = signaling.date, + senderUserId = signaling.senderUserId, + status = signaling.status, + senderUserName = signaling.senderUserName, + expirationData = null + ) + + logger.i(message = "Persisting crypto session reset system message..") + persistMessage(message) + } + private suspend fun processMessage(message: Message.Regular) { logger.i(message = "Message received: { \"message\" : ${message.toLogString()} }") when (val content = message.content) { diff --git a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/HistoryClient.sq b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/HistoryClient.sq index de23cc07f44..29328108dc2 100644 --- a/persistence/src/commonMain/db_user/com/wire/kalium/persistence/HistoryClient.sq +++ b/persistence/src/commonMain/db_user/com/wire/kalium/persistence/HistoryClient.sq @@ -15,10 +15,10 @@ CREATE INDEX history_client_id ON HistoryClient(id); CREATE INDEX history_client_conversation_date ON HistoryClient(conversation_id, creation_date); selectAllForConversation: -SELECT * FROM HistoryClient WHERE conversation_id = ?; +SELECT id, secret, creation_date FROM HistoryClient WHERE conversation_id = ?; selectAllForConversationFromDateOnwards: -SELECT * FROM HistoryClient WHERE conversation_id = ? AND creation_date >= ?; +SELECT id, secret, creation_date FROM HistoryClient WHERE conversation_id = ? AND creation_date >= ?; insertClient: INSERT OR IGNORE INTO HistoryClient(conversation_id, id, secret, creation_date) VALUES (?, ?, ?, ?); diff --git a/protobuf-codegen/src/main/proto/messages.proto b/protobuf-codegen/src/main/proto/messages.proto index 38f43235575..abc6189f8dc 100644 --- a/protobuf-codegen/src/main/proto/messages.proto +++ b/protobuf-codegen/src/main/proto/messages.proto @@ -50,6 +50,9 @@ message GenericMessage { // Next field should be 26 ↓ InCallHandRaise inCallHandRaise = 26; Multipart multipart = 27; + HistoryClientAvailable historyClientAvailable = 28; + HistoryClientRequest historyClientRequest = 29; + HistoryClientResponse historyClientResponse = 30; } optional UnknownStrategy unknownStrategy = 25 [default = IGNORE]; @@ -365,6 +368,25 @@ message TrackingIdentifier { required string identifier = 1; } +message HistoryClient { + required string client_id = 1; + required string created_at = 2; + required bytes secret = 3; +} + +// Sent in a conversation after new history client has been added +message HistoryClientAvailable { + required HistoryClient client = 1; +} + +// Sent in a conversation after you've been added to conversation with history sharing enabled +message HistoryClientRequest {} + +// Sent in a subconversation in response to a HistoryClientRequest +message HistoryClientResponse { + repeated HistoryClient clients = 1; +} + // Enums have to come last because of an unresolved issue with jsdoc // https://github.com/jsdoc/jsdoc/pull/1686