Skip to content

Commit

Permalink
feat: support other message types [WPB-10575] (#3122)
Browse files Browse the repository at this point in the history
Co-authored-by: Yamil Medina <[email protected]>
  • Loading branch information
vitorhugods and yamilmedina authored Dec 5, 2024
1 parent ccb8193 commit f16d679
Show file tree
Hide file tree
Showing 9 changed files with 375 additions and 57 deletions.
1 change: 1 addition & 0 deletions backup/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ kotlin {

// Libsodium
implementation(libs.libsodiumBindingsMP)
api(libs.kermit)
}
}
val commonTest by getting {
Expand Down
80 changes: 77 additions & 3 deletions backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ data class BackupMessage(
val senderUserId: BackupQualifiedId,
val senderClientId: String,
val creationDate: BackupDateTime,
val content: BackupMessageContent
val content: BackupMessageContent,
@Deprecated("Used only by the Webteam in order to simplify debugging", ReplaceWith(""))
val webPrimaryKey: Int? = null,
)

expect class BackupDateTime
Expand All @@ -100,6 +102,78 @@ expect fun BackupDateTime.toLongMilliseconds(): Long
sealed class BackupMessageContent {
data class Text(val text: String) : BackupMessageContent()

// TODO: Not _yet_ implemented
data class Asset(val todo: String) : BackupMessageContent()
data class Asset(
val mimeType: String,
val size: Int,
val name: String?,
val otrKey: ByteArray,
val sha256: ByteArray,
val assetId: String,
val assetToken: String?,
val assetDomain: String?,
val encryption: EncryptionAlgorithm?,
val metaData: AssetMetadata?,
) : BackupMessageContent() {
enum class EncryptionAlgorithm {
AES_GCM, AES_CBC
}

sealed class AssetMetadata {
data class Image(
val width: Int,
val height: Int,
val tag: String?
) : AssetMetadata()

data class Video(
val width: Int?,
val height: Int?,
val duration: Long?,
) : AssetMetadata()

data class Audio(
val normalization: ByteArray?,
val duration: Long?,
) : AssetMetadata()

data class Generic(
val name: String?,
) : AssetMetadata()
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Asset

if (!otrKey.contentEquals(other.otrKey)) return false
if (!sha256.contentEquals(other.sha256)) return false
if (assetId != other.assetId) return false
if (assetToken != other.assetToken) return false
if (assetDomain != other.assetDomain) return false
if (encryption != other.encryption) return false
if (metaData != other.metaData) return false

return true
}

override fun hashCode(): Int {
var result = otrKey.contentHashCode()
result = 31 * result + sha256.contentHashCode()
result = 31 * result + assetId.hashCode()
result = 31 * result + (assetToken?.hashCode() ?: 0)
result = 31 * result + (assetDomain?.hashCode() ?: 0)
result = 31 * result + (encryption?.hashCode() ?: 0)
result = 31 * result + (metaData?.hashCode() ?: 0)
return result
}
}

data class Location(
val longitude: Float,
val latitude: Float,
val name: String?,
val zoom: Int?,
) : BackupMessageContent()
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,12 @@ package com.wire.backup.dump

import com.wire.backup.data.BackupConversation
import com.wire.backup.data.BackupMessage
import com.wire.backup.data.BackupMessageContent
import com.wire.backup.data.BackupQualifiedId
import com.wire.backup.data.BackupUser
import com.wire.backup.data.toLongMilliseconds
import com.wire.backup.data.toProtoModel
import com.wire.backup.ingest.MPBackupMapper
import com.wire.kalium.protobuf.backup.BackupData
import com.wire.kalium.protobuf.backup.BackupInfo
import com.wire.kalium.protobuf.backup.ExportUser
import com.wire.kalium.protobuf.backup.ExportedConversation
import com.wire.kalium.protobuf.backup.ExportedMessage
import com.wire.kalium.protobuf.backup.ExportedText
import kotlinx.datetime.Clock
import pbandk.encodeToByteArray
import kotlin.experimental.ExperimentalObjCName
Expand All @@ -47,6 +42,7 @@ import kotlin.native.ShouldRefineInSwift
abstract class CommonMPBackupExporter(
private val selfUserId: BackupQualifiedId
) {
private val mapper = MPBackupMapper()
private val allUsers = mutableListOf<BackupUser>()
private val allConversations = mutableListOf<BackupConversation>()
private val allMessages = mutableListOf<BackupMessage>()
Expand Down Expand Up @@ -80,28 +76,14 @@ abstract class CommonMPBackupExporter(
creationTime = Clock.System.now().toEpochMilliseconds(),
clientId = "lol"
),
allConversations.map { ExportedConversation(it.id.toProtoModel(), it.name) },
allConversations.map {
mapper.mapConversationToProtobuf(it)
},
allMessages.map {
ExportedMessage(
id = it.id,
timeIso = it.creationDate.toLongMilliseconds(),
senderUserId = it.senderUserId.toProtoModel(),
senderClientId = it.senderClientId,
conversationId = it.conversationId.toProtoModel(),
content = when (val content = it.content) {
is BackupMessageContent.Asset ->
ExportedMessage.Content.Text(ExportedText("FAKE ASSET")) // TODO: Support assets
is BackupMessageContent.Text ->
ExportedMessage.Content.Text(ExportedText(content.text))
}
)
mapper.mapMessageToProtobuf(it)
},
allUsers.map {
ExportUser(
id = it.id.toProtoModel(),
name = it.name,
handle = it.handle
)
mapper.mapUserToProtobuf(it)
},
)
return backupData.encodeToByteArray().also {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import kotlin.native.ShouldRefineInSwift
*/
@OptIn(ExperimentalObjCRefinement::class)
@JsExport
abstract class CommonMPBackupImporter(selfUserDomain: String) {
private val mapper = MPBackupMapper(selfUserDomain)
abstract class CommonMPBackupImporter {
private val mapper = MPBackupMapper()

/**
* Attempts to deserialize backed-up data.
Expand Down
195 changes: 179 additions & 16 deletions backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,120 @@
*/
package com.wire.backup.ingest

import co.touchlab.kermit.Logger
import com.wire.backup.data.BackupConversation
import com.wire.backup.data.BackupData
import com.wire.backup.data.BackupDateTime
import com.wire.backup.data.BackupMessage
import com.wire.backup.data.BackupMessageContent
import com.wire.backup.data.BackupMessageContent.Asset.EncryptionAlgorithm
import com.wire.backup.data.BackupMetadata
import com.wire.backup.data.BackupUser
import com.wire.backup.data.toLongMilliseconds
import com.wire.backup.data.toModel
import com.wire.backup.data.toProtoModel
import com.wire.kalium.protobuf.backup.ExportUser
import com.wire.kalium.protobuf.backup.ExportedAsset
import com.wire.kalium.protobuf.backup.ExportedAudioMetaData
import com.wire.kalium.protobuf.backup.ExportedConversation
import com.wire.kalium.protobuf.backup.ExportedEncryptionAlgorithm
import com.wire.kalium.protobuf.backup.ExportedGenericMetaData
import com.wire.kalium.protobuf.backup.ExportedImageMetaData
import com.wire.kalium.protobuf.backup.ExportedLocation
import com.wire.kalium.protobuf.backup.ExportedMessage
import com.wire.kalium.protobuf.backup.ExportedMessage.Content
import com.wire.kalium.protobuf.backup.ExportedText
import com.wire.kalium.protobuf.backup.ExportedVideoMetaData
import pbandk.ByteArr
import com.wire.kalium.protobuf.backup.BackupData as ProtoBackupData

internal class MPBackupMapper(val selfUserDomain: String) {
internal class MPBackupMapper {

fun mapUserToProtobuf(it: BackupUser) = ExportUser(
id = it.id.toProtoModel(),
name = it.name,
handle = it.handle
)

@Suppress("LongMethod")
fun mapMessageToProtobuf(it: BackupMessage): ExportedMessage {
return ExportedMessage(
id = it.id,
timeIso = it.creationDate.toLongMilliseconds(),
senderUserId = it.senderUserId.toProtoModel(),
senderClientId = it.senderClientId,
conversationId = it.conversationId.toProtoModel(),
content = when (val content = it.content) {
is BackupMessageContent.Asset -> {
Logger.d("MPBackupMapper") { "Mapping asset message to protobuf: ${content.metaData}" }
Content.Asset(
ExportedAsset(
content.mimeType,
content.size.toLong(),
content.name,
ByteArr(content.otrKey),
ByteArr(content.sha256),
content.assetId,
content.assetToken,
content.assetDomain,
when (content.encryption) {
EncryptionAlgorithm.AES_GCM -> ExportedEncryptionAlgorithm.BACKUP_AES_GCM
EncryptionAlgorithm.AES_CBC -> ExportedEncryptionAlgorithm.BACKUP_AES_CBC
null -> null
},
content.metaData?.let {
when (it) {
is BackupMessageContent.Asset.AssetMetadata.Audio ->
ExportedAsset.MetaData.Audio(
ExportedAudioMetaData(
it.duration,
it.normalization?.let { ByteArr(it) }
)
)

is BackupMessageContent.Asset.AssetMetadata.Image ->
ExportedAsset.MetaData.Image(
ExportedImageMetaData(
it.width,
it.height,
it.tag
)
)

is BackupMessageContent.Asset.AssetMetadata.Video ->
ExportedAsset.MetaData.Video(
ExportedVideoMetaData(
it.width,
it.height,
it.duration
)
)

is BackupMessageContent.Asset.AssetMetadata.Generic ->
ExportedAsset.MetaData.Generic(ExportedGenericMetaData(it.name))
}
}
)
)
}

is BackupMessageContent.Text ->
Content.Text(ExportedText(content.text))

is BackupMessageContent.Location -> Content.Location(
ExportedLocation(
content.longitude,
content.latitude,
content.name,
content.zoom
)
)
},
webPk = it.webPrimaryKey?.toLong()
)
}

fun mapConversationToProtobuf(it: BackupConversation) = ExportedConversation(it.id.toProtoModel(), it.name)

fun fromProtoToBackupModel(
protobufData: ProtoBackupData
Expand All @@ -41,28 +143,89 @@ internal class MPBackupMapper(val selfUserDomain: String) {
info.clientId
),
users.map { user ->
BackupUser(user.id.toModel(), user.name, user.handle)
fromUserProtoToBackupModel(user)
}.toTypedArray(),
conversations.map { conversation ->
BackupConversation(conversation.id.toModel(), conversation.name)
fromConversationProtoToBackupModel(conversation)
}.toTypedArray(),
messages.map { message ->
val content = when (val proContent = message.content) {
is Content.Text -> {
BackupMessageContent.Text(proContent.value.content)
fromMessageProtoToBackupModel(message)
}.toTypedArray()
)
}

@Suppress("LongMethod")
private fun fromMessageProtoToBackupModel(message: ExportedMessage): BackupMessage {
val content = when (val protoContent = message.content) {
is Content.Text -> {
BackupMessageContent.Text(protoContent.value.content)
}

is Content.Asset -> BackupMessageContent.Asset(
protoContent.value.mimetype,
protoContent.value.protoSize,
protoContent.value.name,
protoContent.value.otrKey.array,
protoContent.value.sha256.array,
protoContent.value.assetId,
protoContent.value.assetToken,
protoContent.value.assetDomain,
protoContent.value.encryption?.let {
when (it) {
ExportedEncryptionAlgorithm.BACKUP_AES_CBC -> EncryptionAlgorithm.AES_CBC
ExportedEncryptionAlgorithm.BACKUP_AES_GCM -> EncryptionAlgorithm.AES_GCM
is ExportedEncryptionAlgorithm.UNRECOGNIZED -> null
}
},
protoContent.value.metaData?.let {
when (it) {
is ExportedAsset.MetaData.Audio -> BackupMessageContent.Asset.AssetMetadata.Audio(
it.value.normalizedLoudness?.array,
it.value.durationInMillis
)

null -> TODO()
is ExportedAsset.MetaData.Image -> BackupMessageContent.Asset.AssetMetadata.Image(
it.value.width,
it.value.height,
it.value.tag
)

is ExportedAsset.MetaData.Video -> BackupMessageContent.Asset.AssetMetadata.Video(
it.value.width,
it.value.height,
it.value.durationInMillis
)

is ExportedAsset.MetaData.Generic -> BackupMessageContent.Asset.AssetMetadata.Generic(
it.value.name
)
}
}
BackupMessage(
id = message.id,
conversationId = message.conversationId.toModel(),
senderUserId = message.senderUserId.toModel(),
senderClientId = message.senderClientId,
creationDate = BackupDateTime(message.timeIso),
content = content
)
}.toTypedArray()
)

is Content.Location -> BackupMessageContent.Location(
protoContent.value.longitude,
protoContent.value.latitude,
protoContent.value.name,
protoContent.value.zoom
)

null -> throw IllegalArgumentException("Message content cannot be null!")
}
return BackupMessage(
id = message.id,
conversationId = message.conversationId.toModel(),
senderUserId = message.senderUserId.toModel(),
senderClientId = message.senderClientId,
creationDate = BackupDateTime(message.timeIso),
content = content,
webPrimaryKey = message.webPk?.toInt()
)
}

private fun fromConversationProtoToBackupModel(conversation: ExportedConversation) =
BackupConversation(conversation.id.toModel(), conversation.name)

private fun fromUserProtoToBackupModel(user: ExportUser) =
BackupUser(user.id.toModel(), user.name, user.handle)
}
Loading

0 comments on commit f16d679

Please sign in to comment.