diff --git a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt index 7696b4d92..15f221f67 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/ClientTest.kt @@ -214,7 +214,7 @@ class ClientTest { runBlocking { client.conversations.newGroup(listOf(client2.address)) - client.conversations.syncGroups() + client.conversations.syncConversations() assertEquals(client.conversations.listGroups().size, 1) } @@ -233,7 +233,7 @@ class ClientTest { ) } runBlocking { - client.conversations.syncGroups() + client.conversations.syncConversations() assertEquals(client.conversations.listGroups().size, 0) } } @@ -422,7 +422,7 @@ class ClientTest { runBlocking { boClient.conversations.newGroup(listOf(alixClient.address)) - boClient.conversations.syncGroups() + boClient.conversations.syncConversations() } runBlocking { diff --git a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt index 1b8ecdf30..d15ba7603 100644 --- a/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt +++ b/library/src/androidTest/java/org/xmtp/android/library/CodecTest.kt @@ -75,99 +75,4 @@ class CodecTest { assertEquals("Error: This app does not support numbers.", messages[0].fallbackContent) } } - - @Test - @Ignore("Flaky: CI") - fun testCanGetPushInfoBeforeDecoded() { - val codec = NumberCodec() - Client.register(codec = codec) - val fixtures = fixtures() - val aliceClient = fixtures.aliceClient - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.bob.walletAddress) - } - runBlocking { - aliceConversation.send( - content = 3.14, - options = SendOptions(contentType = codec.contentType), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assert(messages.isNotEmpty()) - - val message = MessageV2Builder.buildEncode( - client = aliceClient, - encodedContent = messages[0].encodedContent, - topic = aliceConversation.topic, - keyMaterial = aliceConversation.keyMaterial!!, - codec = codec, - ) - - assertEquals(false, message.shouldPush) - assertEquals(true, message.senderHmac?.isNotEmpty()) - } - - @Test - fun testReturnsAllHMACKeys() { - val alix = PrivateKeyBuilder() - val clientOptions = - ClientOptions(api = ClientOptions.Api(env = XMTPEnvironment.LOCAL, isSecure = false)) - val alixClient = runBlocking { Client().create(alix, clientOptions) } - val conversations = mutableListOf() - repeat(5) { - val account = PrivateKeyBuilder() - val client = runBlocking { Client().create(account, clientOptions) } - runBlocking { - conversations.add( - alixClient.conversations.newConversation( - client.address, - context = InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi") - ) - ) - } - } - - val thirtyDayPeriodsSinceEpoch = Instant.now().epochSecond / 60 / 60 / 24 / 30 - - val hmacKeys = alixClient.conversations.getHmacKeys() - - val topics = hmacKeys.hmacKeysMap.keys - conversations.forEach { convo -> - assertTrue(topics.contains(convo.topic)) - } - - val topicHmacs = mutableMapOf() - val headerBytes = ByteArray(10) - - conversations.forEach { conversation -> - val topic = conversation.topic - val payload = TextCodec().encode(content = "Hello, world!") - - val message = MessageV2Builder.buildEncode( - client = alixClient, - encodedContent = payload, - topic = topic, - keyMaterial = headerBytes, - codec = TextCodec() - ) - - val keyMaterial = conversation.keyMaterial - val info = "$thirtyDayPeriodsSinceEpoch-${alixClient.address}" - val key = Crypto.deriveKey(keyMaterial!!, ByteArray(0), info.toByteArray()) - val hmac = Crypto.calculateMac(key, headerBytes) - - topicHmacs[topic] = hmac - } - - hmacKeys.hmacKeysMap.forEach { (topic, hmacData) -> - hmacData.valuesList.forEachIndexed { idx, hmacKeyThirtyDayPeriod -> - val valid = verifyHmacSignature( - hmacKeyThirtyDayPeriod.hmacKey.toByteArray(), - topicHmacs[topic]!!, - headerBytes - ) - assertTrue(valid == (idx == 1)) - } - } - } } diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt deleted file mode 100644 index c520796b7..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationTest.kt +++ /dev/null @@ -1,951 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.protobuf.kotlin.toByteString -import com.google.protobuf.kotlin.toByteStringUtf8 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertThrows -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.web3j.crypto.Hash -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageHeaderV2Builder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 -import org.xmtp.android.library.messages.SignedContentBuilder -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.recoverWalletSignerPublicKey -import org.xmtp.android.library.messages.sign -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.toSignedPublicKeyBundle -import org.xmtp.android.library.messages.toV2 -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.InvitationV1.Context -import java.nio.charset.StandardCharsets -import java.util.Date - -@RunWith(AndroidJUnit4::class) -class ConversationTest { - lateinit var aliceWallet: PrivateKeyBuilder - lateinit var bobWallet: PrivateKeyBuilder - lateinit var alice: PrivateKey - lateinit var aliceClient: Client - lateinit var bob: PrivateKey - lateinit var bobClient: Client - lateinit var fixtures: Fixtures - - @Before - fun setUp() { - fixtures = fixtures() - aliceWallet = fixtures.aliceAccount - alice = fixtures.alice - bobWallet = fixtures.bobAccount - bob = fixtures.bob - aliceClient = fixtures.aliceClient - bobClient = fixtures.bobClient - } - - @Test - fun testDoesNotAllowConversationWithSelf() { - val client = runBlocking { Client().create(account = aliceWallet) } - assertThrows("Recipient is sender", XMTPException::class.java) { - runBlocking { client.conversations.newConversation(alice.walletAddress) } - } - } - - @Test - fun testCanFindExistingV1Conversation() { - val encoder = TextCodec() - val encodedContent = encoder.encode(content = "hi alice") - // Get a date that's roughly two weeks ago to test with - val someTimeAgo = Date(System.currentTimeMillis() - 2_000_000) - val messageV1 = MessageV1Builder.buildEncode( - sender = bobClient.privateKeyBundleV1!!, - recipient = aliceClient.privateKeyBundleV1?.toPublicKeyBundle()!!, - message = encodedContent.toByteArray(), - timestamp = someTimeAgo, - ) - // Overwrite contact as legacy - runBlocking { - bobClient.publishUserContact(legacy = true) - aliceClient.publishUserContact(legacy = true) - } - runBlocking { - bobClient.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(bob.walletAddress), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(alice.walletAddress), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.directMessageV1( - bob.walletAddress, - alice.walletAddress, - ), - timestamp = someTimeAgo, - message = MessageBuilder.buildFromMessageV1(v1 = messageV1).toByteArray(), - ), - ), - ) - } - var conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - assertEquals(conversation.peerAddress, bob.walletAddress) - assertEquals(conversation.createdAt, someTimeAgo) - conversation = runBlocking { bobClient.conversations.newConversation(alice.walletAddress) } - assertEquals(conversation.peerAddress, alice.walletAddress) - assertEquals(conversation.createdAt, someTimeAgo) - } - - @Test - fun testCanLoadV1Messages() { - // Overwrite contact as legacy so we can get v1 - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - - runBlocking { bobConversation.send(content = "hey alice") } - runBlocking { bobConversation.send(content = "hey alice again") } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(2, messages.size) - assertEquals("hey alice", messages[1].body) - assertEquals(bobWallet.address, messages[1].senderAddress) - } - - @Test - fun testCanLoadV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - runBlocking { bobConversation.send(content = "hey alice") } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hey alice", messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testVerifiesV2MessageSignature() { - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - context = InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - - val codec = TextCodec() - val originalContent = codec.encode(content = "hello") - val tamperedContent = codec.encode(content = "this is a fake") - val originalPayload = originalContent.toByteArray() - val tamperedPayload = tamperedContent.toByteArray() - val date = Date() - val header = MessageHeaderV2Builder.buildFromTopic(aliceConversation.topic, created = date) - val headerBytes = header.toByteArray() - val digest = Hash.sha256(headerBytes + tamperedPayload) - val preKey = aliceClient.keys?.preKeysList?.get(0) - val signature = preKey?.sign(digest) - val bundle = aliceClient.privateKeyBundleV1?.toV2()?.getPublicKeyBundle() - val signedContent = SignedContentBuilder.builderFromPayload( - payload = originalPayload, - sender = bundle, - signature = signature, - ) - val signedBytes = signedContent.toByteArray() - val ciphertext = Crypto.encrypt( - aliceConversation.keyMaterial!!, - signedBytes, - additionalData = headerBytes, - ) - val thirtyDayPeriodsSinceEpoch = - (Date().time / 1000 / 60 / 60 / 24 / 30).toInt() - val info = "$thirtyDayPeriodsSinceEpoch-${aliceClient.address}" - val infoEncoded = info.toByteStringUtf8().toByteArray() - val senderHmacGenerated = - Crypto.calculateMac( - Crypto.deriveKey(aliceConversation.keyMaterial!!, ByteArray(0), infoEncoded), - headerBytes - ) - val tamperedMessage = - MessageV2Builder.buildFromCipherText( - headerBytes = headerBytes, - ciphertext = ciphertext, - senderHmac = senderHmacGenerated, - shouldPush = codec.shouldPush("this is a fake"), - ) - val tamperedEnvelope = EnvelopeBuilder.buildFromString( - topic = aliceConversation.topic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = tamperedMessage.messageV2) - .toByteArray(), - ) - runBlocking { aliceClient.publish(envelopes = listOf(tamperedEnvelope)) } - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - assertThrows("Invalid signature", XMTPException::class.java) { - bobConversation.decode(tamperedEnvelope) - } - // But it should be properly discarded from the message listing. - runBlocking { - assertEquals(0, bobConversation.messages().size) - } - } - - @Test - fun testCanSendGzipCompressedV1Messages() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - runBlocking { - bobConversation.send( - text = MutableList(1000) { "A" }.toString(), - sendOptions = SendOptions(compression = EncodedContentCompression.GZIP), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].content()) - } - - @Test - fun testCanSendDeflateCompressedV1Messages() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - runBlocking { - bobConversation.send( - content = MutableList(1000) { "A" }.toString(), - options = SendOptions(compression = EncodedContentCompression.DEFLATE), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].content()) - } - - @Test - fun testCanSendGzipCompressedV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - runBlocking { - bobConversation.send( - text = MutableList(1000) { "A" }.toString(), - sendOptions = SendOptions(compression = EncodedContentCompression.GZIP), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testCanSendDeflateCompressedV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - aliceWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bobWallet.address, - InvitationV1ContextBuilder.buildFromConversation(conversationId = "hi"), - ) - } - runBlocking { - bobConversation.send( - content = MutableList(1000) { "A" }.toString(), - options = SendOptions(compression = EncodedContentCompression.DEFLATE), - ) - } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals(MutableList(1000) { "A" }.toString(), messages[0].body) - assertEquals(bobWallet.address, messages[0].senderAddress) - } - - @Test - fun testEndToEndConversation() { - val fakeContactWallet = PrivateKeyBuilder() - val fakeContactClient = runBlocking { Client().create(account = fakeContactWallet) } - runBlocking { fakeContactClient.publishUserContact() } - val fakeWallet = PrivateKeyBuilder() - val client = runBlocking { Client().create(account = fakeWallet) } - val contact = client.getUserContact(peerAddress = fakeContactWallet.address)!! - assertEquals(contact.walletAddress, fakeContactWallet.address) - val created = Date() - val invitationContext = Invitation.InvitationV1.Context.newBuilder().also { - it.conversationId = "https://example.com/1" - }.build() - val invitationv1 = InvitationV1.newBuilder().build().createDeterministic( - sender = client.keys, - recipient = fakeContactClient.keys.getPublicKeyBundle(), - context = invitationContext, - ) - val senderBundle = client.privateKeyBundleV1?.toV2() - assertEquals( - senderBundle?.identityKey?.publicKey?.recoverWalletSignerPublicKey()?.walletAddress, - fakeWallet.address, - ) - val invitation = SealedInvitationBuilder.buildFromV1( - sender = client.privateKeyBundleV1!!.toV2(), - recipient = contact.toSignedPublicKeyBundle(), - created = created, - invitation = invitationv1, - ) - val inviteHeader = invitation.v1.header - assertEquals(inviteHeader.sender.walletAddress, fakeWallet.address) - assertEquals(inviteHeader.recipient.walletAddress, fakeContactWallet.address) - val header = SealedInvitationHeaderV1.parseFrom(invitation.v1.headerBytes) - val conversation = - ConversationV2.create(client = client, invitation = invitationv1, header = header) - assertEquals(fakeContactWallet.address, conversation.peerAddress) - - runBlocking { conversation.send(content = "hello world") } - - val conversationList = runBlocking { client.conversations.list() } - val recipientConversation = conversationList.lastOrNull() - - val messages = runBlocking { recipientConversation?.messages() } - val message = messages?.firstOrNull() - if (message != null) { - assertEquals("hello world", message.body) - } - } - - @Test - @Ignore("Rust seems to be Flaky with V1") - fun testCanPaginateV1Messages() { - // Overwrite contact as legacy so we can get v1 - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(alice.walletAddress) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - - val date = Date() - date.time = date.time - 1000000 - runBlocking { bobConversation.send(text = "hey alice 1", sentAt = date) } - runBlocking { bobConversation.send(text = "hey alice 2") } - runBlocking { bobConversation.send(text = "hey alice 3") } - val messages = runBlocking { aliceConversation.messages(limit = 1) } - assertEquals(1, messages.size) - assertEquals("hey alice 3", messages[0].body) - } - - @Test - fun testCanPaginateV2Messages() { - val bobConversation = runBlocking { - bobClient.conversations.newConversation( - alice.walletAddress, - context = InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - val aliceConversation = runBlocking { - aliceClient.conversations.newConversation( - bob.walletAddress, - context = InvitationV1ContextBuilder.buildFromConversation("hi"), - ) - } - val date = Date() - date.time = date.time - 1000000 - runBlocking { - bobConversation.send(text = "hey alice 1", sentAt = date) - bobConversation.send(text = "hey alice 2") - bobConversation.send(text = "hey alice 3") - val messages = aliceConversation.messages(limit = 1) - assertEquals(1, messages.size) - assertEquals("hey alice 3", messages[0].body) - val messages2 = aliceConversation.messages(limit = 1, after = date) - assertEquals(1, messages2.size) - assertEquals("hey alice 3", messages2[0].body) - val messagesAsc = - aliceConversation.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_ASCENDING) - assertEquals("hey alice 1", messagesAsc[0].body) - val messagesDesc = - aliceConversation.messages(direction = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING) - assertEquals("hey alice 3", messagesDesc[0].body) - } - } - - @Test - fun testListBatchMessages() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.caro.walletAddress) - } - - runBlocking { bobConversation.send(text = "hey alice 1") } - runBlocking { bobConversation.send(text = "hey alice 2") } - runBlocking { steveConversation.send(text = "hey alice 3") } - val messages = runBlocking { - aliceClient.conversations.listBatchMessages( - listOf( - Pair(steveConversation.topic, null), - Pair(bobConversation.topic, null), - ), - ) - } - val isSteveOrBobConversation = { topic: String -> - (topic.equals(steveConversation.topic) || topic.equals(bobConversation.topic)) - } - assertEquals(3, messages.size) - assertTrue( - "isSteveOrBobConversation message 0", - isSteveOrBobConversation(messages[0].topic) - ) - assertTrue( - "isSteveOrBobConversation message 1", - isSteveOrBobConversation(messages[1].topic) - ) - assertTrue( - "isSteveOrBobConversation message 2", - isSteveOrBobConversation(messages[2].topic) - ) - } - - @Test - fun testListBatchDecryptedMessages() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = runBlocking { - aliceClient.conversations.newConversation(fixtures.caro.walletAddress) - } - - runBlocking { - bobConversation.send(text = "hey alice 1") - bobConversation.send(text = "hey alice 2") - steveConversation.send(text = "hey alice 3") - } - val messages = runBlocking { - aliceClient.conversations.listBatchDecryptedMessages( - listOf( - Pair(steveConversation.topic, null), - Pair(bobConversation.topic, null), - ), - ) - } - assertEquals(3, messages.size) - } - - @Test - fun testListBatchMessagesWithPagination() { - val bobConversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val steveConversation = - runBlocking { aliceClient.conversations.newConversation(fixtures.caro.walletAddress) } - - runBlocking { - bobConversation.send(text = "hey alice 1 bob") - steveConversation.send(text = "hey alice 1 steve") - } - - Thread.sleep(100) - val date = Date() - - runBlocking { - bobConversation.send(text = "hey alice 2 bob") - bobConversation.send(text = "hey alice 3 bob") - steveConversation.send(text = "hey alice 2 steve") - steveConversation.send(text = "hey alice 3 steve") - } - - val messages = runBlocking { - aliceClient.conversations.listBatchMessages( - listOf( - Pair(steveConversation.topic, Pagination(after = date)), - Pair(bobConversation.topic, Pagination(after = date)), - ), - ) - } - - assertEquals(4, messages.size) - } - - @Test - fun testImportV1ConversationFromJS() { - val jsExportJSONData = - (""" { "version": "v1", "peerAddress": "0x5DAc8E2B64b8523C11AF3e5A2E087c2EA9003f14", "createdAt": "2022-09-20T09:32:50.329Z" } """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x5DAc8E2B64b8523C11AF3e5A2E087c2EA9003f14") - } - - @Test - fun testImportV2ConversationFromJS() { - val jsExportJSONData = - (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z","context":{"conversationId":"pat/messageid","metadata":{}}} """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x436D906d1339fC4E951769b1699051f020373D04") - } - - @Test - fun testImportV2ConversationWithNoContextFromJS() { - val jsExportJSONData = - (""" {"version":"v2","topic":"/xmtp/0/m-2SkdN5Qa0ZmiFI5t3RFbfwIS-OLv5jusqndeenTLvNg/proto","keyMaterial":"ATA1L0O2aTxHmskmlGKCudqfGqwA1H+bad3W/GpGOr8=","peerAddress":"0x436D906d1339fC4E951769b1699051f020373D04","createdAt":"2023-01-26T22:58:45.068Z"} """).toByteArray( - StandardCharsets.UTF_8, - ) - val conversation = aliceClient.importConversation(jsExportJSONData) - assertEquals(conversation.peerAddress, "0x436D906d1339fC4E951769b1699051f020373D04") - } - - @Test - fun testCanStreamConversationsV2() { - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - bobClient.conversations.stream() - .collect { message -> - allMessages.add(message.topic) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - runBlocking { - bobClient.conversations.newConversation(alice.walletAddress) - } - - Thread.sleep(1000) - - assertEquals(1, allMessages.size) - - job.cancel() - } - - @Test - fun testStreamingMessagesFromV1Conversation() { - // Overwrite contact as legacy - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - conversation.streamMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 5) { - runBlocking { conversation.send(text = "Message $i") } - Thread.sleep(1000) - } - - assertEquals(allMessages.size, 5) - job.cancel() - } - - @Test - fun testStreamingMessagesFromV2Conversations() { - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - conversation.streamMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - Thread.sleep(2500) - - for (i in 0 until 5) { - runBlocking { conversation.send(text = "Message $i") } - Thread.sleep(1000) - } - - assertEquals(allMessages.size, 5) - job.cancel() - } - - @Test - fun testV2RejectsSpoofedContactBundles() { - val topic = "/xmtp/0/m-Gdb7oj5nNdfZ3MJFLAcS4WTABgr6al1hePy6JV1-QUE/proto" - val envelopeMessage = - com.google.crypto.tink.subtle.Base64.decode("Er0ECkcIwNruhKLgkKUXEjsveG10cC8wL20tR2RiN29qNW5OZGZaM01KRkxBY1M0V1RBQmdyNmFsMWhlUHk2SlYxLVFVRS9wcm90bxLxAwruAwognstLoG6LWgiBRsWuBOt+tYNJz+CqCj9zq6hYymLoak8SDFsVSy+cVAII0/r3sxq7A/GCOrVtKH6J+4ggfUuI5lDkFPJ8G5DHlysCfRyFMcQDIG/2SFUqSILAlpTNbeTC9eSI2hUjcnlpH9+ncFcBu8StGfmilVGfiADru2fGdThiQ+VYturqLIJQXCHO2DkvbbUOg9xI66E4Hj41R9vE8yRGeZ/eRGRLRm06HftwSQgzAYf2AukbvjNx/k+xCMqti49Qtv9AjzxVnwttLiA/9O+GDcOsiB1RQzbZZzaDjQ/nLDTF6K4vKI4rS9QwzTJqnoCdp0SbMZFf+KVZpq3VWnMGkMxLW5Fr6gMvKny1e1LAtUJSIclI/1xPXu5nsKd4IyzGb2ZQFXFQ/BVL9Z4CeOZTsjZLGTOGS75xzzGHDtKohcl79+0lgIhAuSWSLDa2+o2OYT0fAjChp+qqxXcisAyrD5FB6c9spXKfoDZsqMV/bnCg3+udIuNtk7zBk7jdTDMkofEtE3hyIm8d3ycmxKYOakDPqeo+Nk1hQ0ogxI8Z7cEoS2ovi9+rGBMwREzltUkTVR3BKvgV2EOADxxTWo7y8WRwWxQ+O6mYPACsiFNqjX5Nvah5lRjihphQldJfyVOG8Rgf4UwkFxmI") - val keyMaterial = - com.google.crypto.tink.subtle.Base64.decode("R0BBM5OPftNEuavH/991IKyJ1UqsgdEG4SrdxlIG2ZY=") - - val conversation = ConversationV2( - topic = topic, - keyMaterial = keyMaterial, - context = Context.newBuilder().build(), - peerAddress = "0x2f25e33D7146602Ec08D43c1D6B1b65fc151A677", - client = aliceClient, - header = Invitation.SealedInvitationHeaderV1.newBuilder().build(), - ) - val envelope = EnvelopeBuilder.buildFromString( - topic = topic, - timestamp = Date(), - message = envelopeMessage, - ) - assertThrows("pre-key not signed by identity key", XMTPException::class.java) { - conversation.decodeEnvelope(envelope) - } - } - - @Test - fun testCanPrepareV1Message() { - // Publish legacy contacts so we can get v1 conversations - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - assertEquals(conversation.version, Conversation.Version.V1) - val preparedMessage = conversation.prepareMessage(content = "hi") - val messageID = preparedMessage.messageId - runBlocking { conversation.send(prepared = preparedMessage) } - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testCanPrepareV2Message() { - val conversation = - runBlocking { aliceClient.conversations.newConversation(bob.walletAddress) } - val preparedMessage = conversation.prepareMessage(content = "hi") - val messageID = preparedMessage.messageId - runBlocking { conversation.send(prepared = preparedMessage) } - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testCanSendPreparedMessageWithoutConversation() { - val conversation = - runBlocking { 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. - runBlocking { aliceClient.publish(envelopes = preparedMessage.envelopes) } - - val messages = runBlocking { conversation.messages() } - val message = messages[0] - assertEquals("hi", message.body) - assertEquals(message.id, messageID) - } - - @Test - fun testFetchConversation() { - // Generated from JS script - val ints = arrayOf( - 31, - 116, - 198, - 193, - 189, - 122, - 19, - 254, - 191, - 189, - 211, - 215, - 255, - 131, - 171, - 239, - 243, - 33, - 4, - 62, - 143, - 86, - 18, - 195, - 251, - 61, - 128, - 90, - 34, - 126, - 219, - 236, - ) - val bytes = - ints.foldIndexed(ByteArray(ints.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - - val key = PrivateKey.newBuilder().also { - it.secp256K1 = it.secp256K1.toBuilder().also { builder -> - builder.bytes = bytes.toByteString() - }.build() - it.publicKey = it.publicKey.toBuilder().also { builder -> - builder.secp256K1Uncompressed = - builder.secp256K1Uncompressed.toBuilder().also { keyBuilder -> - keyBuilder.bytes = - KeyUtil.addUncompressedByte(KeyUtil.getPublicKey(bytes)).toByteString() - }.build() - }.build() - }.build() - - val client = runBlocking { Client().create(account = PrivateKeyBuilder(key)) } - runBlocking { - val conversations = client.conversations.list() - assertEquals(1, conversations.size) - val topic = conversations[0].topic - val conversation = client.fetchConversation(topic) - assertEquals(conversations[0].topic, conversation?.topic) - assertEquals(conversations[0].peerAddress, conversation?.peerAddress) - - val noConversation = client.fetchConversation("invalid_topic") - assertEquals(null, noConversation) - } - } - - @Test - fun testCanSendEncodedContentV1Message() { - fixtures.publishLegacyContact(client = bobClient) - fixtures.publishLegacyContact(client = aliceClient) - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val aliceConversation = - runBlocking { aliceClient.conversations.newConversation(bobWallet.address) } - val encodedContent = TextCodec().encode(content = "hi") - runBlocking { bobConversation.send(encodedContent = encodedContent) } - val messages = runBlocking { aliceConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hi", messages[0].content()) - } - - @Test - fun testCanSendEncodedContentV2Message() { - val bobConversation = - runBlocking { bobClient.conversations.newConversation(aliceWallet.address) } - val encodedContent = TextCodec().encode(content = "hi") - runBlocking { bobConversation.send(encodedContent = encodedContent) } - val messages = runBlocking { bobConversation.messages() } - assertEquals(1, messages.size) - assertEquals("hi", messages[0].content()) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanHaveConsentState() { - runBlocking { - val bobConversation = - runBlocking { bobClient.conversations.newConversation(alice.walletAddress, null) } - Thread.sleep(1000) - val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED - // Conversations you start should start as allowed - assertTrue("Bob convo should be allowed", isAllowed) - assertTrue( - "Bob contacts should be allowed", - bobClient.contacts.isAllowed(alice.walletAddress) - ) - - runBlocking { - bobClient.contacts.deny(listOf(alice.walletAddress)) - bobClient.contacts.refreshConsentList() - } - val isDenied = bobConversation.consentState() == ConsentState.DENIED - assertEquals(bobClient.contacts.consentList.entries.size, 1) - assertTrue("Bob Conversation should be denied", isDenied) - - val aliceConversation = runBlocking { aliceClient.conversations.list()[0] } - val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN - - // Conversations started with you should start as unknown - assertTrue("Alice conversation should be unknown", isUnknown) - - runBlocking { aliceClient.contacts.allow(listOf(bob.walletAddress)) } - - val isBobAllowed = aliceConversation.consentState() == ConsentState.ALLOWED - assertTrue("Bob should be allowed from alice conversation", isBobAllowed) - - val aliceClient2 = runBlocking { Client().create(aliceWallet) } - val aliceConversation2 = runBlocking { aliceClient2.conversations.list()[0] } - - runBlocking { aliceClient2.contacts.refreshConsentList() } - - // Allow state should sync across clients - val isBobAllowed2 = aliceConversation2.consentState() == ConsentState.ALLOWED - - assertTrue("Bob should be allowed from conversation 2", isBobAllowed2) - } - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanHaveImplicitConsentOnMessageSend() { - runBlocking { - val bobConversation = bobClient.conversations.newConversation(alice.walletAddress, null) - Thread.sleep(1000) - val isAllowed = bobConversation.consentState() == ConsentState.ALLOWED - - // Conversations you start should start as allowed - assertTrue("Bob convo should be allowed", isAllowed) - - val aliceConversation = aliceClient.conversations.list()[0] - val isUnknown = aliceConversation.consentState() == ConsentState.UNKNOWN - - // Conversations you receive should start as unknown - assertTrue("Alice convo should be unknown", isUnknown) - - aliceConversation.send(content = "hey bob") - aliceClient.contacts.refreshConsentList() - val isNowAllowed = aliceConversation.consentState() == ConsentState.ALLOWED - - // Conversations you send a message to get marked as allowed - assertTrue("Should now be allowed", isNowAllowed) - } - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testCanPublishMultipleAddressConsentState() { - runBlocking { - val bobConversation = bobClient.conversations.newConversation(alice.walletAddress) - val caroConversation = - bobClient.conversations.newConversation(fixtures.caro.walletAddress) - bobClient.contacts.refreshConsentList() - Thread.sleep(1000) - assertEquals(bobClient.contacts.consentList.entries.size, 2) - assertTrue( - "Bob convo should be allowed", - bobConversation.consentState() == ConsentState.ALLOWED - ) - assertTrue( - "Caro convo should be allowed", - caroConversation.consentState() == ConsentState.ALLOWED - ) - bobClient.contacts.deny(listOf(alice.walletAddress, fixtures.caro.walletAddress)) - assertEquals(bobClient.contacts.consentList.entries.size, 2) - assertTrue( - "Bob convo should be denied", - bobConversation.consentState() == ConsentState.DENIED - ) - assertTrue( - "Caro convo should be denied", - caroConversation.consentState() == ConsentState.DENIED - ) - } - } - - @Test - fun testCanValidateTopicsInsideConversation() { - val validId = "sdfsadf095b97a9284dcd82b2274856ccac8a21de57bebe34e7f9eeb855fb21126d3b8f" - - // Creation of all known types of topics - val privateStore = Topic.userPrivateStoreKeyBundle(validId).description - val contact = Topic.contact(validId).description - val userIntro = Topic.userIntro(validId).description - val userInvite = Topic.userInvite(validId).description - val directMessageV1 = Topic.directMessageV1(validId, "sd").description - val directMessageV2 = Topic.directMessageV2(validId).description - val preferenceList = Topic.preferenceList(validId).description - - // check if validation of topics accepts all types - assertTrue("Private Store should be valid topic", Topic.isValidTopic(privateStore)) - assertTrue("Contact should be valid topic", Topic.isValidTopic(contact)) - assertTrue("User Intro should be valid topic", Topic.isValidTopic(userIntro)) - assertTrue("userInvite should be valid topic", Topic.isValidTopic(userInvite)) - assertTrue("directMessageV1 should be valid topic", Topic.isValidTopic(directMessageV1)) - assertTrue("directMessageV2 should be valid topic", Topic.isValidTopic(directMessageV2)) - assertTrue("preferenceList should be valid topic", Topic.isValidTopic(preferenceList)) - } - - @Test - fun testCannotValidateTopicsInsideConversation() { - val invalidId = "��\\u0005�!\\u000b���5\\u00001\\u0007�蛨\\u001f\\u00172��.����K9K`�" - - // Creation of all known types of topics - val privateStore = Topic.userPrivateStoreKeyBundle(invalidId).description - val contact = Topic.contact(invalidId).description - val userIntro = Topic.userIntro(invalidId).description - val userInvite = Topic.userInvite(invalidId).description - val directMessageV1 = Topic.directMessageV1(invalidId, "sd").description - val directMessageV2 = Topic.directMessageV2(invalidId).description - val preferenceList = Topic.preferenceList(invalidId).description - - // check if validation of topics no accept all types with invalid topic - assertFalse(Topic.isValidTopic(privateStore)) - assertFalse(Topic.isValidTopic(contact)) - assertFalse(Topic.isValidTopic(userIntro)) - assertFalse(Topic.isValidTopic(userInvite)) - assertFalse(Topic.isValidTopic(directMessageV1)) - assertFalse(Topic.isValidTopic(directMessageV2)) - assertFalse(Topic.isValidTopic(preferenceList)) - } -} diff --git a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt b/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt deleted file mode 100644 index 3ee56c5e2..000000000 --- a/library/src/androidTest/java/org/xmtp/android/library/ConversationsTest.kt +++ /dev/null @@ -1,266 +0,0 @@ -package org.xmtp.android.library - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import org.xmtp.android.library.codecs.TextCodec -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1 -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.PrivateKey -import org.xmtp.android.library.messages.PrivateKeyBuilder -import org.xmtp.android.library.messages.SealedInvitationBuilder -import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.consentProofText -import org.xmtp.android.library.messages.createDeterministic -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.rawDataWithNormalizedRecovery -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.contents.Invitation -import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import java.lang.Thread.sleep -import java.util.Date - -@RunWith(AndroidJUnit4::class) -class ConversationsTest { - lateinit var alixWallet: PrivateKeyBuilder - lateinit var boWallet: PrivateKeyBuilder - lateinit var alix: PrivateKey - lateinit var alixClient: Client - lateinit var bo: PrivateKey - lateinit var boClient: Client - lateinit var caroClient: Client - lateinit var fixtures: Fixtures - - @Before - fun setUp() { - fixtures = fixtures() - alixWallet = fixtures.aliceAccount - alix = fixtures.alice - boWallet = fixtures.bobAccount - bo = fixtures.bob - alixClient = fixtures.aliceClient - boClient = fixtures.bobClient - caroClient = fixtures.caroClient - } - - @Test - fun testCanGetConversationFromIntroEnvelope() { - val created = Date() - val newWallet = PrivateKeyBuilder() - val newClient = runBlocking { Client().create(account = newWallet) } - val message = MessageV1Builder.buildEncode( - sender = newClient.v1keys, - recipient = fixtures.aliceClient.v1keys.toPublicKeyBundle(), - message = TextCodec().encode(content = "hello").toByteArray(), - timestamp = created - ) - val envelope = EnvelopeBuilder.buildFromTopic( - topic = Topic.userIntro(alixClient.address), - timestamp = created, - message = MessageBuilder.buildFromMessageV1(v1 = message).toByteArray() - ) - val conversation = alixClient.conversations.fromIntro(envelope = envelope) - assertEquals(conversation.peerAddress, newWallet.address) - assertEquals(conversation.createdAt.time, created.time) - } - - @Test - fun testCanGetConversationFromInviteEnvelope() { - val created = Date() - val newWallet = PrivateKeyBuilder() - val newClient = runBlocking { Client().create(account = newWallet) } - val invitation = InvitationV1.newBuilder().build().createDeterministic( - sender = newClient.keys, - recipient = alixClient.keys.getPublicKeyBundle() - ) - val sealed = SealedInvitationBuilder.buildFromV1( - sender = newClient.keys, - recipient = alixClient.keys.getPublicKeyBundle(), - created = created, - invitation = invitation - ) - val peerAddress = alix.walletAddress - val envelope = EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite(peerAddress), - timestamp = created, - message = sealed.toByteArray() - ) - val conversation = alixClient.conversations.fromInvite(envelope = envelope) - assertEquals(conversation.peerAddress, newWallet.address) - assertEquals(conversation.createdAt.time, created.time) - } - - @Test - fun testStreamAllMessages() { - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address) } - - // Record message stream across all conversations - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { boConversation.send(text = "Message $i") } - sleep(1000) - } - assertEquals(5, allMessages.size) - - val caroConversation = - runBlocking { caroClient.conversations.newConversation(alixClient.address) } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { caroConversation.send(text = "Message $i") } - sleep(1000) - } - - assertEquals(10, allMessages.size) - - job.cancel() - - CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - sleep(2500) - - for (i in 0 until 5) { - runBlocking { boConversation.send(text = "Message $i") } - sleep(1000) - } - - assertEquals(15, allMessages.size) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testStreamTimeOutsAllMessages() { - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address) } - - // Record message stream across all conversations - val allMessages = mutableListOf() - - val job = CoroutineScope(Dispatchers.IO).launch { - try { - alixClient.conversations.streamAllMessages().collect { message -> - allMessages.add(message) - } - } catch (e: Exception) { - } - } - sleep(2500) - - runBlocking { boConversation.send(text = "first message") } - sleep(2000) - assertEquals(allMessages.size, 1) - sleep(121000) - runBlocking { boConversation.send(text = "second message") } - sleep(2000) - assertEquals(allMessages.size, 2) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testSendConversationWithConsentSignature() { - val timestamp = Date().time - val signatureClass = Signature.newBuilder().build() - val signatureText = signatureClass.consentProofText(boClient.address, timestamp) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - val boConversation = - runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) } - val alixConversations = runBlocking { - alixClient.conversations.list() - } - val alixConversation = alixConversations.find { - it.topic == boConversation.topic - } - assertNotNull("Alix Conversation should exist " + alixConversations.size, alixConversation) -// Commenting out for now, the signature being created is not valid - val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) } - assertTrue(isAllowed) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testNetworkConsentOverConsentProof() { - val timestamp = Date().time - val signatureText = Signature.newBuilder().build().consentProofText(boClient.address, timestamp) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - runBlocking { alixClient.contacts.deny(listOf(boClient.address)) } - val boConversation = runBlocking { boClient.conversations.newConversation(alixClient.address, null, consentProofPayload) } - val alixConversations = runBlocking { alixClient.conversations.list() } - val alixConversation = alixConversations.find { it.topic == boConversation.topic } - assertNotNull(alixConversation) - val isDenied = runBlocking { alixClient.contacts.isDenied(boClient.address) } - assertTrue(isDenied) - } - - @Test - @Ignore("TODO: Fix Flaky Test") - fun testConsentProofInvalidSignature() { - val timestamp = Date().time - val signatureText = - Signature.newBuilder().build().consentProofText(boClient.address, timestamp + 1) - val signature = runBlocking { alixWallet.sign(signatureText) } - val hex = signature.rawDataWithNormalizedRecovery.toHex() - val consentProofPayload = ConsentProofPayload.newBuilder().also { - it.signature = hex - it.timestamp = timestamp - it.payloadVersion = - Invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1 - }.build() - - val boConversation = runBlocking { - boClient.conversations.newConversation( - alixClient.address, - null, - consentProofPayload - ) - } - val alixConversations = runBlocking { alixClient.conversations.list() } - val alixConversation = alixConversations.find { it.topic == boConversation.topic } - assertNotNull("Alix conversation should exist" + alixConversations.size, alixConversation) - val isAllowed = runBlocking { alixClient.contacts.isAllowed(boClient.address) } - assertFalse("Should not be allowed", isAllowed) - } -} diff --git a/library/src/main/java/org/xmtp/android/library/Client.kt b/library/src/main/java/org/xmtp/android/library/Client.kt index ed72e2609..f91ba3920 100644 --- a/library/src/main/java/org/xmtp/android/library/Client.kt +++ b/library/src/main/java/org/xmtp/android/library/Client.kt @@ -593,17 +593,6 @@ class Client() { return client.subscribe(request, callback) } - suspend fun fetchConversation( - topic: String?, - includeGroups: Boolean = false, - ): Conversation? { - if (topic.isNullOrBlank()) return null - return conversations.list(includeGroups = includeGroups).firstOrNull { - it.topic == topic - } - } - - @Deprecated("Find now includes DMs and Groups", replaceWith = ReplaceWith("findConversation")) fun findGroup(groupId: String): Group? { val client = v3Client ?: throw XMTPException("Error no V3 client initialized") try { @@ -682,61 +671,6 @@ class Client() { publishUserContact(legacy = true) } - fun importConversation(conversationData: ByteArray): Conversation { - val gson = GsonBuilder().create() - val v2Export = gson.fromJson( - conversationData.toString(StandardCharsets.UTF_8), - ConversationV2Export::class.java, - ) - return try { - importV2Conversation(export = v2Export) - } catch (e: java.lang.Exception) { - val v1Export = gson.fromJson( - conversationData.toString(StandardCharsets.UTF_8), - ConversationV1Export::class.java, - ) - try { - importV1Conversation(export = v1Export) - } catch (e: java.lang.Exception) { - throw XMTPException("Invalid input data", e) - } - } - } - - fun importV2Conversation(export: ConversationV2Export): Conversation { - val keyMaterial = Base64.decode(export.keyMaterial) - return Conversation.V2( - ConversationV2( - topic = export.topic, - keyMaterial = keyMaterial, - context = InvitationV1ContextBuilder.buildFromConversation( - conversationId = export.context?.conversationId ?: "", - metadata = export.context?.metadata ?: mapOf(), - ), - peerAddress = export.peerAddress, - client = this, - header = SealedInvitationHeaderV1.newBuilder().build(), - ), - ) - } - - fun importV1Conversation(export: ConversationV1Export): Conversation { - val sentAt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Date.from(Instant.parse(export.createdAt)) - } else { - val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()) - df.timeZone = TimeZone.getTimeZone("UTC") - df.parse(export.createdAt) - } - return Conversation.V1( - ConversationV1( - client = this, - peerAddress = export.peerAddress, - sentAt = sentAt, - ), - ) - } - /** * Whether or not we can send messages to [address]. * @param peerAddress is the address of the client that you want to send messages diff --git a/library/src/main/java/org/xmtp/android/library/Conversation.kt b/library/src/main/java/org/xmtp/android/library/Conversation.kt index 5a701bb4f..26c6ca0e5 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversation.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversation.kt @@ -25,19 +25,14 @@ import java.util.Date * It attempts to give uniform shape to v1 and v2 conversations. */ sealed class Conversation { - data class V1(val conversationV1: ConversationV1) : Conversation() - data class V2(val conversationV2: ConversationV2) : Conversation() - data class Group(val group: org.xmtp.android.library.Group) : Conversation() data class Dm(val dm: org.xmtp.android.library.Dm) : Conversation() - enum class Version { V1, V2, GROUP, DM } + enum class Version { GROUP, DM } val version: Version get() { return when (this) { - is V1 -> Version.V1 - is V2 -> Version.V2 is Group -> Version.GROUP is Dm -> Version.DM } @@ -46,8 +41,6 @@ sealed class Conversation { val id: String get() { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.id is Dm -> dm.id } @@ -56,8 +49,6 @@ sealed class Conversation { val topic: String get() { return when (this) { - is V1 -> conversationV1.topic.description - is V2 -> conversationV2.topic is Group -> group.topic is Dm -> dm.topic } @@ -66,8 +57,6 @@ sealed class Conversation { val createdAt: Date get() { return when (this) { - is V1 -> conversationV1.sentAt - is V2 -> conversationV2.createdAt is Group -> group.createdAt is Dm -> dm.createdAt } @@ -75,8 +64,6 @@ sealed class Conversation { fun isCreator(): Boolean { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.isCreator() is Dm -> dm.isCreator() } @@ -84,8 +71,6 @@ sealed class Conversation { suspend fun members(): List { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.members() is Dm -> dm.members() } @@ -93,8 +78,6 @@ sealed class Conversation { suspend fun updateConsentState(state: ConsentState) { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.updateConsentState(state) is Dm -> dm.updateConsentState(state) } @@ -102,17 +85,13 @@ sealed class Conversation { suspend fun consentState(): ConsentState { return when (this) { - is V1 -> conversationV1.client.contacts.consentList.state(address = peerAddress) - is V2 -> conversationV2.client.contacts.consentList.state(address = peerAddress) is Group -> group.consentState() is Dm -> dm.consentState() } } - suspend fun prepareMessageV3(content: T, options: SendOptions? = null): String { + suspend fun prepareMessage(content: T, options: SendOptions? = null): String { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.prepareMessage(content, options) is Dm -> dm.prepareMessage(content, options) } @@ -120,8 +99,6 @@ sealed class Conversation { suspend fun send(content: T, options: SendOptions? = null): String { return when (this) { - is V1 -> conversationV1.send(content = content, options = options) - is V2 -> conversationV2.send(content = content, options = options) is Group -> group.send(content = content, options = options) is Dm -> dm.send(content = content, options = options) } @@ -129,8 +106,6 @@ sealed class Conversation { suspend fun send(text: String, sendOptions: SendOptions? = null, sentAt: Date? = null): String { return when (this) { - is V1 -> conversationV1.send(text = text, sendOptions, sentAt) - is V2 -> conversationV2.send(text = text, sendOptions, sentAt) is Group -> group.send(text) is Dm -> dm.send(text) } @@ -138,8 +113,6 @@ sealed class Conversation { suspend fun send(encodedContent: EncodedContent, options: SendOptions? = null): String { return when (this) { - is V1 -> conversationV1.send(encodedContent = encodedContent, options = options) - is V2 -> conversationV2.send(encodedContent = encodedContent, options = options) is Group -> group.send(encodedContent = encodedContent) is Dm -> dm.send(encodedContent = encodedContent) } @@ -147,8 +120,6 @@ sealed class Conversation { suspend fun sync() { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.sync() is Dm -> dm.sync() } @@ -174,21 +145,6 @@ sealed class Conversation { direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, ): List { return when (this) { - is V1 -> conversationV1.messages( - limit = limit, - before = before, - after = after, - direction = direction, - ) - - is V2 -> - conversationV2.messages( - limit = limit, - before = before, - after = after, - direction = direction, - ) - is Group -> { group.messages( limit = limit, @@ -209,28 +165,22 @@ sealed class Conversation { direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, ): List { return when (this) { - is V1 -> conversationV1.decryptedMessages(limit, before, after, direction) - is V2 -> conversationV2.decryptedMessages(limit, before, after, direction) is Group -> group.decryptedMessages(limit, before, after, direction) is Dm -> dm.decryptedMessages(limit, before, after, direction) } } - fun decryptV3( + fun decrypt( message: MessageV3, ): DecryptedMessage { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> message.decrypt() is Dm -> message.decrypt() } } - fun decodeV3(message: MessageV3): DecodedMessage { + fun decode(message: MessageV3): DecodedMessage { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> message.decode() is Dm -> message.decode() } @@ -238,8 +188,6 @@ sealed class Conversation { suspend fun processMessage(envelopeBytes: ByteArray): MessageV3 { return when (this) { - is V1 -> throw XMTPException("Only supported for V3") - is V2 -> throw XMTPException("Only supported for V3") is Group -> group.processMessage(envelopeBytes) is Dm -> dm.processMessage(envelopeBytes) } @@ -248,8 +196,6 @@ sealed class Conversation { val consentProof: ConsentProofPayload? get() { return when (this) { - is V1 -> return null - is V2 -> conversationV2.consentProof is Group -> return null is Dm -> return null } @@ -259,8 +205,6 @@ sealed class Conversation { val client: Client get() { return when (this) { - is V1 -> conversationV1.client - is V2 -> conversationV2.client is Group -> group.client is Dm -> dm.client } @@ -272,8 +216,6 @@ sealed class Conversation { */ fun streamMessages(): Flow { return when (this) { - is V1 -> conversationV1.streamMessages() - is V2 -> conversationV2.streamMessages() is Group -> group.streamMessages() is Dm -> dm.streamMessages() } @@ -281,162 +223,8 @@ sealed class Conversation { fun streamDecryptedMessages(): Flow { return when (this) { - is V1 -> conversationV1.streamDecryptedMessages() - is V2 -> conversationV2.streamDecryptedMessages() is Group -> group.streamDecryptedMessages() is Dm -> dm.streamDecryptedMessages() } } - - // ------- V1 V2 to be deprecated ------ - - fun decrypt( - envelope: Envelope, - ): DecryptedMessage { - return when (this) { - is V1 -> conversationV1.decrypt(envelope) - is V2 -> conversationV2.decrypt(envelope) - is Group -> throw XMTPException("Use decryptV3 instead") - is Dm -> throw XMTPException("Use decryptV3 instead") - } - } - - fun decode(envelope: Envelope): DecodedMessage { - return when (this) { - is V1 -> conversationV1.decode(envelope) - is V2 -> conversationV2.decodeEnvelope(envelope) - is Group -> throw XMTPException("Use decodeV3 instead") - is Dm -> throw XMTPException("Use decodeV3 instead") - } - } - - // This is the address of the peer that I am talking to. - val peerAddress: String - get() { - return when (this) { - is V1 -> conversationV1.peerAddress - is V2 -> conversationV2.peerAddress - is Group -> runBlocking { group.peerInboxIds().joinToString(",") } - is Dm -> dm.peerInboxId - } - } - - val peerAddresses: List - get() { - return when (this) { - is V1 -> listOf(conversationV1.peerAddress) - is V2 -> listOf(conversationV2.peerAddress) - is Group -> runBlocking { group.peerInboxIds() } - is Dm -> listOf(dm.peerInboxId) - } - } - - // This distinctly identifies between two addresses. - // Note: this will be empty for older v1 conversations. - val conversationId: String? - get() { - return when (this) { - is V1 -> null - is V2 -> conversationV2.context.conversationId - is Group -> null - is Dm -> null - } - } - - val keyMaterial: ByteArray? - get() { - return when (this) { - is V1 -> null - is V2 -> conversationV2.keyMaterial - is Group -> null - is Dm -> null - } - } - - /** - * This method is to create a TopicData object - * @return [TopicData] that contains all the information about the Topic, the conversation - * context and the necessary encryption data for it. - */ - 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() - - is Group -> throw XMTPException("Groups do not support topics") - is Dm -> throw XMTPException("DMs do not support topics") - } - } - - fun decodeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decode(envelope) - } catch (e: Exception) { - Log.d("CONVERSATION", "discarding message that failed to decode", e) - null - } - } - - fun prepareMessage(content: T, options: SendOptions? = null): PreparedMessage { - return when (this) { - is V1 -> conversationV1.prepareMessage(content = content, options = options) - is V2 -> conversationV2.prepareMessage(content = content, options = options) - is Group -> throw XMTPException("Use prepareMessageV3 instead") - is Dm -> throw XMTPException("Use prepareMessageV3 instead") - } - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions? = null, - ): PreparedMessage { - return when (this) { - is V1 -> conversationV1.prepareMessage( - encodedContent = encodedContent, - options = options - ) - - is V2 -> conversationV2.prepareMessage( - encodedContent = encodedContent, - options = options - ) - - is Group -> throw XMTPException("Use prepareMessageV3 instead") - is Dm -> throw XMTPException("Use prepareMessageV3 instead") - } - } - - suspend fun send(prepared: PreparedMessage): String { - return when (this) { - is V1 -> conversationV1.send(prepared = prepared) - is V2 -> conversationV2.send(prepared = prepared) - is Group -> throw XMTPException("Groups do not support sending prepared messages call sync instead") - is Dm -> throw XMTPException("DMs do not support sending prepared messages call sync instead") - } - } - - val clientAddress: String - get() { - return client.address - } - - fun streamEphemeral(): Flow { - return when (this) { - is V1 -> return conversationV1.streamEphemeral() - is V2 -> return conversationV2.streamEphemeral() - is Group -> throw XMTPException("Groups do not support ephemeral messages") - is Dm -> throw XMTPException("DMs do not support ephemeral messages") - } - } } diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt b/library/src/main/java/org/xmtp/android/library/ConversationV1.kt deleted file mode 100644 index 2d031b5fe..000000000 --- a/library/src/main/java/org/xmtp/android/library/ConversationV1.kt +++ /dev/null @@ -1,300 +0,0 @@ -package org.xmtp.android.library - -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import org.web3j.crypto.Hash -import org.xmtp.android.library.Util.Companion.envelopeFromFFi -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.compress -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Message -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV1Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.android.library.messages.Topic -import org.xmtp.android.library.messages.decrypt -import org.xmtp.android.library.messages.header -import org.xmtp.android.library.messages.sentAt -import org.xmtp.android.library.messages.toPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import uniffi.xmtpv3.FfiEnvelope -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import java.util.Date - -data class ConversationV1( - val client: Client, - val peerAddress: String, - val sentAt: Date, -) { - - val topic: Topic - get() = Topic.directMessageV1(client.address, peerAddress) - - /** - * Get the stream of all messages of the current [Client] - * @return Flow object of [DecodedMessage] that represents all the messages of the - * current [Client] as userInvite and userIntro - * @see Conversations.streamAllMessages - */ - fun streamMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decode(envelope = envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic.description), streamCallback) - awaitClose { launch { stream.end() } } - } - - /** - * This lists messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - * @see Conversation.messages - */ - suspend fun messages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val result = apiClient.envelopes(topic = topic.description, pagination = pagination) - - return result.mapNotNull { envelope -> - decodeOrNull(envelope = envelope) - } - } - - /** - * This lists decrypted messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - */ - suspend fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = - apiClient.envelopes( - topic = Topic.directMessageV1(client.address, peerAddress).description, - pagination = pagination, - ) - - return envelopes.map { decrypt(it) } - } - - /** - * This decrypts a message - * @param envelope Object that contains all the information of the encrypted message - * @return [DecryptedMessage] object - */ - fun decrypt(envelope: Envelope): DecryptedMessage { - try { - val message = Message.parseFrom(envelope.message) - val decrypted = message.v1.decrypt(client.privateKeyBundleV1) - - val encodedMessage = EncodedContent.parseFrom(decrypted) - val header = message.v1.header - - return DecryptedMessage( - id = generateId(envelope), - encodedContent = encodedMessage, - senderAddress = header.sender.walletAddress, - sentAt = message.v1.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decrypting message", e) - } - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object - */ - fun decode(envelope: Envelope): DecodedMessage { - try { - val decryptedMessage = decrypt(envelope) - - return DecodedMessage( - id = generateId(envelope), - client = client, - topic = envelope.contentTopic, - encodedContent = decryptedMessage.encodedContent, - senderAddress = decryptedMessage.senderAddress, - sent = decryptedMessage.sentAt, - ) - } catch (e: Exception) { - throw XMTPException("Error decoding message", e) - } - } - - private fun decodeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decode(envelope) - } catch (e: Exception) { - Log.d("CONV_V1", "discarding message that failed to decode", e) - null - } - } - - suspend fun send(text: String, options: SendOptions? = null): String { - return send(text = text, sendOptions = options, sentAt = null) - } - - internal suspend fun send( - text: String, - sendOptions: SendOptions? = null, - sentAt: Date? = null, - ): String { - val preparedMessage = prepareMessage(content = text, options = sendOptions) - return send(preparedMessage) - } - - suspend fun send(content: T, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(content = content, options = options) - return send(preparedMessage) - } - - suspend fun send(encodedContent: EncodedContent, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options) - return send(preparedMessage) - } - - suspend fun send(prepared: PreparedMessage): String { - client.publish(envelopes = prepared.envelopes) - if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { - client.contacts.allow(addresses = listOf(peerAddress)) - } - return prepared.messageId - } - - fun prepareMessage(content: T, options: SendOptions?): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(content = contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() - } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) - } - return prepareMessage(encodedContent = encoded, options = options) - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions? = null, - ): PreparedMessage { - val contact = client.contacts.find(peerAddress) ?: throw XMTPException("address not found") - val recipient = contact.toPublicKeyBundle() - if (!recipient.identityKey.hasSignature()) { - throw Exception("no signature for id key") - } - val date = Date() - val message = MessageV1Builder.buildEncode( - sender = client.v1keys, - recipient = recipient, - message = encodedContent.toByteArray(), - timestamp = date, - ) - - val isEphemeral: Boolean = options != null && options.ephemeral - - val env = - EnvelopeBuilder.buildFromString( - topic = if (isEphemeral) ephemeralTopic else topic.description, - 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 - } - return PreparedMessage(envelopes) - } - - private fun generateId(envelope: Envelope): String = - Hash.sha256(envelope.message.toByteArray()).toHex() - - val ephemeralTopic: String - get() = topic.description.replace("/xmtp/0/dm-", "/xmtp/0/dmE-") - - fun streamEphemeral(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(envelopeFromFFi(message)) - } - } - val stream = client.subscribe(listOf(ephemeralTopic), streamCallback) - awaitClose { launch { stream.end() } } - } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decrypt(envelope = envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic.description), streamCallback) - awaitClose { launch { stream.end() } } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt b/library/src/main/java/org/xmtp/android/library/ConversationV2.kt deleted file mode 100644 index 981934d8f..000000000 --- a/library/src/main/java/org/xmtp/android/library/ConversationV2.kt +++ /dev/null @@ -1,301 +0,0 @@ -package org.xmtp.android.library - -import android.util.Log -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.launch -import org.web3j.crypto.Hash -import org.xmtp.android.library.codecs.ContentCodec -import org.xmtp.android.library.codecs.EncodedContent -import org.xmtp.android.library.codecs.compress -import org.xmtp.android.library.messages.DecryptedMessage -import org.xmtp.android.library.messages.Envelope -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.Message -import org.xmtp.android.library.messages.MessageBuilder -import org.xmtp.android.library.messages.MessageV2Builder -import org.xmtp.android.library.messages.Pagination -import org.xmtp.android.library.messages.PagingInfoSortDirection -import org.xmtp.android.library.messages.SealedInvitationHeaderV1 -import org.xmtp.android.library.messages.getPublicKeyBundle -import org.xmtp.android.library.messages.walletAddress -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation -import uniffi.xmtpv3.FfiEnvelope -import uniffi.xmtpv3.FfiV2SubscriptionCallback -import java.util.Date - -data class ConversationV2( - val topic: String, - val keyMaterial: ByteArray, - val context: Invitation.InvitationV1.Context, - var consentProof: Invitation.ConsentProofPayload? = null, - val peerAddress: String, - val client: Client, - val createdAtNs: Long? = null, - private val header: SealedInvitationHeaderV1, -) { - - companion object { - fun create( - client: Client, - invitation: Invitation.InvitationV1, - header: SealedInvitationHeaderV1, - ): ConversationV2 { - val myKeys = client.keys.getPublicKeyBundle() - val peer = - if (myKeys.walletAddress == (header.sender.walletAddress)) header.recipient else header.sender - val peerAddress = peer.walletAddress - val keyMaterial = invitation.aes256GcmHkdfSha256.keyMaterial.toByteArray() - return ConversationV2( - topic = invitation.topic, - keyMaterial = keyMaterial, - context = invitation.context, - peerAddress = peerAddress, - client = client, - createdAtNs = header.createdNs, - header = header, - consentProof = if (invitation.hasConsentProof()) invitation.consentProof else null - ) - } - } - - val createdAt: Date = Date((createdAtNs ?: header.createdNs) / 1_000_000) - - /** - * This lists messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - * @see Conversation.messages - */ - suspend fun messages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val result = - apiClient.envelopes( - topic = topic, - pagination = pagination, - ) - - return result.mapNotNull { envelope -> - decodeEnvelopeOrNull(envelope) - } - } - - /** - * This lists decrypted messages sent to the [Conversation]. - * @param before initial date to filter - * @param after final date to create a range of dates and filter - * @param limit is the number of result that will be returned - * @param direction is the way of srting the information, by default is descending, you can - * know more about it in class [MessageApiOuterClass]. - * @see MessageApiOuterClass.SortDirection - * @return The list of messages sent. If [before] or [after] are specified then this will only list messages - * sent at or [after] and at or [before]. - * If [limit] is specified then results are pulled in pages of that size. - * If [direction] is specified then that will control the sort order of te messages. - */ - suspend fun decryptedMessages( - limit: Int? = null, - before: Date? = null, - after: Date? = null, - direction: PagingInfoSortDirection = MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING, - ): List { - val pagination = - Pagination(limit = limit, before = before, after = after, direction = direction) - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = apiClient.envelopes(topic, pagination) - - return envelopes.map { envelope -> - decrypt(envelope) - } - } - - /** - * This decrypts a message - * @param envelope Object that contains all the information of the encrypted message - * @return [DecryptedMessage] object - */ - fun decrypt(envelope: Envelope): DecryptedMessage { - val message = Message.parseFrom(envelope.message) - return MessageV2Builder.buildDecrypt( - id = generateId(envelope = envelope), - topic, - message.v2, - keyMaterial, - client, - ) - } - - fun streamMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - decodeEnvelopeOrNull(envelope = Util.envelopeFromFFi(message))?.let { - trySend(it) - } - } - } - val stream = client.subscribe(listOf(topic), streamCallback) - awaitClose { launch { stream.end() } } - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object - */ - fun decodeEnvelope(envelope: Envelope): DecodedMessage { - val message = Message.parseFrom(envelope.message) - return MessageV2Builder.buildDecode( - generateId(envelope = envelope), - topic = topic, - message.v2, - keyMaterial = keyMaterial, - client = client, - ) - } - - /** - * This encrypts a message - * @param envelope Object that contains all the information of the decrypted message - * @return [DecodedMessage] object if is not possible will return null - */ - private fun decodeEnvelopeOrNull(envelope: Envelope): DecodedMessage? { - return try { - decodeEnvelope(envelope) - } catch (e: Exception) { - Log.d("CONV_V2", "discarding message that failed to decode", e) - null - } - } - - suspend fun send(content: T, options: SendOptions? = null): String { - val preparedMessage = prepareMessage(content = content, options = options) - return send(preparedMessage) - } - - suspend fun send(text: String, options: SendOptions? = null, sentAt: Date? = null): String { - val preparedMessage = prepareMessage(content = text, options = options) - return send(preparedMessage) - } - - suspend fun send(encodedContent: EncodedContent, options: SendOptions?): String { - val preparedMessage = prepareMessage(encodedContent = encodedContent, options = options) - return send(preparedMessage) - } - - suspend fun send(prepared: PreparedMessage): String { - client.publish(envelopes = prepared.envelopes) - if (client.contacts.consentList.state(address = peerAddress) == ConsentState.UNKNOWN) { - client.contacts.allow(addresses = listOf(peerAddress)) - } - return prepared.messageId - } - - fun , T> encode(codec: Codec, content: T): ByteArray { - val encodedContent = codec.encode(content = content) - val message = MessageV2Builder.buildEncode( - client = client, - encodedContent = encodedContent, - topic = topic, - keyMaterial = keyMaterial, - codec = codec, - ) - val envelope = EnvelopeBuilder.buildFromString( - topic = topic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), - ) - return envelope.toByteArray() - } - - fun prepareMessage(content: T, options: SendOptions?): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - - fun > encode(codec: Codec, content: Any?): EncodedContent { - val contentType = content as? T - if (contentType != null) { - return codec.encode(contentType) - } else { - throw XMTPException("Codec type is not registered") - } - } - - var encoded = encode(codec = codec as ContentCodec, content = content) - val fallback = codec.fallback(content) - if (!fallback.isNullOrBlank()) { - encoded = encoded.toBuilder().also { - it.fallback = fallback - }.build() - } - val compression = options?.compression - if (compression != null) { - encoded = encoded.compress(compression) - } - return prepareMessage(encoded, options = options) - } - - fun prepareMessage( - encodedContent: EncodedContent, - options: SendOptions?, - ): PreparedMessage { - val codec = Client.codecRegistry.find(options?.contentType) - val message = MessageV2Builder.buildEncode( - client = client, - encodedContent = encodedContent, - topic = topic, - keyMaterial = keyMaterial, - codec = codec, - ) - - val newTopic = if (options?.ephemeral == true) ephemeralTopic else topic - - val envelope = EnvelopeBuilder.buildFromString( - topic = newTopic, - timestamp = Date(), - message = MessageBuilder.buildFromMessageV2(v2 = message.messageV2).toByteArray(), - ) - return PreparedMessage(listOf(envelope)) - } - - private fun generateId(envelope: Envelope): String = - Hash.sha256(envelope.message.toByteArray()).toHex() - - val ephemeralTopic: String - get() = topic.replace("/xmtp/0/m", "/xmtp/0/mE") - - fun streamEphemeral(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(Util.envelopeFromFFi(message)) - } - } - val stream = client.subscribe(listOf(ephemeralTopic), streamCallback) - awaitClose { launch { stream.end() } } - } - - fun streamDecryptedMessages(): Flow = callbackFlow { - val streamCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - trySend(decrypt(envelope = Util.envelopeFromFFi(message))) - } - } - val stream = client.subscribe(listOf(topic), streamCallback) - awaitClose { launch { stream.end() } } - } -} diff --git a/library/src/main/java/org/xmtp/android/library/Conversations.kt b/library/src/main/java/org/xmtp/android/library/Conversations.kt index a346cc710..2c65f379e 100644 --- a/library/src/main/java/org/xmtp/android/library/Conversations.kt +++ b/library/src/main/java/org/xmtp/android/library/Conversations.kt @@ -75,7 +75,7 @@ data class Conversations( LAST_MESSAGE; } - suspend fun conversationFromWelcome(envelopeBytes: ByteArray): Conversation { + suspend fun fromWelcome(envelopeBytes: ByteArray): Conversation { val conversation = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes) ?: throw XMTPException("Client does not support Groups") if (conversation.groupMetadata().conversationType() == "dm") { @@ -85,12 +85,6 @@ data class Conversations( } } - suspend fun fromWelcome(envelopeBytes: ByteArray): Group { - val group = libXMTPConversations?.processStreamedWelcomeMessage(envelopeBytes) - ?: throw XMTPException("Client does not support Groups") - return Group(client, group) - } - suspend fun newGroup( accountAddresses: List, permissions: GroupPermissionPreconfiguration = GroupPermissionPreconfiguration.ALL_MEMBERS, @@ -167,12 +161,6 @@ data class Conversations( return Group(client, group) } - // Sync from the network the latest list of groups - @Deprecated("Sync now includes DMs and Groups", replaceWith = ReplaceWith("syncConversations")) - suspend fun syncGroups() { - libXMTPConversations?.sync() - } - // Sync from the network the latest list of conversations suspend fun syncConversations() { libXMTPConversations?.sync() @@ -183,17 +171,12 @@ data class Conversations( return libXMTPConversations?.syncAllConversations() } - // Sync all existing local groups data from the network (Note: call syncGroups() first to get the latest list of groups) - @Deprecated( - "Sync now includes DMs and Groups", - replaceWith = ReplaceWith("syncAllConversations") - ) - suspend fun syncAllGroups(): UInt? { - return libXMTPConversations?.syncAllConversations() + suspend fun newConversation(peerAddress: String): Conversation { + val dm = findOrCreateDm(peerAddress) + return Conversation.Dm(dm) } suspend fun findOrCreateDm(peerAddress: String): Dm { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") if (peerAddress.lowercase() == client.address.lowercase()) { throw XMTPException("Recipient is sender") } @@ -212,97 +195,6 @@ data class Conversations( return dm } - /** - * This creates a new [Conversation] using a specified address - * @param peerAddress The address of the client that you want to start a new conversation - * @param context Context of the invitation. - * @return New [Conversation] using the address and according to that address is able to find - * the topics if exists for that new conversation. - */ - suspend fun newConversation( - peerAddress: String, - context: Invitation.InvitationV1.Context? = null, - consentProof: Invitation.ConsentProofPayload? = null, - ): Conversation { - if (peerAddress.lowercase() == client.address.lowercase()) { - throw XMTPException("Recipient is sender") - } - val existingConversation = conversationsByTopic.values.firstOrNull { - it.peerAddress == peerAddress && it.conversationId == context?.conversationId - } - if (existingConversation != null) { - return existingConversation - } - val contact = client.contacts.find(peerAddress) - ?: throw XMTPException("Recipient not on network") - // See if we have an existing v1 convo - if (context?.conversationId.isNullOrEmpty()) { - val invitationPeers = listIntroductionPeers() - val peerSeenAt = invitationPeers[peerAddress] - if (peerSeenAt != null) { - val conversation = Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = peerSeenAt, - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - } - - // If the contact is v1, start a v1 conversation - if (Contact.ContactBundle.VersionCase.V1 == contact.versionCase && context?.conversationId.isNullOrEmpty()) { - val conversation = Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = Date(), - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - // See if we have a v2 conversation - for (sealedInvitation in listInvitations()) { - if (!sealedInvitation.involves(contact)) { - continue - } - val invite = sealedInvitation.v1.getInvitation(viewer = client.keys) - if (invite.context.conversationId == context?.conversationId && invite.context.conversationId != "") { - val conversation = Conversation.V2( - ConversationV2( - topic = invite.topic, - keyMaterial = invite.aes256GcmHkdfSha256.keyMaterial.toByteArray(), - context = invite.context, - peerAddress = peerAddress, - client = client, - header = sealedInvitation.v1.header, - consentProof = if (invite.hasConsentProof()) invite.consentProof else null - ), - ) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - } - // We don't have an existing conversation, make a v2 one - val recipient = contact.toSignedPublicKeyBundle() - val invitation = Invitation.InvitationV1.newBuilder().build() - .createDeterministic(client.keys, recipient, context, consentProof) - val sealedInvitation = - sendInvitation(recipient = recipient, invitation = invitation, created = Date()) - val conversationV2 = ConversationV2.create( - client = client, - invitation = invitation, - header = sealedInvitation.v1.header, - ) - client.contacts.allow(addresses = listOf(peerAddress)) - val conversation = Conversation.V2(conversationV2) - conversationsByTopic[conversation.topic] = conversation - return conversation - } - suspend fun listGroups( after: Date? = null, before: Date? = null, @@ -326,7 +218,6 @@ data class Conversations( before: Date? = null, limit: Int? = null, ): List { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") val ffiDms = libXMTPConversations?.listDms( opts = FfiListConversationsOptions( after?.time?.nanoseconds?.toLong(DurationUnit.NANOSECONDS), @@ -340,7 +231,7 @@ data class Conversations( } } - suspend fun listConversations( + suspend fun list( after: Date? = null, before: Date? = null, limit: Int? = null, @@ -411,52 +302,6 @@ data class Conversations( } } - /** - * Get the list of conversations that current user has - * @return The list of [Conversation] that the current [Client] has. - */ - suspend fun list(includeGroups: Boolean = false): List { - val newConversations = mutableListOf() - val mostRecent = conversationsByTopic.values.maxOfOrNull { it.createdAt } - val pagination = Pagination(after = mostRecent) - val seenPeers = listIntroductionPeers(pagination = pagination) - for ((peerAddress, sentAt) in seenPeers) { - newConversations.add( - Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = sentAt, - ), - ), - ) - } - val invitations = listInvitations(pagination = pagination) - for (sealedInvitation in invitations) { - try { - val newConversation = Conversation.V2(conversation(sealedInvitation)) - newConversations.add(newConversation) - val consentProof = newConversation.consentProof - if (consentProof != null) { - handleConsentProof(consentProof, newConversation.peerAddress) - } - } catch (e: Exception) { - Log.d(TAG, e.message.toString()) - } - } - - conversationsByTopic += newConversations.filter { - it.peerAddress != client.address && Topic.isValidTopic(it.topic) - }.map { Pair(it.topic, it) } - - if (includeGroups) { - syncConversations() - val groups = listGroups() - conversationsByTopic += groups.map { Pair(it.topic, Conversation.Group(it)) } - } - return conversationsByTopic.values.sortedByDescending { it.createdAt } - } - fun getHmacKeys( request: Keystore.GetConversationHmacKeysRequest? = null, ): Keystore.GetConversationHmacKeysResponse { @@ -470,28 +315,28 @@ data class Conversations( request!!.topicsList.contains(it.key) }.toMutableMap() } - - topics.iterator().forEach { - val conversation = it.value - val hmacKeys = HmacKeys.newBuilder() - if (conversation.keyMaterial != null) { - (thirtyDayPeriodsSinceEpoch - 1..thirtyDayPeriodsSinceEpoch + 1).iterator() - .forEach { value -> - val info = "$value-${client.address}" - val hmacKey = - Crypto.deriveKey( - conversation.keyMaterial!!, - ByteArray(0), - info.toByteArray(Charsets.UTF_8), - ) - val hmacKeyData = HmacKeyData.newBuilder() - hmacKeyData.hmacKey = hmacKey.toByteString() - hmacKeyData.thirtyDayPeriodsSinceEpoch = value - hmacKeys.addValues(hmacKeyData) - } - hmacKeysResponse.putHmacKeys(conversation.topic, hmacKeys.build()) - } - } + // TODO +// topics.iterator().forEach { +// val conversation = it.value +// val hmacKeys = HmacKeys.newBuilder() +// if (conversation.keyMaterial != null) { +// (thirtyDayPeriodsSinceEpoch - 1..thirtyDayPeriodsSinceEpoch + 1).iterator() +// .forEach { value -> +// val info = "$value-${client.address}" +// val hmacKey = +// Crypto.deriveKey( +// conversation.keyMaterial!!, +// ByteArray(0), +// info.toByteArray(Charsets.UTF_8), +// ) +// val hmacKeyData = HmacKeyData.newBuilder() +// hmacKeyData.hmacKey = hmacKey.toByteString() +// hmacKeyData.thirtyDayPeriodsSinceEpoch = value +// hmacKeys.addValues(hmacKeyData) +// } +// hmacKeysResponse.putHmacKeys(conversation.topic, hmacKeys.build()) +// } +// } return hmacKeysResponse.build() } @@ -512,48 +357,7 @@ data class Conversations( } } - fun stream(): Flow = callbackFlow { - val streamedConversationTopics: MutableSet = mutableSetOf() - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - val envelope = envelopeFromFFi(message) - if (envelope.contentTopic == Topic.userIntro(client.address).description) { - val conversationV1 = fromIntro(envelope = envelope) - if (!streamedConversationTopics.contains(conversationV1.topic)) { - streamedConversationTopics.add(conversationV1.topic) - trySend(conversationV1) - } - } - - if (envelope.contentTopic == Topic.userInvite(client.address).description) { - val conversationV2 = fromInvite(envelope = envelope) - if (!streamedConversationTopics.contains(conversationV2.topic)) { - streamedConversationTopics.add(conversationV2.topic) - trySend(conversationV2) - } - } - } - } - - val stream = client.subscribe2( - FfiV2SubscribeRequest( - listOf( - Topic.userIntro(client.address).description, - Topic.userInvite(client.address).description - ) - ), - subscriptionCallback - ) - - awaitClose { launch { stream.end() } } - } - - fun streamAll(): Flow { - return merge(streamGroupConversations(), stream()) - } - - fun streamConversations(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") + fun stream(/*Maybe Put a way to specify group, dm, or both?*/): Flow = callbackFlow { val conversationCallback = object : FfiConversationCallback { override fun onConversation(conversation: FfiConversation) { if (conversation.groupMetadata().conversationType() == "dm") { @@ -562,7 +366,6 @@ data class Conversations( trySend(Conversation.Group(Group(client, conversation))) } } - override fun onError(error: FfiSubscribeException) { Log.e("XMTP Conversation stream", error.message.toString()) } @@ -572,91 +375,7 @@ data class Conversations( awaitClose { stream.end() } } - private fun streamGroupConversations(): Flow = callbackFlow { - val groupCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - trySend(Conversation.Group(Group(client, conversation))) - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Conversation stream", error.message.toString()) - } - } - - val stream = libXMTPConversations?.streamGroups(groupCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamGroups(): Flow = callbackFlow { - val groupCallback = object : FfiConversationCallback { - override fun onConversation(conversation: FfiConversation) { - trySend(Group(client, conversation)) - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP Group stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamGroups(groupCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllMessages(includeGroups: Boolean = false): Flow { - return if (includeGroups) { - merge(streamAllV2Messages(), streamAllGroupMessages()) - } else { - streamAllV2Messages() - } - } - - fun streamAllDecryptedMessages(includeGroups: Boolean = false): Flow { - return if (includeGroups) { - merge(streamAllV2DecryptedMessages(), streamAllGroupDecryptedMessages()) - } else { - streamAllV2DecryptedMessages() - } - } - - fun streamAllGroupMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decodedMessage = MessageV3(client, message).decodeOrNull() - decodedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all group message stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamAllGroupMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllGroupDecryptedMessages(): Flow = callbackFlow { - val messageCallback = object : FfiMessageCallback { - override fun onMessage(message: FfiMessage) { - val decryptedMessage = MessageV3(client, message).decryptOrNull() - decryptedMessage?.let { - trySend(it) - } - } - - override fun onError(error: FfiSubscribeException) { - Log.e("XMTP all group message stream", error.message.toString()) - } - } - val stream = libXMTPConversations?.streamAllGroupMessages(messageCallback) - ?: throw XMTPException("Client does not support Groups") - awaitClose { stream.end() } - } - - fun streamAllConversationMessages(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") + fun streamAllMessages(/*Maybe Put a way to specify group, dm, or both?*/): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { val conversation = client.findConversation(message.convoId.toHex()) @@ -665,7 +384,6 @@ data class Conversations( Conversation.Version.DM -> { decodedMessage?.let { trySend(it) } } - else -> { decodedMessage?.let { trySend(it) } } @@ -683,8 +401,7 @@ data class Conversations( awaitClose { stream.end() } } - fun streamAllConversationDecryptedMessages(): Flow = callbackFlow { - if (client.hasV2Client) throw XMTPException("Only supported for V3 only clients.") + fun streamAllDecryptedMessages(): Flow = callbackFlow { val messageCallback = object : FfiMessageCallback { override fun onMessage(message: FfiMessage) { val conversation = client.findConversation(message.convoId.toHex()) @@ -711,364 +428,4 @@ data class Conversations( awaitClose { stream.end() } } - - // ------- V1 V2 to be deprecated ------ - - /** - * @return This lists messages sent to the [Conversation]. - * This pulls messages from multiple conversations in a single call. - * @see Conversation.messages - */ - suspend fun listBatchMessages( - topics: List>, - ): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence of messages. Use listConversations order lastMessage") - - val requests = topics.map { (topic, page) -> - makeQueryRequest(topic = topic, pagination = page) - } - - // The maximum number of requests permitted in a single batch call. - val maxQueryRequestsPerBatch = 50 - val messages: MutableList = mutableListOf() - val batches = requests.chunked(maxQueryRequestsPerBatch) - for (batch in batches) { - messages.addAll( - client.batchQuery(batch).responsesOrBuilderList.flatMap { res -> - res.envelopesList.mapNotNull { envelope -> - val conversation = conversationsByTopic[envelope.contentTopic] - if (conversation == null) { - Log.d(TAG, "discarding message, unknown conversation $envelope") - return@mapNotNull null - } - val msg = conversation.decodeOrNull(envelope) - msg - } - }, - ) - } - return messages - } - - /** - * @return This lists messages sent to the [Conversation] when the messages are encrypted. - * This pulls messages from multiple conversations in a single call. - * @see listBatchMessages - */ - suspend fun listBatchDecryptedMessages( - topics: List>, - ): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence of messages. Use listConversations order lastMessage") - - val requests = topics.map { (topic, page) -> - makeQueryRequest(topic = topic, pagination = page) - } - - // The maximum number of requests permitted in a single batch call. - val maxQueryRequestsPerBatch = 50 - val messages: MutableList = mutableListOf() - val batches = requests.chunked(maxQueryRequestsPerBatch) - for (batch in batches) { - messages.addAll( - client.batchQuery(batch).responsesOrBuilderList.flatMap { res -> - res.envelopesList.mapNotNull { envelope -> - val conversation = conversationsByTopic[envelope.contentTopic] - if (conversation == null) { - Log.d(TAG, "discarding message, unknown conversation $envelope") - return@mapNotNull null - } - try { - val msg = conversation.decrypt(envelope) - msg - } catch (e: Exception) { - Log.e(TAG, "Error decrypting message: $envelope", e) - null - } - } - }, - ) - } - return messages - } - - fun importTopicData(data: TopicData): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. The local database handles persistence.") - 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, - createdAtNs = data.createdNs, - header = Invitation.SealedInvitationHeaderV1.getDefaultInstance(), - consentProof = if (data.invitation.hasConsentProof()) data.invitation.consentProof else null - ), - ) - } - conversationsByTopic[conversation.topic] = conversation - return conversation - } - - /** - * This method creates a new conversation from an invitation. - * @param envelope Object that contains the information of the current [Client] such as topic - * and timestamp. - * @return [Conversation] from an invitation suing the current [Client]. - */ - fun fromInvite(envelope: Envelope): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val sealedInvitation = Invitation.SealedInvitation.parseFrom(envelope.message) - val unsealed = sealedInvitation.v1.getInvitation(viewer = client.keys) - return Conversation.V2( - ConversationV2.create( - client = client, - invitation = unsealed, - header = sealedInvitation.v1.header, - ), - ) - } - - /** - * This method creates a new conversation from an Intro. - * @param envelope Object that contains the information of the current [Client] such as topic - * and timestamp. - * @return [Conversation] from an Intro suing the current [Client]. - */ - fun fromIntro(envelope: Envelope): Conversation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val messageV1 = MessageV1Builder.buildFromBytes(envelope.message.toByteArray()) - val senderAddress = messageV1.header.sender.walletAddress - val recipientAddress = messageV1.header.recipient.walletAddress - val peerAddress = if (client.address == senderAddress) recipientAddress else senderAddress - return Conversation.V1( - ConversationV1( - client = client, - peerAddress = peerAddress, - sentAt = messageV1.sentAt, - ), - ) - } - - fun conversation(sealedInvitation: SealedInvitation): ConversationV2 { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use client.findDm to find the dm.") - val unsealed = sealedInvitation.v1.getInvitation(viewer = client.keys) - return ConversationV2.create( - client = client, - invitation = unsealed, - header = sealedInvitation.v1.header, - ) - } - - /** - * Send an invitation from the current [Client] to the specified recipient (Client) - * @param recipient The public key of the client that you want to send the invitation - * @param invitation Invitation object that will be send - * @param created Specified date creation for this invitation. - * @return [SealedInvitation] with the specified information. - */ - suspend fun sendInvitation( - recipient: SignedPublicKeyBundle, - invitation: InvitationV1, - created: Date, - ): SealedInvitation { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use newConversation to create welcome.") - client.keys.let { - val sealed = SealedInvitationBuilder.buildFromV1( - sender = it, - recipient = recipient, - created = created, - invitation = invitation, - ) - val peerAddress = recipient.walletAddress - - client.publish( - envelopes = listOf( - EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite( - client.address, - ), - timestamp = created, - message = sealed.toByteArray(), - ), - EnvelopeBuilder.buildFromTopic( - topic = Topic.userInvite( - peerAddress, - ), - timestamp = created, - message = sealed.toByteArray(), - ), - ), - ) - return sealed - } - } - - /** - * Get the stream of all messages of the current [Client] - * @return Flow object of [DecodedMessage] that represents all the messages of the - * current [Client] as userInvite and userIntro - */ - private fun streamAllV2Messages(): Flow = callbackFlow { - val topics = mutableListOf( - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ) - - for (conversation in list()) { - topics.add(conversation.topic) - } - - val subscriptionRequest = FfiV2SubscribeRequest(topics) - var stream = FfiV2Subscription(NoPointer) - - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - when { - conversationsByTopic.containsKey(message.contentTopic) -> { - val conversation = conversationsByTopic[message.contentTopic] - val decoded = conversation?.decode(envelopeFromFFi(message)) - decoded?.let { trySend(it) } - } - - message.contentTopic.startsWith("/xmtp/0/invite-") -> { - val conversation = fromInvite(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - message.contentTopic.startsWith("/xmtp/0/intro-") -> { - val conversation = fromIntro(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - val decoded = conversation.decode(envelopeFromFFi(message)) - trySend(decoded) - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - else -> {} - } - } - } - - stream = client.subscribe2(subscriptionRequest, subscriptionCallback) - - awaitClose { launch { stream.end() } } - } - - private fun streamAllV2DecryptedMessages(): Flow = callbackFlow { - val topics = mutableListOf( - Topic.userInvite(client.address).description, - Topic.userIntro(client.address).description, - ) - - for (conversation in list()) { - topics.add(conversation.topic) - } - - val subscriptionRequest = FfiV2SubscribeRequest(topics) - var stream = FfiV2Subscription(NoPointer) - - val subscriptionCallback = object : FfiV2SubscriptionCallback { - override fun onMessage(message: FfiEnvelope) { - when { - conversationsByTopic.containsKey(message.contentTopic) -> { - val conversation = conversationsByTopic[message.contentTopic] - val decrypted = conversation?.decrypt(envelopeFromFFi(message)) - decrypted?.let { trySend(it) } - } - - message.contentTopic.startsWith("/xmtp/0/invite-") -> { - val conversation = fromInvite(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - message.contentTopic.startsWith("/xmtp/0/intro-") -> { - val conversation = fromIntro(envelope = envelopeFromFFi(message)) - conversationsByTopic[conversation.topic] = conversation - val decrypted = conversation.decrypt(envelopeFromFFi(message)) - trySend(decrypted) - topics.add(conversation.topic) - subscriptionRequest.contentTopics = topics - launch { stream.update(subscriptionRequest) } - } - - else -> {} - } - } - } - - stream = client.subscribe2(subscriptionRequest, subscriptionCallback) - - awaitClose { launch { stream.end() } } - } - - /** - * Get the list of invitations using the data sent [pagination] - * @param pagination Information of the topics, ranges (dates), etc. - * @return List of [SealedInvitation] that are inside of the range specified by [pagination] - */ - private suspend fun listInvitations(pagination: Pagination? = null): List { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = - apiClient.envelopes(Topic.userInvite(client.address).description, pagination) - return envelopes.map { envelope -> - SealedInvitation.parseFrom(envelope.message) - } - } - - private suspend fun listIntroductionPeers(pagination: Pagination? = null): Map { - if (!client.hasV2Client) throw XMTPException("Not supported for V3. Use conversationFromWelcome.") - val apiClient = client.apiClient ?: throw XMTPException("V2 only function") - val envelopes = apiClient.queryTopic( - topic = Topic.userIntro(client.address), - pagination = pagination, - ).envelopesList - val messages = envelopes.mapNotNull { envelope -> - try { - val message = MessageV1Builder.buildFromBytes(envelope.message.toByteArray()) - // Attempt to decrypt, just to make sure we can - message.decrypt(client.privateKeyBundleV1) - message - } catch (e: Exception) { - Log.d(TAG, e.message.toString()) - null - } - } - val seenPeers: MutableMap = mutableMapOf() - for (message in messages) { - val recipientAddress = message.recipientAddress - val senderAddress = message.senderAddress - val sentAt = message.sentAt - val peerAddress = - if (recipientAddress == client.address) senderAddress else recipientAddress - val existing = seenPeers[peerAddress] - if (existing == null) { - seenPeers[peerAddress] = sentAt - continue - } - if (existing > sentAt) { - seenPeers[peerAddress] = sentAt - } - } - return seenPeers - } -} +} \ No newline at end of file