Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support other message types [WPB-10575] #3122

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading