Skip to content

Commit

Permalink
feat: support import/export of conversations using TopicData (#84)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmccartney authored Jun 23, 2023
1 parent 9b6acda commit e1d1450
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import org.xmtp.android.library.messages.generate
import org.xmtp.android.library.messages.secp256K1Uncompressed
import org.xmtp.android.library.messages.toPublicKeyBundle
import org.xmtp.android.library.messages.walletAddress
import org.xmtp.proto.keystore.api.v1.Keystore
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.QueryRequest
import org.xmtp.proto.message.contents.Contact
import org.xmtp.proto.message.contents.InvitationV1Kt.context
import org.xmtp.proto.message.contents.PrivateKeyOuterClass
import org.xmtp.proto.message.contents.PrivateKeyOuterClass.PrivateKeyBundle
import java.util.Date

@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -186,6 +188,50 @@ class LocalInstrumentedTest {
assertEquals("example.com/alice-bob-1", aliceConvoList[1].conversationId)
}

@Test
fun testUsingSavedCredentialsAndKeyMaterial() {
val options = ClientOptions(ClientOptions.Api(XMTPEnvironment.LOCAL, isSecure = false))
val alice = Client().create(PrivateKeyBuilder(), options)
val bob = Client().create(PrivateKeyBuilder(), options)

// Alice starts a conversation with Bob
val aliceConvo = alice.conversations.newConversation(
bob.address,
context = context {
conversationId = "example.com/alice-bob-1"
metadata["title"] = "Chatting Using Saved Credentials"
}
)
aliceConvo.send("Hello Bob")
delayToPropagate()

// Alice stores her credentials and conversations to her device
val keyBundle = alice.privateKeyBundle.toByteArray()
val topicData = aliceConvo.toTopicData().toByteArray()

// Meanwhile, Bob sends a reply.
val bobConvos = bob.conversations.list()
val bobConvo = bobConvos[0]
bobConvo.send("Oh, hello Alice")
delayToPropagate()

// When Alice's device wakes up, it uses her saved credentials
val alice2 = Client().buildFromBundle(
PrivateKeyBundle.parseFrom(keyBundle),
options
)
// And it uses the saved topic data for the conversation
val aliceConvo2 = alice2.conversations.importTopicData(
Keystore.TopicMap.TopicData.parseFrom(topicData)
)
assertEquals("example.com/alice-bob-1", aliceConvo2.conversationId)

// Now Alice should be able to load message using her saved key material.
val messages = aliceConvo2.messages()
assertEquals("Hello Bob", messages[1].body)
assertEquals("Oh, hello Alice", messages[0].body)
}

@Test
fun testCanPaginateV1Messages() {
val bob = PrivateKeyBuilder()
Expand Down
22 changes: 22 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Conversation.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.xmtp.android.library

import android.util.Log
import com.google.protobuf.kotlin.toByteString
import kotlinx.coroutines.flow.Flow
import org.xmtp.android.library.codecs.EncodedContent
import org.xmtp.android.library.messages.Envelope
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
import org.xmtp.proto.message.contents.Invitation
import org.xmtp.proto.message.contents.Invitation.InvitationV1.Aes256gcmHkdfsha256
import java.util.Date

sealed class Conversation {
Expand Down Expand Up @@ -63,6 +67,24 @@ sealed class Conversation {
}
}

fun toTopicData(): TopicData {
val data = TopicData.newBuilder()
.setCreatedNs(createdAt.time * 1_000_000)
.setPeerAddress(peerAddress)
return when (this) {
is V1 -> data.build()
is V2 -> data.setInvitation(
Invitation.InvitationV1.newBuilder()
.setTopic(topic)
.setContext(conversationV2.context)
.setAes256GcmHkdfSha256(
Aes256gcmHkdfsha256.newBuilder()
.setKeyMaterial(conversationV2.keyMaterial.toByteString())
)
).build()
}
}

fun decode(envelope: Envelope): DecodedMessage {
return when (this) {
is V1 -> conversationV1.decode(envelope)
Expand Down
29 changes: 29 additions & 0 deletions library/src/main/java/org/xmtp/android/library/Conversations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.xmtp.android.library.messages.senderAddress
import org.xmtp.android.library.messages.sentAt
import org.xmtp.android.library.messages.toSignedPublicKeyBundle
import org.xmtp.android.library.messages.walletAddress
import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData
import org.xmtp.proto.message.contents.Contact
import org.xmtp.proto.message.contents.Invitation
import java.lang.Exception
Expand Down Expand Up @@ -183,6 +184,34 @@ data class Conversations(
return conversationsByTopic.values.sortedByDescending { it.createdAt }
}

fun importTopicData(data: TopicData): Conversation {
val conversation: Conversation
if (!data.hasInvitation()) {
val sentAt = Date(data.createdNs / 1_000_000)
conversation = Conversation.V1(
ConversationV1(
client,
data.peerAddress,
sentAt
)
)
} else {
conversation = Conversation.V2(
ConversationV2(
topic = data.invitation.topic,
keyMaterial = data.invitation.aes256GcmHkdfSha256.keyMaterial.toByteArray(),
context = data.invitation.context,
peerAddress = data.peerAddress,
client = client,
isGroup = false,
header = Invitation.SealedInvitationHeaderV1.getDefaultInstance()
)
)
}
conversationsByTopic[conversation.topic] = conversation
return conversation
}

private fun listIntroductionPeers(pagination: Pagination? = null): Map<String, Date> {
val envelopes =
runBlocking {
Expand Down

0 comments on commit e1d1450

Please sign in to comment.