Skip to content

Commit

Permalink
feat: improve PreparedMessage handling (#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmccartney authored Sep 6, 2023
1 parent b82ac6c commit 0976446
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ class ConversationTest {
assertEquals(conversation.version, Conversation.Version.V1)
val preparedMessage = conversation.prepareMessage(content = "hi")
val messageID = preparedMessage.messageId
preparedMessage.send()
conversation.send(prepared = preparedMessage)
val messages = conversation.messages()
val message = messages[0]
assertEquals("hi", message.body)
Expand All @@ -609,7 +609,23 @@ class ConversationTest {
val conversation = aliceClient.conversations.newConversation(bob.walletAddress)
val preparedMessage = conversation.prepareMessage(content = "hi")
val messageID = preparedMessage.messageId
preparedMessage.send()
conversation.send(prepared = preparedMessage)
val messages = conversation.messages()
val message = messages[0]
assertEquals("hi", message.body)
assertEquals(message.id, messageID)
}

@Test
fun testCanSendPreparedMessageWithoutConversation() {
val conversation = aliceClient.conversations.newConversation(bob.walletAddress)
val preparedMessage = conversation.prepareMessage(content = "hi")
val messageID = preparedMessage.messageId

// This does not need the `conversation` to `.publish` the message.
// This simulates a background task publishing all pending messages upon connection.
aliceClient.publish(envelopes = preparedMessage.envelopes)

val messages = conversation.messages()
val message = messages[0]
assertEquals("hi", message.body)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ sealed class Conversation {
}
}

fun send(prepared: PreparedMessage) {
when (this) {
is V1 -> conversationV1.send(prepared = prepared)
is V2 -> conversationV2.send(prepared = prepared)
}
}

fun <T> send(content: T, options: SendOptions? = null) {
when (this) {
is V1 -> conversationV1.send(content = content, options = options)
Expand Down
53 changes: 23 additions & 30 deletions library/src/main/java/org/xmtp/android/library/ConversationV1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.xmtp.android.library

import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.runBlocking
import org.web3j.crypto.Hash
Expand Down Expand Up @@ -89,20 +88,22 @@ data class ConversationV1(
sentAt: Date? = null,
): String {
val preparedMessage = prepareMessage(content = text, options = sendOptions)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun <T> send(content: T, options: SendOptions? = null): String {
val preparedMessage = prepareMessage(content = content, options = options)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun send(encodedContent: EncodedContent, options: SendOptions? = null): String {
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun send(prepared: PreparedMessage): String {
client.publish(envelopes = prepared.envelopes)
return prepared.messageId
}

fun <T> prepareMessage(content: T, options: SendOptions?): PreparedMessage {
Expand Down Expand Up @@ -147,36 +148,28 @@ data class ConversationV1(

val isEphemeral: Boolean = options != null && options.ephemeral

val messageEnvelope =
val env =
EnvelopeBuilder.buildFromString(
topic = if (isEphemeral) ephemeralTopic else topic.description,
timestamp = date,
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
)
return PreparedMessage(
messageEnvelope = messageEnvelope,
conversation = Conversation.V1(this)
) {
val envelopes = mutableListOf(messageEnvelope)
if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) {
envelopes.addAll(
listOf(
EnvelopeBuilder.buildFromTopic(
topic = Topic.userIntro(peerAddress),
timestamp = date,
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
),
EnvelopeBuilder.buildFromTopic(
topic = Topic.userIntro(client.address),
timestamp = date,
message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray()
)
)

val envelopes = mutableListOf(env)
if (client.contacts.needsIntroduction(peerAddress) && !isEphemeral) {
envelopes.addAll(
listOf(
env.toBuilder().apply {
contentTopic = Topic.userIntro(peerAddress).description
}.build(),
env.toBuilder().apply {
contentTopic = Topic.userIntro(client.address).description
}.build(),
)
client.contacts.hasIntroduced[peerAddress] = true
}
client.publish(envelopes = envelopes)
)
client.contacts.hasIntroduced[peerAddress] = true
}
return PreparedMessage(envelopes)
}

private fun generateId(envelope: Envelope): String =
Expand Down
18 changes: 9 additions & 9 deletions library/src/main/java/org/xmtp/android/library/ConversationV2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,20 +100,22 @@ data class ConversationV2(

fun <T> send(content: T, options: SendOptions? = null): String {
val preparedMessage = prepareMessage(content = content, options = options)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun send(text: String, options: SendOptions? = null, sentAt: Date? = null): String {
val preparedMessage = prepareMessage(content = text, options = options)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun send(encodedContent: EncodedContent, options: SendOptions?): String {
val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options)
preparedMessage.send()
return preparedMessage.messageId
return send(preparedMessage)
}

fun send(prepared: PreparedMessage): String {
client.publish(envelopes = prepared.envelopes)
return prepared.messageId
}

fun <Codec : ContentCodec<T>, T> encode(codec: Codec, content: T): ByteArray {
Expand Down Expand Up @@ -170,9 +172,7 @@ data class ConversationV2(
timestamp = Date(),
message = MessageBuilder.buildFromMessageV2(v2 = message).toByteArray()
)
return PreparedMessage(messageEnvelope = envelope, conversation = Conversation.V2(this)) {
client.publish(envelopes = listOf(envelope))
}
return PreparedMessage(listOf(envelope))
}

private fun generateId(envelope: Envelope): String =
Expand Down
35 changes: 26 additions & 9 deletions library/src/main/java/org/xmtp/android/library/PreparedMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,37 @@ package org.xmtp.android.library

import org.web3j.crypto.Hash
import org.xmtp.android.library.messages.Envelope
import org.xmtp.proto.message.api.v1.MessageApiOuterClass.PublishRequest

// This houses a fully prepared message that can be published
// as soon as the API client has connectivity.
//
// To support persistence layers that queue pending messages (e.g. while offline)
// this struct supports serializing to/from bytes that can be written to disk or elsewhere.
// See toSerializedData() and fromSerializedData()
data class PreparedMessage(
var messageEnvelope: Envelope,
var conversation: Conversation,
var onSend: () -> Unit,
// The first envelope should send the message to the conversation itself.
// Any more are for required intros/invites etc.
// A client can just publish these when it has connectivity.
val envelopes: List<Envelope>
) {
companion object {
fun fromSerializedData(data: ByteArray): PreparedMessage {
val req = PublishRequest.parseFrom(data)
return PreparedMessage(req.envelopesList)
}
}

fun decodedMessage(): DecodedMessage =
conversation.decode(messageEnvelope)

fun send() {
onSend()
fun toSerializedData(): ByteArray {
val req = PublishRequest.newBuilder()
.addAllEnvelopes(envelopes)
.build()
return req.toByteArray()
}

val messageId: String
get() = Hash.sha256(messageEnvelope.message.toByteArray()).toHex()
get() = Hash.sha256(envelopes.first().message.toByteArray()).toHex()

val conversationTopic: String
get() = envelopes.first().contentTopic
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.xmtp.android.library

import com.google.protobuf.kotlin.toByteStringUtf8
import org.junit.Assert.assertEquals
import org.junit.Test
import org.xmtp.android.library.messages.Envelope

class PreparedMessageTest {

@Test
fun testSerializing() {
val original = PreparedMessage(
listOf(
Envelope.newBuilder().apply {
contentTopic = "topic1"
timestampNs = 1234
message = "abc123".toByteStringUtf8()
}.build(),
Envelope.newBuilder().apply {
contentTopic = "topic2"
timestampNs = 5678
message = "def456".toByteStringUtf8()
}.build(),
)
)
val serialized = original.toSerializedData()
val unserialized = PreparedMessage.fromSerializedData(serialized)
assertEquals(original, unserialized)
}
}

0 comments on commit 0976446

Please sign in to comment.