Skip to content

Commit

Permalink
feat: add support for asset and location message content
Browse files Browse the repository at this point in the history
  • Loading branch information
vitorhugods committed Nov 22, 2024
1 parent b824fb5 commit 8b78c9b
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 59 deletions.
50 changes: 48 additions & 2 deletions backup/src/commonMain/kotlin/com/wire/backup/data/BackupData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,52 @@ 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?,
) : BackupMessageContent() {
enum class EncryptionAlgorithm {
AES_GCM, AES_CBC
}

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

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)
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 All @@ -44,7 +44,7 @@ abstract class CommonMPBackupImporter(selfUserDomain: String) {
BackupImportResult.Success(
mapper.fromProtoToBackupModel(ProtoBackupData.decodeFromByteArray(data))
)
} catch (e: Exception) {
} catch (e: Throwable) {
e.printStackTrace()
println(e)
BackupImportResult.ParsingFailure
Expand Down
128 changes: 111 additions & 17 deletions backup/src/commonMain/kotlin/com/wire/backup/ingest/MPBackupMapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,72 @@ 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.ExportedConversation
import com.wire.kalium.protobuf.backup.ExportedEncryptionAlgorithm
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 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
)

fun mapMessageToProtobuf(it: BackupMessage) = 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 ->
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
}
)
)

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

is BackupMessageContent.Location -> Content.Location(
ExportedLocation(
content.longitude,
content.latitude,
content.name,
content.zoom
)
)
}
)

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

fun fromProtoToBackupModel(
protobufData: ProtoBackupData
Expand All @@ -41,28 +100,63 @@ 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()
)
}

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

null -> TODO()
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
}
}
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
)
}

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

private fun fromUserProtoToBackupModel(user: ExportUser) =
BackupUser(user.id.toModel(), user.name, user.handle)
}
57 changes: 48 additions & 9 deletions backup/src/commonTest/kotlin/com/wire/backup/BackupEndToEndTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,72 @@ import com.wire.backup.ingest.CommonMPBackupImporter
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertIs

class BackupEndToEndTest {

@Test
fun givenBackedUpMessages_whenRestoring_thenShouldReadTheSameContent() = runTest {
fun givenBackedUpTextMessages_whenRestoring_thenShouldReadTheSameContent() = runTest {
shouldBackupAndRestoreSameContent(BackupMessageContent.Text("Hello from the backup!"))
}

@Test
fun givenBackedUpAssetMessage_whenRestoring_thenShouldReadTheSameContent() = runTest {
val content = BackupMessageContent.Asset(
mimeType = "image/jpeg",
size = 42,
name = "pudim.jpg",
otrKey = byteArrayOf(31),
sha256 = byteArrayOf(33),
assetId = "assetId",
assetToken = "token",
assetDomain = "domain",
encryption = BackupMessageContent.Asset.EncryptionAlgorithm.AES_GCM
)
shouldBackupAndRestoreSameContent(content)
}

@Test
fun givenBackedUpLocationMessage_whenRestoring_thenShouldReadTheSameContent() = runTest {
val content = BackupMessageContent.Location(
longitude = 42f,
latitude = 24f,
name = "Somewhere over the rainbow",
zoom = 13
)
shouldBackupAndRestoreSameContent(content)
}

private fun shouldBackupAndRestoreSameContent(content: BackupMessageContent) {
val expectedMessage = BackupMessage(
"messageId",
BackupQualifiedId("value", "domain"),
BackupQualifiedId("senderID", "senderDomain"),
"senderClientId",
BackupDateTime(24232L),
BackupMessageContent.Text("Hello from the backup!")
id = "messageId",
conversationId = BackupQualifiedId("value", "domain"),
senderUserId = BackupQualifiedId("senderID", "senderDomain"),
senderClientId = "senderClientId",
creationDate = BackupDateTime(0L),
content = content,
)
val exporter = object : CommonMPBackupExporter(BackupQualifiedId("eghyue", "potato")) {}
exporter.addMessage(expectedMessage)
val encoded = exporter.serialize()

val importer = object : CommonMPBackupImporter("potato") {}
val importer = object : CommonMPBackupImporter() {}
val result = importer.importBackup(encoded)
assertIs<BackupImportResult.Success>(result)
val firstMessage = result.backupData.messages.first()
assertEquals(expectedMessage.conversationId, firstMessage.conversationId)
assertEquals(expectedMessage.id, firstMessage.id)
assertEquals(expectedMessage.senderClientId, firstMessage.senderClientId)
assertEquals(expectedMessage.senderUserId, firstMessage.senderUserId)
assertEquals(expectedMessage.creationDate, firstMessage.creationDate)
assertEquals(expectedMessage.content, firstMessage.content)
assertContentEquals(arrayOf(expectedMessage), result.backupData.messages)
}

@Test
fun givenBackUpDataIsUnrecognisable_whenRestoring_thenShouldReturnParsingError() = runTest {
val importer = object : CommonMPBackupImporter("potato") {}
val importer = object : CommonMPBackupImporter() {}
val result = importer.importBackup(byteArrayOf(0x42, 0x42, 0x42))
assertIs<BackupImportResult.ParsingFailure>(result)
}
Expand Down
15 changes: 14 additions & 1 deletion backup/src/jsMain/kotlin/com/wire/backup/data/BackupDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@ package com.wire.backup.data
import kotlin.js.Date

@JsExport
actual data class BackupDateTime(val date: Date)
actual data class BackupDateTime(val date: Date) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class.js != other::class.js) return false

other as BackupDateTime

return this.toLongMilliseconds() == other.toLongMilliseconds()
}

override fun hashCode(): Int {
return date.hashCode()
}
}

actual fun BackupDateTime(timestampMillis: Long): BackupDateTime {
return BackupDateTime(Date(timestampMillis))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
package com.wire.backup.ingest

@JsExport
actual class MPBackupImporter(selfUserDomain: String) : CommonMPBackupImporter(selfUserDomain)
actual class MPBackupImporter : CommonMPBackupImporter()
Loading

0 comments on commit 8b78c9b

Please sign in to comment.