From a1dedd77e76f4bde330c013458a0517a3e5576de Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 12:35:33 -0800 Subject: [PATCH 01/21] get on the latest 3.0.0 version --- android/build.gradle | 2 +- example/ios/Podfile.lock | 8 ++++---- ios/XMTPReactNative.podspec | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index fb7a25ce2..9ea79223c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.16.2" + implementation "org.xmtp:android:3.0.0" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a30ad4b3a..f9492c5c9 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,7 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.16.2): + - XMTP (3.0.0): - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.6.0) @@ -458,7 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.16.2) + - XMTP (= 3.0.0) - Yoga (1.14.0) DEPENDENCIES: @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 281c763321f3be82b3e5d91bfd79f107b1169e30 - XMTPReactNative: 2428cbce29fca3ca3e7682b096765ccf3dca739f + XMTP: 15d027733802cb4a0fed06529701ce920c0d2f17 + XMTPReactNative: 02ae4f694b984cd320d444281f9940e35b02de6b Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 426cef57a..c0a001898 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.16.2" + s.dependency "XMTP", "= 3.0.0" end From 942d79becb288159e24d58efc760c69ab3ccb87f Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 14:12:19 -0800 Subject: [PATCH 02/21] update android bridge to be v3 only --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 1487 +++-------------- .../wrappers/AuthParamsWrapper.kt | 2 - .../wrappers/ConversationContainerWrapper.kt | 43 - .../wrappers/ConversationWrapper.kt | 45 +- .../wrappers/DecodedMessageWrapper.kt | 8 +- .../xmtpreactnativesdk/wrappers/DmWrapper.kt | 6 +- .../wrappers/GroupWrapper.kt | 6 +- .../wrappers/PreparedLocalMessage.kt | 27 - src/index.ts | 4 +- src/lib/Conversation.ts | 345 +--- src/lib/ConversationContainer.ts | 42 - src/lib/Conversations.ts | 42 +- src/lib/Dm.ts | 6 +- src/lib/Group.ts | 6 +- 14 files changed, 316 insertions(+), 1753 deletions(-) delete mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt delete mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PreparedLocalMessage.kt delete mode 100644 src/lib/ConversationContainer.ts diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 26de287d2..1f4d010e5 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -6,7 +6,6 @@ import android.util.Base64 import android.util.Base64.NO_WRAP import android.util.Log import androidx.core.net.toUri -import com.google.gson.JsonParser import com.google.protobuf.kotlin.toByteString import expo.modules.kotlin.exception.Exceptions import expo.modules.kotlin.functions.Coroutine @@ -14,22 +13,19 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.xmtpreactnativesdk.wrappers.AuthParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.ClientWrapper -import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString import expo.modules.xmtpreactnativesdk.wrappers.ContentJson -import expo.modules.xmtpreactnativesdk.wrappers.ConversationContainerWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper +import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.CreateGroupParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.DmWrapper import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment -import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper import expo.modules.xmtpreactnativesdk.wrappers.PermissionPolicySetWrapper -import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -37,15 +33,14 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import org.json.JSONObject import org.xmtp.android.library.Client import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation import org.xmtp.android.library.Conversations.ConversationOrder +import org.xmtp.android.library.Dm import org.xmtp.android.library.Group import org.xmtp.android.library.PreEventCallback -import org.xmtp.android.library.PreparedMessage import org.xmtp.android.library.SendOptions import org.xmtp.android.library.SigningKey import org.xmtp.android.library.WalletType @@ -58,24 +53,16 @@ import org.xmtp.android.library.codecs.EncryptedEncodedContent import org.xmtp.android.library.codecs.RemoteAttachment import org.xmtp.android.library.codecs.decoded import org.xmtp.android.library.hexToByteArray -import org.xmtp.android.library.messages.EnvelopeBuilder -import org.xmtp.android.library.messages.InvitationV1ContextBuilder -import org.xmtp.android.library.messages.Pagination +import org.xmtp.android.library.libxmtp.Message import org.xmtp.android.library.messages.PrivateKeyBuilder import org.xmtp.android.library.messages.Signature -import org.xmtp.android.library.messages.getPublicKeyBundle import org.xmtp.android.library.push.Service import org.xmtp.android.library.push.XMTPPush -import org.xmtp.proto.keystore.api.v1.Keystore.TopicMap.TopicData -import org.xmtp.proto.message.api.v1.MessageApiOuterClass -import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload -import org.xmtp.proto.message.contents.PrivateKeyOuterClass import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import java.io.BufferedReader import java.io.File import java.io.InputStreamReader -import java.util.Date import java.util.UUID import kotlin.coroutines.Continuation import kotlin.coroutines.resume @@ -149,8 +136,8 @@ fun Group.cacheKey(inboxId: String): String { return "${inboxId}:${id}" } -fun Conversation.cacheKeyV3(inboxId: String): String { - return "${inboxId}:${topic}:${id}" +fun Dm.cacheKey(inboxId: String): String { + return "${inboxId}:${id}" } class XMTPModule : Module() { @@ -181,29 +168,17 @@ class XMTPModule : Module() { } private fun clientOptions( - dbEncryptionKey: List?, + dbEncryptionKey: List, authParams: String, - hasCreateIdentityCallback: Boolean? = null, - hasEnableIdentityCallback: Boolean? = null, hasPreAuthenticateToInboxCallback: Boolean? = null, ): ClientOptions { - if (hasCreateIdentityCallback == true) - preCreateIdentityCallbackDeferred = CompletableDeferred() - if (hasEnableIdentityCallback == true) - preEnableIdentityCallbackDeferred = CompletableDeferred() if (hasPreAuthenticateToInboxCallback == true) preAuthenticateToInboxCallbackDeferred = CompletableDeferred() - val preCreateIdentityCallback: PreEventCallback? = - preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } - val preEnableIdentityCallback: PreEventCallback? = - preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } val preAuthenticateToInboxCallback: PreEventCallback? = preAuthenticateToInboxCallback.takeIf { hasPreAuthenticateToInboxCallback == true } - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - val context = if (authOptions.enableV3) context else null val encryptionKeyBytes = - dbEncryptionKey?.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> + dbEncryptionKey.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } @@ -216,10 +191,7 @@ class XMTPModule : Module() { return ClientOptions( api = apiEnvironments(authOptions.environment, authOptions.appVersion), - preCreateIdentityCallback = preCreateIdentityCallback, - preEnableIdentityCallback = preEnableIdentityCallback, preAuthenticateToInboxCallback = preAuthenticateToInboxCallback, - enableV3 = authOptions.enableV3, appContext = context, dbEncryptionKey = encryptionKeyBytes, dbDirectory = authOptions.dbDirectory, @@ -231,11 +203,7 @@ class XMTPModule : Module() { private var xmtpPush: XMTPPush? = null private var signer: ReactNativeSigner? = null private val isDebugEnabled = BuildConfig.DEBUG // TODO: consider making this configurable - private val conversations: MutableMap = mutableMapOf() - private val groups: MutableMap = mutableMapOf() private val subscriptions: MutableMap = mutableMapOf() - private var preEnableIdentityCallbackDeferred: CompletableDeferred? = null - private var preCreateIdentityCallbackDeferred: CompletableDeferred? = null private var preAuthenticateToInboxCallbackDeferred: CompletableDeferred? = null @@ -245,24 +213,10 @@ class XMTPModule : Module() { // Auth "sign", "authed", - "authedV3", - "bundleAuthed", - "preCreateIdentityCallback", - "preEnableIdentityCallback", "preAuthenticateToInboxCallback", - // ConversationV2 "conversation", - "conversationContainer", + "allMessages", "message", - "conversationMessage", - // ConversationV3 - "conversationV3", - "allConversationMessages", - "conversationV3Message", - // Group - "groupMessage", - "allGroupMessage", - "group", ) Function("address") { inboxId: String -> @@ -335,27 +289,6 @@ class XMTPModule : Module() { // // Auth functions // - AsyncFunction("auth") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> - withContext(Dispatchers.IO) { - - logV("auth") - val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) - signer = reactSigner - val options = clientOptions( - dbEncryptionKey, - authParams, - hasCreateIdentityCallback, - hasEnableIdentityCallback, - hasAuthInboxCallback, - ) - val client = Client().create(account = reactSigner, options = options) - clients[client.inboxId] = client - ContentJson.Companion - signer = null - sendEvent("authed", ClientWrapper.encodeToObj(client)) - } - } - Function("receiveSignature") { requestID: String, signature: String -> logV("receiveSignature") signer?.handle(id = requestID, signature = signature) @@ -366,82 +299,9 @@ class XMTPModule : Module() { signer?.handleSCW(id = requestID, signature = signature) } - // Generate a random wallet and set the client to that - AsyncFunction("createRandom") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> - withContext(Dispatchers.IO) { - logV("createRandom") - val privateKey = PrivateKeyBuilder() - val options = clientOptions( - dbEncryptionKey, - authParams, - hasCreateIdentityCallback, - hasEnableIdentityCallback, - hasPreAuthenticateToInboxCallback, - ) - val randomClient = Client().create(account = privateKey, options = options) - - ContentJson.Companion - clients[randomClient.inboxId] = randomClient - ClientWrapper.encodeToObj(randomClient) - } - } - - AsyncFunction("createFromKeyBundle") Coroutine { keyBundle: String, dbEncryptionKey: List?, authParams: String -> - withContext(Dispatchers.IO) { - logV("createFromKeyBundle") - try { - val options = clientOptions( - dbEncryptionKey, - authParams - ) - val bundle = - PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( - Base64.decode( - keyBundle, - NO_WRAP - ) - ) - val client = Client().buildFromBundle(bundle = bundle, options = options) - ContentJson.Companion - clients[client.inboxId] = client - ClientWrapper.encodeToObj(client) - } catch (e: Exception) { - throw XMTPException("Failed to create client: $e") - } - } - } - - AsyncFunction("createFromKeyBundleWithSigner") Coroutine { address: String, keyBundle: String, dbEncryptionKey: List?, authParams: String -> - withContext(Dispatchers.IO) { - logV("createFromKeyBundleWithSigner") - try { - val options = clientOptions( - dbEncryptionKey, - authParams - ) - val bundle = - PrivateKeyOuterClass.PrivateKeyBundle.parseFrom( - Base64.decode( - keyBundle, - NO_WRAP - ) - ) - val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) - signer = reactSigner - val client = Client().buildFromBundle(bundle = bundle, options = options, account = reactSigner) - clients[client.inboxId] = client - ContentJson.Companion - signer = null - sendEvent("bundleAuthed", ClientWrapper.encodeToObj(client)) - } catch (e: Exception) { - throw XMTPException("Failed to create client: $e") - } - } - } - - AsyncFunction("createV3") Coroutine { address: String, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> + AsyncFunction("create") Coroutine { address: String, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String -> withContext(Dispatchers.IO) { - logV("createV3") + logV("create") val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) val reactSigner = ReactNativeSigner( module = this@XMTPModule, @@ -454,11 +314,9 @@ class XMTPModule : Module() { val options = clientOptions( dbEncryptionKey, authParams, - hasCreateIdentityCallback, - hasEnableIdentityCallback, hasAuthInboxCallback, ) - val client = Client().createV3(account = reactSigner, options = options) + val client = Client().create(account = reactSigner, options = options) clients[client.inboxId] = client ContentJson.Companion signer = null @@ -466,33 +324,31 @@ class XMTPModule : Module() { } } - AsyncFunction("buildV3") Coroutine { address: String, dbEncryptionKey: List?, authParams: String -> + AsyncFunction("build") Coroutine { address: String, dbEncryptionKey: List, authParams: String -> withContext(Dispatchers.IO) { - logV("buildV3") + logV("build") val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) val options = clientOptions( dbEncryptionKey, authParams, ) - val client = Client().buildV3(address = address, options = options) + val client = Client().build(address = address, options = options) ContentJson.Companion clients[client.inboxId] = client ClientWrapper.encodeToObj(client) } } - AsyncFunction("createRandomV3") Coroutine { hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean?, hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List?, authParams: String -> + AsyncFunction("createRandom") Coroutine { hasPreAuthenticateToInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String -> withContext(Dispatchers.IO) { - logV("createRandomV3") + logV("createRandom") val privateKey = PrivateKeyBuilder() val options = clientOptions( dbEncryptionKey, authParams, - hasCreateIdentityCallback, - hasEnableIdentityCallback, hasPreAuthenticateToInboxCallback, ) - val randomClient = Client().createV3(account = privateKey, options = options) + val randomClient = Client().create(account = privateKey, options = options) ContentJson.Companion clients[randomClient.inboxId] = randomClient @@ -508,110 +364,11 @@ class XMTPModule : Module() { } } - AsyncFunction("sign") Coroutine { inboxId: String, digest: List, keyType: String, preKeyIndex: Int -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("sign") - val client = clients[inboxId] ?: throw XMTPException("No client") - val digestBytes = - digest.foldIndexed(ByteArray(digest.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) - } - } - val privateKeyBundle = client.keys - val signedPrivateKey = if (keyType == "prekey") { - privateKeyBundle.preKeysList[preKeyIndex] - } else { - privateKeyBundle.identityKey - } - val privateKey = PrivateKeyBuilder.buildFromSignedPrivateKey(signedPrivateKey) - val signature = PrivateKeyBuilder(privateKey).sign(digestBytes) - signature.toByteArray().map { it.toInt() and 0xFF } - } - } - - AsyncFunction("exportPublicKeyBundle") { inboxId: String -> - // V2 ONLY - logV("exportPublicKeyBundle") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.keys.getPublicKeyBundle().toByteArray().map { it.toInt() and 0xFF } - } - - AsyncFunction("exportKeyBundle") { inboxId: String -> - // V2 ONLY - logV("exportKeyBundle") - val client = clients[inboxId] ?: throw XMTPException("No client") - Base64.encodeToString(client.privateKeyBundle.toByteArray(), NO_WRAP) - } - - // Export the conversation's serialized topic data. - AsyncFunction("exportConversationTopicData") Coroutine { inboxId: String, topic: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("exportConversationTopicData") - val conversation = findConversation(inboxId, topic) - ?: throw XMTPException("no conversation found for $topic") - Base64.encodeToString(conversation.toTopicData().toByteArray(), NO_WRAP) - } - } - - AsyncFunction("getHmacKeys") { inboxId: String -> - logV("getHmacKeys") - val client = clients[inboxId] ?: throw XMTPException("No client") - val hmacKeys = client.conversations.getHmacKeys() - logV("$hmacKeys") - hmacKeys.toByteArray().map { it.toInt() and 0xFF } - } - - // Import a conversation from its serialized topic data. - AsyncFunction("importConversationTopicData") { inboxId: String, topicData: String -> - // V2 ONLY - logV("importConversationTopicData") - val client = clients[inboxId] ?: throw XMTPException("No client") - val data = TopicData.parseFrom(Base64.decode(topicData, NO_WRAP)) - val conversation = client.conversations.importTopicData(data) - conversations[conversation.cacheKey(inboxId)] = conversation - if (conversation.keyMaterial == null) { - logV("Null key material before encode conversation") - } - ConversationWrapper.encode(client, conversation) - } - - // - // Client API - AsyncFunction("canMessage") Coroutine { inboxId: String, peerAddress: String -> - // V2 ONLY + AsyncFunction("canMessage") Coroutine { inboxId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("canMessage") - - val client = clients[inboxId] ?: throw XMTPException("No client") - - client.canMessage(peerAddress) - } - } - - AsyncFunction("canGroupMessage") Coroutine { inboxId: String, peerAddresses: List -> - withContext(Dispatchers.IO) { - logV("canGroupMessage") val client = clients[inboxId] ?: throw XMTPException("No client") - client.canMessageV3(peerAddresses) - } - } - - AsyncFunction("staticCanMessage") Coroutine { peerAddress: String, environment: String, appVersion: String? -> - // V2 ONLY - withContext(Dispatchers.IO) { - try { - logV("staticCanMessage") - val options = ClientOptions(api = apiEnvironments(environment, appVersion)) - Client.canMessage(peerAddress = peerAddress, options = options) - } catch (e: Exception) { - throw XMTPException("Failed to create client: ${e.message}") - } + client.canMessage(peerAddresses) } } @@ -619,8 +376,10 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { try { logV("getOrCreateInboxId") - val options = ClientOptions(api = apiEnvironments(environment, null)) - Client.getOrCreateInboxId(options = options, address = address) + Client.getOrCreateInboxId( + environment = apiEnvironments(environment, null), + address = address + ) } catch (e: Exception) { throw XMTPException("Failed to getOrCreateInboxId: ${e.message}") } @@ -681,46 +440,6 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("sendEncodedContent") Coroutine { inboxId: String, topic: String, encodedContentData: List -> - // V2 ONLY - withContext(Dispatchers.IO) { - val conversation = - findConversation( - inboxId = inboxId, - topic = topic - ) ?: throw XMTPException("no conversation found for $topic") - - val encodedContentDataBytes = - encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) - } - } - val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes) - - conversation.send(encodedContent = encodedContent) - } - } - - AsyncFunction("listConversations") Coroutine { inboxId: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("listConversations") - val client = clients[inboxId] ?: throw XMTPException("No client") - val conversationList = client.conversations.list() - conversationList.map { conversation -> - conversations[conversation.cacheKey(inboxId)] = conversation - if (conversation.keyMaterial == null) { - logV("Null key material before encode conversation") - } - ConversationWrapper.encode(client, conversation) - } - } - } - AsyncFunction("listGroups") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> withContext(Dispatchers.IO) { logV("listGroups") @@ -730,7 +449,7 @@ class XMTPModule : Module() { val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) { client.conversations.listGroups() .sortedByDescending { group -> - group.decryptedMessages(limit = 1).firstOrNull()?.sentAt + group.messages(limit = 1).firstOrNull()?.sent } .let { groups -> if (limit != null && limit > 0) groups.take(limit) else groups @@ -739,87 +458,72 @@ class XMTPModule : Module() { client.conversations.listGroups(limit = limit) } sortedGroupList.map { group -> - groups[group.cacheKey(inboxId)] = group GroupWrapper.encode(client, group, params) } } } - AsyncFunction("listV3Conversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listConversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int? -> withContext(Dispatchers.IO) { - logV("listV3Conversations") + logV("listConversations") val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") val conversations = - client.conversations.listConversations(order = order, limit = limit) + client.conversations.list(order = order, limit = limit) conversations.map { conversation -> - ConversationContainerWrapper.encode(client, conversation, params) + ConversationWrapper.encode(client, conversation, params) } } } - AsyncFunction("listAll") Coroutine { inboxId: String -> + AsyncFunction("listDms") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> withContext(Dispatchers.IO) { + logV("listDms") val client = clients[inboxId] ?: throw XMTPException("No client") - val conversationContainerList = client.conversations.list(includeGroups = true) - conversationContainerList.map { conversation -> - conversations[conversation.cacheKey(inboxId)] = conversation - ConversationContainerWrapper.encode(client, conversation) + val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") + val order = getConversationSortOrder(sortOrder ?: "") + val sortedDmList = if (order == ConversationOrder.LAST_MESSAGE) { + client.conversations.listDms() + .sortedByDescending { dm -> + dm.messages(limit = 1).firstOrNull()?.sent + } + .let { dms -> + if (limit != null && limit > 0) dms.take(limit) else dms + } + } else { + client.conversations.listDms(limit = limit) + } + sortedDmList.map { dm -> + DmWrapper.encode(client, dm, params) } } } - AsyncFunction("loadMessages") Coroutine { inboxId: String, topic: String, limit: Int?, before: Long?, after: Long?, direction: String? -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("loadMessages") - val conversation = - findConversation( - inboxId = inboxId, - topic = topic, - ) ?: throw XMTPException("no conversation found for $topic") - val beforeDate = if (before != null) Date(before) else null - val afterDate = if (after != null) Date(after) else null - - conversation.decryptedMessages( - limit = limit, - before = beforeDate, - after = afterDate, - direction = MessageApiOuterClass.SortDirection.valueOf( - direction ?: "SORT_DIRECTION_DESCENDING" - ) - ) - .map { DecodedMessageWrapper.encode(it) } - } - } - - AsyncFunction("conversationMessages") Coroutine { inboxId: String, conversationId: String, limit: Int?, before: Long?, after: Long?, direction: String? -> + AsyncFunction("conversationMessages") Coroutine { inboxId: String, conversationId: String, limit: Int?, beforeNs: Long?, afterNs: Long?, direction: String? -> withContext(Dispatchers.IO) { logV("conversationMessages") val client = clients[inboxId] ?: throw XMTPException("No client") - val beforeDate = if (before != null) Date(before) else null - val afterDate = if (after != null) Date(after) else null val conversation = client.findConversation(conversationId) - conversation?.decryptedMessages( + conversation?.messages( limit = limit, - before = beforeDate, - after = afterDate, - direction = MessageApiOuterClass.SortDirection.valueOf( - direction ?: "SORT_DIRECTION_DESCENDING" + beforeNs = beforeNs, + afterNs = afterNs, + direction = Message.SortDirection.valueOf( + direction ?: "DESCENDING" ) )?.map { DecodedMessageWrapper.encode(it) } } } - AsyncFunction("findV3Message") Coroutine { inboxId: String, messageId: String -> + AsyncFunction("findMessage") Coroutine { inboxId: String, messageId: String -> withContext(Dispatchers.IO) { - logV("findV3Message") + logV("findMessage") val client = clients[inboxId] ?: throw XMTPException("No client") val message = client.findMessage(messageId) message?.let { - DecodedMessageWrapper.encode(it.decrypt()) + DecodedMessageWrapper.encode(it.decode()) } } } @@ -841,7 +545,7 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) conversation?.let { - ConversationContainerWrapper.encode(client, conversation) + ConversationWrapper.encode(client, conversation) } } } @@ -852,7 +556,7 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversationByTopic(topic) conversation?.let { - ConversationContainerWrapper.encode(client, conversation) + ConversationWrapper.encode(client, conversation) } } } @@ -868,72 +572,6 @@ class XMTPModule : Module() { } } - AsyncFunction("loadBatchMessages") Coroutine { inboxId: String, topics: List -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("loadBatchMessages") - val client = clients[inboxId] ?: throw XMTPException("No client") - val topicsList = mutableListOf>() - topics.forEach { - val jsonObj = JSONObject(it) - val topic = jsonObj.get("topic").toString() - var limit: Int? = null - var before: Long? = null - var after: Long? = null - var direction: MessageApiOuterClass.SortDirection = - MessageApiOuterClass.SortDirection.SORT_DIRECTION_DESCENDING - - try { - limit = jsonObj.get("limit").toString().toInt() - before = jsonObj.get("before").toString().toLong() - after = jsonObj.get("after").toString().toLong() - direction = MessageApiOuterClass.SortDirection.valueOf( - if (jsonObj.get("direction").toString().isNullOrBlank()) { - "SORT_DIRECTION_DESCENDING" - } else { - jsonObj.get("direction").toString() - } - ) - } catch (e: Exception) { - Log.e( - "XMTPModule", - "Pagination given incorrect information ${e.message}" - ) - } - - val page = Pagination( - limit = if (limit != null && limit > 0) limit else null, - before = if (before != null && before > 0) Date(before) else null, - after = if (after != null && after > 0) Date(after) else null, - direction = direction - ) - - topicsList.add(Pair(topic, page)) - } - - client.conversations.listBatchDecryptedMessages(topicsList) - .map { DecodedMessageWrapper.encode(it) } - } - } - - AsyncFunction("sendMessage") Coroutine { inboxId: String, conversationTopic: String, contentJson: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("sendMessage") - val conversation = - findConversation( - inboxId = inboxId, - topic = conversationTopic - ) - ?: throw XMTPException("no conversation found for $conversationTopic") - val sending = ContentJson.fromJson(contentJson) - conversation.send( - content = sending.content, - options = SendOptions(contentType = sending.type) - ) - } - } - AsyncFunction("sendMessageToConversation") Coroutine { inboxId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { logV("sendMessageToConversation") @@ -948,164 +586,27 @@ class XMTPModule : Module() { } } - AsyncFunction("publishPreparedGroupMessages") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("publishPreparedMessages") Coroutine { inboxId: String, id: String -> withContext(Dispatchers.IO) { - logV("publishPreparedGroupMessages") - val group = - findGroup( - inboxId = inboxId, - id = groupId - ) - ?: throw XMTPException("no group found for $groupId") - - group.publishMessages() - } - } - - AsyncFunction("prepareConversationMessage") Coroutine { inboxId: String, id: String, contentJson: String -> - withContext(Dispatchers.IO) { - logV("prepareConversationMessage") + logV("publishPreparedMessages") val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") - val sending = ContentJson.fromJson(contentJson) - conversation.prepareMessageV3( - content = sending.content, - options = SendOptions(contentType = sending.type) - ) + conversation.publishMessages() } } - AsyncFunction("prepareMessage") Coroutine { inboxId: String, conversationTopic: String, contentJson: String -> - // V2 ONLY + AsyncFunction("prepareMessage") Coroutine { inboxId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { logV("prepareMessage") - val conversation = - findConversation( - inboxId = inboxId, - topic = conversationTopic - ) - ?: throw XMTPException("no conversation found for $conversationTopic") + val client = clients[inboxId] ?: throw XMTPException("No client") + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) - val prepared = conversation.prepareMessage( + conversation.prepareMessage( content = sending.content, options = SendOptions(contentType = sending.type) ) - val preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 - val preparedFile = File.createTempFile(prepared.messageId, null) - preparedFile.writeBytes(prepared.toSerializedData()) - PreparedLocalMessage( - messageId = prepared.messageId, - preparedFileUri = preparedFile.toURI().toString(), - preparedAt = preparedAtMillis, - ).toJson() - } - } - - AsyncFunction("prepareEncodedMessage") Coroutine { inboxId: String, conversationTopic: String, encodedContentData: List -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("prepareEncodedMessage") - val conversation = - findConversation( - inboxId = inboxId, - topic = conversationTopic - ) - ?: throw XMTPException("no conversation found for $conversationTopic") - - val encodedContentDataBytes = - encodedContentData.foldIndexed(ByteArray(encodedContentData.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) - } - } - val encodedContent = EncodedContent.parseFrom(encodedContentDataBytes) - - val prepared = conversation.prepareMessage( - encodedContent = encodedContent, - ) - val preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 - val preparedFile = File.createTempFile(prepared.messageId, null) - preparedFile.writeBytes(prepared.toSerializedData()) - PreparedLocalMessage( - messageId = prepared.messageId, - preparedFileUri = preparedFile.toURI().toString(), - preparedAt = preparedAtMillis, - ).toJson() - } - } - - AsyncFunction("sendPreparedMessage") Coroutine { inboxId: String, preparedLocalMessageJson: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("sendPreparedMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") - val local = PreparedLocalMessage.fromJson(preparedLocalMessageJson) - val preparedFileUrl = Uri.parse(local.preparedFileUri) - val contentResolver = appContext.reactContext?.contentResolver!! - val preparedData = contentResolver.openInputStream(preparedFileUrl)!! - .use { it.buffered().readBytes() } - val prepared = PreparedMessage.fromSerializedData(preparedData) - client.publish(envelopes = prepared.envelopes) - try { - contentResolver.delete(preparedFileUrl, null, null) - } catch (ignore: Exception) { - /* ignore: the sending succeeds even if we fail to rm the tmp file afterward */ - } - prepared.messageId - } - } - - AsyncFunction("createConversation") Coroutine { inboxId: String, peerAddress: String, contextJson: String, consentProofPayload: List -> - // V2 Only - withContext(Dispatchers.IO) { - logV("createConversation: $contextJson") - val client = clients[inboxId] ?: throw XMTPException("No client") - val context = JsonParser.parseString(contextJson).asJsonObject - - var consentProof: ConsentProofPayload? = null - if (consentProofPayload.isNotEmpty()) { - val consentProofDataBytes = - consentProofPayload.foldIndexed(ByteArray(consentProofPayload.size)) { i, a, v -> - a.apply { - set( - i, - v.toByte() - ) - } - } - consentProof = ConsentProofPayload.parseFrom(consentProofDataBytes) - } - - val conversation = client.conversations.newConversation( - peerAddress, - context = InvitationV1ContextBuilder.buildFromConversation( - conversationId = when { - context.has("conversationID") -> context.get("conversationID").asString - else -> "" - }, - metadata = when { - context.has("metadata") -> { - val metadata = context.get("metadata").asJsonObject - metadata.entrySet() - .associate { (key, value) -> key to value.asString } - } - - else -> mapOf() - }, - ), - consentProof - ) - if (conversation.keyMaterial == null) { - logV("Null key material before encode conversation") - } - if (conversation.consentProof == null) { - logV("Null consent before encode conversation") - } - ConversationWrapper.encode(client, conversation) } } @@ -1163,18 +664,19 @@ class XMTPModule : Module() { } - AsyncFunction("listMemberInboxIds") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("listMemberInboxIds") Coroutine { inboxId: String, id: String -> withContext(Dispatchers.IO) { logV("listMembers") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, groupId) - group?.members()?.map { it.inboxId } + val conversation = client.findConversation(id) + ?: throw XMTPException("no conversation found for $id") + conversation.members().map { it.inboxId } } } AsyncFunction("dmPeerInboxId") Coroutine { inboxId: String, dmId: String -> withContext(Dispatchers.IO) { - logV("listPeerInboxId") + logV("dmPeerInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(dmId) ?: throw XMTPException("no conversation found for $dmId") @@ -1197,7 +699,7 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { logV("syncConversations") val client = clients[inboxId] ?: throw XMTPException("No client") - client.conversations.syncConversations() + client.conversations.sync() } } @@ -1205,10 +707,8 @@ class XMTPModule : Module() { withContext(Dispatchers.IO) { logV("syncAllConversations") val client = clients[inboxId] ?: throw XMTPException("No client") - // Expo Modules do not support UInt, so we need to convert to Int val numGroupsSyncedInt: Int = - client.conversations.syncAllConversations()?.toInt() - ?: throw IllegalArgumentException("Value cannot be null") + client.conversations.syncAllConversations().toInt() numGroupsSyncedInt } } @@ -1223,337 +723,339 @@ class XMTPModule : Module() { } } - AsyncFunction("addGroupMembers") Coroutine { inboxId: String, id: String, peerAddresses: List -> + AsyncFunction("addGroupMembers") Coroutine { inboxId: String, groupId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("addGroupMembers") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.addMembers(peerAddresses) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.addMembers(peerAddresses) } } - AsyncFunction("removeGroupMembers") Coroutine { inboxId: String, id: String, peerAddresses: List -> + AsyncFunction("removeGroupMembers") Coroutine { inboxId: String, groupId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("removeGroupMembers") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.removeMembers(peerAddresses) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.removeMembers(peerAddresses) } } - AsyncFunction("addGroupMembersByInboxId") Coroutine { inboxId: String, id: String, peerInboxIds: List -> + AsyncFunction("addGroupMembersByInboxId") Coroutine { inboxId: String, groupId: String, peerInboxIds: List -> withContext(Dispatchers.IO) { logV("addGroupMembersByInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.addMembersByInboxId(peerInboxIds) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.addMembersByInboxId(peerInboxIds) } } - AsyncFunction("removeGroupMembersByInboxId") Coroutine { inboxId: String, id: String, peerInboxIds: List -> + AsyncFunction("removeGroupMembersByInboxId") Coroutine { inboxId: String, groupId: String, peerInboxIds: List -> withContext(Dispatchers.IO) { logV("removeGroupMembersByInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.removeMembersByInboxId(peerInboxIds) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.removeMembersByInboxId(peerInboxIds) } } - AsyncFunction("groupName") Coroutine { inboxId: String, id: String -> + AsyncFunction("groupName") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupName") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.name + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.name } } - AsyncFunction("updateGroupName") Coroutine { inboxId: String, id: String, groupName: String -> + AsyncFunction("updateGroupName") Coroutine { inboxId: String, groupId: String, groupName: String -> withContext(Dispatchers.IO) { logV("updateGroupName") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.updateGroupName(groupName) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupName(groupName) } } - AsyncFunction("groupImageUrlSquare") Coroutine { inboxId: String, id: String -> + AsyncFunction("groupImageUrlSquare") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupImageUrlSquare") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.imageUrlSquare + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.imageUrlSquare } } - AsyncFunction("updateGroupImageUrlSquare") Coroutine { inboxId: String, id: String, groupImageUrl: String -> + AsyncFunction("updateGroupImageUrlSquare") Coroutine { inboxId: String, groupId: String, groupImageUrl: String -> withContext(Dispatchers.IO) { logV("updateGroupImageUrlSquare") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.updateGroupImageUrlSquare(groupImageUrl) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupImageUrlSquare(groupImageUrl) } } - AsyncFunction("groupDescription") Coroutine { inboxId: String, id: String -> + AsyncFunction("groupDescription") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupDescription") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.description + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.description } } - AsyncFunction("updateGroupDescription") Coroutine { inboxId: String, id: String, groupDescription: String -> + AsyncFunction("updateGroupDescription") Coroutine { inboxId: String, groupId: String, groupDescription: String -> withContext(Dispatchers.IO) { logV("updateGroupDescription") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.updateGroupDescription(groupDescription) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupDescription(groupDescription) } } - AsyncFunction("groupPinnedFrameUrl") Coroutine { inboxId: String, id: String -> + AsyncFunction("groupPinnedFrameUrl") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupPinnedFrameUrl") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.pinnedFrameUrl + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.pinnedFrameUrl } } - AsyncFunction("updateGroupPinnedFrameUrl") Coroutine { inboxId: String, id: String, pinnedFrameUrl: String -> + AsyncFunction("updateGroupPinnedFrameUrl") Coroutine { inboxId: String, groupId: String, pinnedFrameUrl: String -> withContext(Dispatchers.IO) { logV("updateGroupPinnedFrameUrl") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.updateGroupPinnedFrameUrl(pinnedFrameUrl) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupPinnedFrameUrl(pinnedFrameUrl) } } - AsyncFunction("isGroupActive") Coroutine { inboxId: String, id: String -> + AsyncFunction("isGroupActive") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("isGroupActive") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.isActive() + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.isActive() } } - AsyncFunction("addedByInboxId") Coroutine { inboxId: String, id: String -> + AsyncFunction("addedByInboxId") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("addedByInboxId") - val group = findGroup(inboxId, id) ?: throw XMTPException("No group found") - + val client = clients[inboxId] ?: throw XMTPException("No client") + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") group.addedByInboxId() } } - AsyncFunction("creatorInboxId") Coroutine { inboxId: String, id: String -> + AsyncFunction("creatorInboxId") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("creatorInboxId") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.creatorInboxId() + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.creatorInboxId() } } - AsyncFunction("isAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("isAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("isGroupAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.isAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.isAdmin(inboxId) } } - AsyncFunction("isSuperAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("isSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("isSuperAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.isSuperAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.isSuperAdmin(inboxId) } } - AsyncFunction("listAdmins") Coroutine { inboxId: String, id: String -> + AsyncFunction("listAdmins") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("listAdmins") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.listAdmins() + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.listAdmins() } } - AsyncFunction("listSuperAdmins") Coroutine { inboxId: String, id: String -> + AsyncFunction("listSuperAdmins") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("listSuperAdmins") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - - group?.listSuperAdmins() + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.listSuperAdmins() } } - AsyncFunction("addAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("addAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("addAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - group?.addAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.addAdmin(inboxId) } } - AsyncFunction("addSuperAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("addSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("addSuperAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.addSuperAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.addSuperAdmin(inboxId) } } - AsyncFunction("removeAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("removeAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("removeAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.removeAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.removeAdmin(inboxId) } } - AsyncFunction("removeSuperAdmin") Coroutine { clientInboxId: String, id: String, inboxId: String -> + AsyncFunction("removeSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("removeSuperAdmin") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.removeSuperAdmin(inboxId) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.removeSuperAdmin(inboxId) } } - AsyncFunction("updateAddMemberPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateAddMemberPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateAddMemberPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateAddMemberPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateAddMemberPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateRemoveMemberPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateRemoveMemberPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateRemoveMemberPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateRemoveMemberPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateRemoveMemberPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateAddAdminPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateAddAdminPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateAddAdminPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateAddAdminPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateAddAdminPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateRemoveAdminPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateRemoveAdminPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateRemoveAdminPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateRemoveAdminPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateRemoveAdminPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupNamePermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateGroupNamePermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupNamePermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateGroupNamePermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupNamePermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupImageUrlSquarePermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateGroupImageUrlSquarePermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupImageUrlSquarePermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateGroupImageUrlSquarePermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupImageUrlSquarePermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupDescriptionPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateGroupDescriptionPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupDescriptionPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateGroupDescriptionPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupDescriptionPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupPinnedFrameUrlPermission") Coroutine { clientInboxId: String, id: String, newPermission: String -> + AsyncFunction("updateGroupPinnedFrameUrlPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupPinnedFrameUrlPermission") val client = clients[clientInboxId] ?: throw XMTPException("No client") - val group = findGroup(clientInboxId, id) - - group?.updateGroupPinnedFrameUrlPermission(getPermissionOption(newPermission)) + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") + group.updateGroupPinnedFrameUrlPermission(getPermissionOption(newPermission)) } } - AsyncFunction("permissionPolicySet") Coroutine { inboxId: String, id: String -> + AsyncFunction("permissionPolicySet") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupImageUrlSquare") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = findGroup(inboxId, id) - + val group = client.findGroup(groupId) + ?: throw XMTPException("no group found for $groupId") val permissionPolicySet = group?.permissionPolicySet() if (permissionPolicySet != null) { PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) } else { - throw XMTPException("Permission policy set not found for group: $id") + throw XMTPException("Permission policy set not found for group: $groupId") } } } - AsyncFunction("processConversationMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> + AsyncFunction("processMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> withContext(Dispatchers.IO) { - logV("processGroupMessage") + logV("processMessage") val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") val message = conversation.processMessage(Base64.decode(encryptedMessage, NO_WRAP)) - DecodedMessageWrapper.encodeMap(message.decrypt()) + DecodedMessageWrapper.encodeMap(message.decode()) } } @@ -1562,114 +1064,14 @@ class XMTPModule : Module() { logV("processWelcomeMessage") val client = clients[inboxId] ?: throw XMTPException("No client") - val group = - client.conversations.fromWelcome(Base64.decode(encryptedMessage, NO_WRAP)) - GroupWrapper.encode(client, group) - } - } - - AsyncFunction("processConversationWelcomeMessage") Coroutine { inboxId: String, encryptedMessage: String -> - withContext(Dispatchers.IO) { - logV("processConversationWelcomeMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") - val conversation = - client.conversations.conversationFromWelcome( + client.conversations.fromWelcome( Base64.decode( encryptedMessage, NO_WRAP ) ) - ConversationContainerWrapper.encode(client, conversation) - } - } - - Function("subscribeToConversations") { inboxId: String -> - // V2 ONLY - logV("subscribeToConversations") - subscribeToConversations(inboxId = inboxId) - } - - Function("subscribeToGroups") { inboxId: String -> - logV("subscribeToGroups") - subscribeToGroups(inboxId = inboxId) - } - - Function("subscribeToAll") { inboxId: String -> - logV("subscribeToAll") - subscribeToAll(inboxId = inboxId) - } - - Function("subscribeToAllMessages") { inboxId: String, includeGroups: Boolean -> - logV("subscribeToAllMessages") - subscribeToAllMessages(inboxId = inboxId, includeGroups = includeGroups) - } - - Function("subscribeToAllGroupMessages") { inboxId: String -> - logV("subscribeToAllGroupMessages") - subscribeToAllGroupMessages(inboxId = inboxId) - } - - AsyncFunction("subscribeToMessages") Coroutine { inboxId: String, topic: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("subscribeToMessages") - subscribeToMessages( - inboxId = inboxId, - topic = topic - ) - } - } - - AsyncFunction("subscribeToGroupMessages") Coroutine { inboxId: String, id: String -> - withContext(Dispatchers.IO) { - logV("subscribeToGroupMessages") - subscribeToGroupMessages( - inboxId = inboxId, - id = id - ) - } - } - - Function("unsubscribeFromConversations") { inboxId: String -> - // V2 ONLY - logV("unsubscribeFromConversations") - subscriptions[getConversationsKey(inboxId)]?.cancel() - } - - Function("unsubscribeFromGroups") { inboxId: String -> - logV("unsubscribeFromGroups") - subscriptions[getGroupsKey(inboxId)]?.cancel() - } - - Function("unsubscribeFromAllMessages") { inboxId: String -> - logV("unsubscribeFromAllMessages") - subscriptions[getMessagesKey(inboxId)]?.cancel() - } - - Function("unsubscribeFromAllGroupMessages") { inboxId: String -> - logV("unsubscribeFromAllGroupMessages") - subscriptions[getGroupMessagesKey(inboxId)]?.cancel() - } - - AsyncFunction("unsubscribeFromMessages") Coroutine { inboxId: String, topic: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("unsubscribeFromMessages") - unsubscribeFromMessages( - inboxId = inboxId, - topic = topic - ) - } - } - - AsyncFunction("unsubscribeFromGroupMessages") Coroutine { inboxId: String, id: String -> - withContext(Dispatchers.IO) { - logV("unsubscribeFromGroupMessages") - unsubscribeFromGroupMessages( - inboxId = inboxId, - id = id - ) + ConversationWrapper.encode(client, conversation) } } @@ -1687,21 +1089,8 @@ class XMTPModule : Module() { } val client = clients[inboxId] ?: throw XMTPException("No client") - val hmacKeysResult = client.conversations.getHmacKeys() val subscriptions = topics.map { - val hmacKeys = hmacKeysResult.hmacKeysMap - val result = hmacKeys[it]?.valuesList?.map { hmacKey -> - Service.Subscription.HmacKey.newBuilder().also { sub_key -> - sub_key.key = hmacKey.hmacKey - sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch - }.build() - } - Service.Subscription.newBuilder().also { sub -> - sub.addAllHmacKeys(result) - if (!result.isNullOrEmpty()) { - sub.addAllHmacKeys(result) - } sub.topic = it }.build() } @@ -1710,103 +1099,35 @@ class XMTPModule : Module() { } } - AsyncFunction("decodeMessage") Coroutine { inboxId: String, topic: String, encryptedMessage: String -> - // V2 ONLY - withContext(Dispatchers.IO) { - logV("decodeMessage") - val encryptedMessageData = Base64.decode(encryptedMessage, NO_WRAP) - val envelope = EnvelopeBuilder.buildFromString(topic, Date(), encryptedMessageData) - val conversation = - findConversation( - inboxId = inboxId, - topic = topic - ) - ?: throw XMTPException("no conversation found for $topic") - val decodedMessage = conversation.decrypt(envelope) - DecodedMessageWrapper.encode(decodedMessage) - } - } - - AsyncFunction("isAllowed") Coroutine { inboxId: String, address: String -> - withContext(Dispatchers.IO) { - logV("isAllowed") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.isAllowed(address) - } - } - - AsyncFunction("isDenied") Coroutine { inboxId: String, address: String -> - withContext(Dispatchers.IO) { - logV("isDenied") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.isDenied(address) - } - } - - AsyncFunction("denyContacts") Coroutine { inboxId: String, addresses: List -> - withContext(Dispatchers.IO) { - logV("denyContacts") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.deny(addresses) - } - } - - AsyncFunction("allowContacts") Coroutine { inboxId: String, addresses: List -> + AsyncFunction("setConsentState") Coroutine { inboxId: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.allow(addresses) - } - } - - AsyncFunction("isInboxAllowed") Coroutine { clientInboxId: String, inboxId: String -> - withContext(Dispatchers.IO) { - logV("isInboxIdAllowed") - val client = clients[clientInboxId] ?: throw XMTPException("No client") - client.contacts.isInboxAllowed(inboxId) - } - } - - AsyncFunction("isInboxDenied") Coroutine { clientInboxId: String, inboxId: String -> - withContext(Dispatchers.IO) { - logV("isInboxIdDenied") - val client = clients[clientInboxId] ?: throw XMTPException("No client") - client.contacts.isInboxDenied(inboxId) + val consentList = client.preferences.consentList.setConsentState() } } - AsyncFunction("denyInboxes") Coroutine { inboxId: String, inboxIds: List -> + AsyncFunction("consentAddressState") Coroutine { inboxId: String, peerAddress: String -> withContext(Dispatchers.IO) { - logV("denyInboxIds") val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.denyInboxes(inboxIds) + consentStateToString(client.preferences.consentList.addressState(peerAddress)) } } - AsyncFunction("allowInboxes") Coroutine { inboxId: String, inboxIds: List -> + AsyncFunction("consentInboxIdState") Coroutine { inboxId: String, peerInboxId: String -> withContext(Dispatchers.IO) { - logV("allowInboxIds") val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.allowInboxes(inboxIds) + consentStateToString(client.preferences.consentList.inboxIdState(peerInboxId)) } } - AsyncFunction("refreshConsentList") Coroutine { inboxId: String -> + AsyncFunction("consentConversationIdState") Coroutine { inboxId: String, conversationId: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") - val consentList = client.contacts.refreshConsentList() - consentList.entries.map { ConsentWrapper.encode(it.value) } - } - } - - AsyncFunction("conversationConsentState") Coroutine { inboxId: String, conversationTopic: String -> - withContext(Dispatchers.IO) { - val conversation = findConversation(inboxId, conversationTopic) - ?: throw XMTPException("no conversation found for $conversationTopic") - consentStateToString(conversation.consentState()) + consentStateToString(client.preferences.consentList.conversationState(conversationId)) } } - AsyncFunction("conversationV3ConsentState") Coroutine { inboxId: String, conversationId: String -> + AsyncFunction("conversationConsentState") Coroutine { inboxId: String, conversationId: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) @@ -1815,56 +1136,11 @@ class XMTPModule : Module() { } } - AsyncFunction("consentList") { inboxId: String -> - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.consentList.entries.map { ConsentWrapper.encode(it.value) } - } - - Function("preCreateIdentityCallbackCompleted") { - logV("preCreateIdentityCallbackCompleted") - preCreateIdentityCallbackDeferred?.complete(Unit) - } - - Function("preEnableIdentityCallbackCompleted") { - logV("preEnableIdentityCallbackCompleted") - preEnableIdentityCallbackDeferred?.complete(Unit) - } - Function("preAuthenticateToInboxCallbackCompleted") { logV("preAuthenticateToInboxCallbackCompleted") preAuthenticateToInboxCallbackDeferred?.complete(Unit) } - AsyncFunction("allowGroups") Coroutine { inboxId: String, groupIds: List -> - withContext(Dispatchers.IO) { - logV("allowGroups") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.allowGroups(groupIds) - } - } - - AsyncFunction("denyGroups") Coroutine { inboxId: String, groupIds: List -> - withContext(Dispatchers.IO) { - logV("denyGroups") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.denyGroups(groupIds) - } - } - - AsyncFunction("isGroupAllowed") Coroutine { inboxId: String, groupId: String -> - withContext(Dispatchers.IO) { - logV("isGroupAllowed") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.isGroupAllowed(groupId) - } - } - AsyncFunction("isGroupDenied") Coroutine { inboxId: String, groupId: String -> - withContext(Dispatchers.IO) { - logV("isGroupDenied") - val client = clients[inboxId] ?: throw XMTPException("No client") - client.contacts.isGroupDenied(groupId) - } - } AsyncFunction("updateConversationConsent") Coroutine { inboxId: String, conversationId: String, state: String -> withContext(Dispatchers.IO) { logV("updateConversationConsent") @@ -1894,14 +1170,15 @@ class XMTPModule : Module() { } } - Function("subscribeToV3Conversations") { inboxId: String -> - logV("subscribeToV3Conversations") - subscribeToV3Conversations(inboxId = inboxId) + Function("subscribeToConversations") { inboxId: String, type: String -> + logV("subscribeToConversations") + + subscribeToConversations(inboxId = inboxId, getStreamType(type)) } - Function("subscribeToAllConversationMessages") { inboxId: String -> - logV("subscribeToAllConversationMessages") - subscribeToAllConversationMessages(inboxId = inboxId) + Function("subscribeToAllMessages") { inboxId: String, type: String -> + logV("subscribeToAllMessages") + subscribeToAllMessages(inboxId = inboxId, getStreamType(type)) } AsyncFunction("subscribeToConversationMessages") Coroutine { inboxId: String, id: String -> @@ -1914,14 +1191,14 @@ class XMTPModule : Module() { } } - Function("unsubscribeFromAllConversationMessages") { inboxId: String -> - logV("unsubscribeFromAllConversationMessages") - subscriptions[getConversationMessagesKey(inboxId)]?.cancel() + Function("unsubscribeFromAllMessages") { inboxId: String -> + logV("unsubscribeFromAllMessages") + subscriptions[getMessagesKey(inboxId)]?.cancel() } - Function("unsubscribeFromV3Conversations") { inboxId: String -> - logV("unsubscribeFromV3Conversations") - subscriptions[getV3ConversationsKey(inboxId)]?.cancel() + Function("unsubscribeFromConversations") { inboxId: String -> + logV("unsubscribeFromConversations") + subscriptions[getConversationsKey(inboxId)]?.cancel() } AsyncFunction("unsubscribeFromConversationMessages") Coroutine { inboxId: String, id: String -> @@ -1949,6 +1226,14 @@ class XMTPModule : Module() { } } + private fun getStreamType(typeString: String): ConversationType { + return when (typeString) { + "groups" -> GROUPS + "dms" -> DMS + else -> ALL + } + } + private fun getConsentState(stateString: String): ConsentState { return when (stateString) { "allowed" -> ConsentState.ALLOWED @@ -1964,112 +1249,19 @@ class XMTPModule : Module() { } } - private suspend fun findConversation( - inboxId: String, - topic: String, - ): Conversation? { + private fun subscribeToConversations(inboxId: String, type: ConversationType) { val client = clients[inboxId] ?: throw XMTPException("No client") - val cacheKey = "${inboxId}:${topic}" - val cacheConversation = conversations[cacheKey] - if (cacheConversation != null) { - return cacheConversation - } else { - val conversation = client.conversations.list() - .firstOrNull { it.topic == topic } - if (conversation != null) { - conversations[conversation.cacheKey(inboxId)] = conversation - return conversation - } - } - return null - } - - private fun findGroup( - inboxId: String, - id: String, - ): Group? { - val client = clients[inboxId] ?: throw XMTPException("No client") - - val cacheKey = "${inboxId}:${id}" - val cacheGroup = groups[cacheKey] - if (cacheGroup != null) { - return cacheGroup - } else { - val group = client.findGroup(id) - if (group != null) { - groups[group.cacheKey(inboxId)] = group - return group - } - } - return null - } - - private fun subscribeToConversations(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getConversationsKey(inboxId)]?.cancel() - subscriptions[getConversationsKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.stream().collect { conversation -> - run { - if (conversation.keyMaterial == null) { - logV("Null key material before encode conversation") - } - sendEvent( - "conversation", - mapOf( - "inboxId" to inboxId, - "conversation" to ConversationWrapper.encodeToObj( - client, - conversation - ) - ) - ) - } - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in conversations subscription: $e") - subscriptions[getConversationsKey(inboxId)]?.cancel() - } - } - } - - private fun subscribeToGroups(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getGroupsKey(client.inboxId)]?.cancel() - subscriptions[getGroupsKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.streamGroups().collect { group -> - sendEvent( - "group", - mapOf( - "inboxId" to inboxId, - "group" to GroupWrapper.encodeToObj(client, group) - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in group subscription: $e") - subscriptions[getGroupsKey(client.inboxId)]?.cancel() - } - } - } - - private fun subscribeToV3Conversations(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() - subscriptions[getV3ConversationsKey(client.inboxId)] = + subscriptions[getConversationsKey(client.inboxId)]?.cancel() + subscriptions[getConversationsKey(client.inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { - client.conversations.streamConversations().collect { conversation -> + client.conversations.stream(type).collect { conversation -> sendEvent( - "conversationV3", + "conversation", mapOf( "inboxId" to inboxId, - "conversation" to ConversationContainerWrapper.encodeToObj( + "conversation" to ConversationWrapper.encodeToObj( client, conversation ) @@ -2078,68 +1270,20 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in group subscription: $e") - subscriptions[getV3ConversationsKey(client.inboxId)]?.cancel() + subscriptions[getConversationsKey(client.inboxId)]?.cancel() } } } - private fun subscribeToAll(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getConversationsKey(inboxId)]?.cancel() - subscriptions[getConversationsKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.streamAll().collect { conversation -> - sendEvent( - "conversationContainer", - mapOf( - "inboxId" to inboxId, - "conversationContainer" to ConversationContainerWrapper.encodeToObj( - client, - conversation - ) - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in subscription to groups + conversations: $e") - subscriptions[getConversationsKey(inboxId)]?.cancel() - } - } - } - - private fun subscribeToAllMessages(inboxId: String, includeGroups: Boolean = false) { + private fun subscribeToAllMessages(inboxId: String, type: ConversationType) { val client = clients[inboxId] ?: throw XMTPException("No client") subscriptions[getMessagesKey(inboxId)]?.cancel() subscriptions[getMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { - client.conversations.streamAllDecryptedMessages(includeGroups = includeGroups) - .collect { message -> - sendEvent( - "message", - mapOf( - "inboxId" to inboxId, - "message" to DecodedMessageWrapper.encodeMap(message), - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in all messages subscription: $e") - subscriptions[getMessagesKey(inboxId)]?.cancel() - } - } - } - - private fun subscribeToAllGroupMessages(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getGroupMessagesKey(inboxId)]?.cancel() - subscriptions[getGroupMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.streamAllGroupDecryptedMessages().collect { message -> + client.conversations.streamAllMessages(type).collect { message -> sendEvent( - "allGroupMessage", + "allMessages", mapOf( "inboxId" to inboxId, "message" to DecodedMessageWrapper.encodeMap(message), @@ -2148,99 +1292,22 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in all group messages subscription: $e") - subscriptions[getGroupMessagesKey(inboxId)]?.cancel() - } - } - } - - private fun subscribeToAllConversationMessages(inboxId: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - subscriptions[getConversationMessagesKey(inboxId)]?.cancel() - subscriptions[getConversationMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { - try { - client.conversations.streamAllConversationDecryptedMessages().collect { message -> - sendEvent( - "allConversationMessages", - mapOf( - "inboxId" to inboxId, - "message" to DecodedMessageWrapper.encodeMap(message), - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in all group messages subscription: $e") - subscriptions[getConversationMessagesKey(inboxId)]?.cancel() + subscriptions[getMessagesKey(inboxId)]?.cancel() } } } - private suspend fun subscribeToMessages(inboxId: String, topic: String) { - val conversation = - findConversation( - inboxId = inboxId, - topic = topic - ) ?: return - subscriptions[conversation.cacheKey(inboxId)]?.cancel() - subscriptions[conversation.cacheKey(inboxId)] = - CoroutineScope(Dispatchers.IO).launch { - try { - conversation.streamDecryptedMessages().collect { message -> - sendEvent( - "conversationMessage", - mapOf( - "inboxId" to inboxId, - "message" to DecodedMessageWrapper.encodeMap(message), - "topic" to topic, - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in messages subscription: $e") - subscriptions[conversation.cacheKey(inboxId)]?.cancel() - } - } - } - - private suspend fun subscribeToGroupMessages(inboxId: String, id: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") - val group = - findGroup( - inboxId = inboxId, - id = id - ) ?: return - subscriptions[group.cacheKey(inboxId)]?.cancel() - subscriptions[group.cacheKey(inboxId)] = - CoroutineScope(Dispatchers.IO).launch { - try { - group.streamDecryptedMessages().collect { message -> - sendEvent( - "groupMessage", - mapOf( - "inboxId" to inboxId, - "message" to DecodedMessageWrapper.encodeMap(message), - "groupId" to id, - ) - ) - } - } catch (e: Exception) { - Log.e("XMTPModule", "Error in messages subscription: $e") - subscriptions[group.cacheKey(inboxId)]?.cancel() - } - } - } - private suspend fun subscribeToConversationMessages(inboxId: String, id: String) { val client = clients[inboxId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") - subscriptions[conversation.cacheKeyV3(inboxId)]?.cancel() - subscriptions[conversation.cacheKeyV3(inboxId)] = + subscriptions[conversation.cacheKey(inboxId)]?.cancel() + subscriptions[conversation.cacheKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { try { - conversation.streamDecryptedMessages().collect { message -> + conversation.streamMessages().collect { message -> sendEvent( - "conversationV3Message", + "message", mapOf( "inboxId" to inboxId, "message" to DecodedMessageWrapper.encodeMap(message), @@ -2250,7 +1317,7 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in messages subscription: $e") - subscriptions[conversation?.cacheKey(inboxId)]?.cancel() + subscriptions[conversation.cacheKey(inboxId)]?.cancel() } } } @@ -2259,52 +1326,10 @@ class XMTPModule : Module() { return "messages:$inboxId" } - private fun getGroupMessagesKey(inboxId: String): String { - return "groupMessages:$inboxId" - } - - private fun getConversationMessagesKey(inboxId: String): String { - return "conversationMessages:$inboxId" - } - private fun getConversationsKey(inboxId: String): String { return "conversations:$inboxId" } - private fun getV3ConversationsKey(inboxId: String): String { - return "conversationsV3:$inboxId" - } - - private fun getGroupsKey(inboxId: String): String { - return "groups:$inboxId" - } - - private suspend fun unsubscribeFromMessages( - inboxId: String, - topic: String, - ) { - val conversation = - findConversation( - inboxId = inboxId, - topic = topic - ) ?: return - subscriptions[conversation.cacheKey(inboxId)]?.cancel() - } - - private fun unsubscribeFromGroupMessages( - inboxId: String, - id: String, - ) { - val client = clients[inboxId] ?: throw XMTPException("No client") - - val group = - findGroup( - inboxId = inboxId, - id = id - ) ?: return - subscriptions[group.cacheKey(inboxId)]?.cancel() - } - private fun unsubscribeFromConversationMessages( inboxId: String, id: String, @@ -2320,18 +1345,6 @@ class XMTPModule : Module() { } } - private val preEnableIdentityCallback: suspend () -> Unit = { - sendEvent("preEnableIdentityCallback") - preEnableIdentityCallbackDeferred?.await() - preCreateIdentityCallbackDeferred == null - } - - private val preCreateIdentityCallback: suspend () -> Unit = { - sendEvent("preCreateIdentityCallback") - preCreateIdentityCallbackDeferred?.await() - preCreateIdentityCallbackDeferred = null - } - private val preAuthenticateToInboxCallback: suspend () -> Unit = { sendEvent("preAuthenticateToInboxCallback") preAuthenticateToInboxCallbackDeferred?.await() diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt index a1a461cea..feeae6b21 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt @@ -6,7 +6,6 @@ import org.xmtp.android.library.WalletType class AuthParamsWrapper( val environment: String, val appVersion: String?, - val enableV3: Boolean = false, val dbDirectory: String?, val historySyncUrl: String?, val walletType: WalletType = WalletType.EOA, @@ -19,7 +18,6 @@ class AuthParamsWrapper( return AuthParamsWrapper( jsonOptions.get("environment").asString, if (jsonOptions.has("appVersion")) jsonOptions.get("appVersion").asString else null, - if (jsonOptions.has("enableV3")) jsonOptions.get("enableV3").asBoolean else false, if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null, if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null, if (jsonOptions.has("walletType")) { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt deleted file mode 100644 index 546fe2b16..000000000 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationContainerWrapper.kt +++ /dev/null @@ -1,43 +0,0 @@ -package expo.modules.xmtpreactnativesdk.wrappers - -import com.google.gson.GsonBuilder -import org.xmtp.android.library.Client -import org.xmtp.android.library.Conversation - -class ConversationContainerWrapper { - - companion object { - suspend fun encodeToObj( - client: Client, - conversation: Conversation, - conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), - ): Map { - return when (conversation.version) { - Conversation.Version.GROUP -> { - val group = (conversation as Conversation.Group).group - GroupWrapper.encodeToObj(client, group, conversationParams) - } - - Conversation.Version.DM -> { - val dm = (conversation as Conversation.Dm).dm - DmWrapper.encodeToObj(client, dm, conversationParams) - } - - else -> { - ConversationWrapper.encodeToObj(client, conversation) - } - } - } - - suspend fun encode( - client: Client, - conversation: Conversation, - conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), - ): String { - val gson = GsonBuilder().create() - val obj = - ConversationContainerWrapper.encodeToObj(client, conversation, conversationParams) - return gson.toJson(obj) - } - } -} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt index 09854f46d..9e4e1caa6 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt @@ -1,6 +1,5 @@ package expo.modules.xmtpreactnativesdk.wrappers -import android.util.Base64 import com.google.gson.GsonBuilder import org.xmtp.android.library.Client import org.xmtp.android.library.Conversation @@ -8,33 +7,33 @@ import org.xmtp.android.library.Conversation class ConversationWrapper { companion object { - fun encodeToObj(client: Client, conversation: Conversation): Map { - val context = when (conversation.version) { - Conversation.Version.V2 -> mapOf( - "conversationID" to (conversation.conversationId ?: ""), - // TODO: expose the context/metadata explicitly in xmtp-android - "metadata" to conversation.toTopicData().invitation.context.metadataMap, - ) + fun encodeToObj( + client: Client, + conversation: Conversation, + conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): Map { + return when (conversation.type) { + Conversation.Type.GROUP -> { + val group = (conversation as Conversation.Group).group + GroupWrapper.encodeToObj(client, group, conversationParams) + } - else -> mapOf() + Conversation.Type.DM -> { + val dm = (conversation as Conversation.Dm).dm + DmWrapper.encodeToObj(client, dm, conversationParams) + } } - return mapOf( - "clientAddress" to client.address, - "createdAt" to conversation.createdAt.time, - "context" to context, - "topic" to conversation.topic, - "peerAddress" to conversation.peerAddress, - "version" to "DIRECT", - "conversationID" to (conversation.conversationId ?: ""), - "keyMaterial" to (conversation.keyMaterial?.let { Base64.encodeToString(it, Base64.NO_WRAP) } ?: ""), - "consentProof" to if (conversation.consentProof != null) Base64.encodeToString(conversation.consentProof?.toByteArray(), Base64.NO_WRAP) else null - ) } - fun encode(client: Client, conversation: Conversation): String { + fun encode( + client: Client, + conversation: Conversation, + conversationParams: ConversationParamsWrapper = ConversationParamsWrapper(), + ): String { val gson = GsonBuilder().create() - val obj = encodeToObj(client, conversation) + val obj = + ConversationWrapper.encodeToObj(client, conversation, conversationParams) return gson.toJson(obj) } } -} \ No newline at end of file +} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt index ab871f023..d2a7e6f54 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -1,19 +1,19 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder +import org.xmtp.android.library.DecodedMessage import org.xmtp.android.library.codecs.description -import org.xmtp.android.library.messages.DecryptedMessage class DecodedMessageWrapper { companion object { - fun encode(model: DecryptedMessage): String { + fun encode(model: DecodedMessage): String { val gson = GsonBuilder().create() val message = encodeMap(model) return gson.toJson(message) } - fun encodeMap(model: DecryptedMessage): Map { + fun encodeMap(model: DecodedMessage): Map { // Kotlin/Java Protos don't support null values and will always put the default "" // Check if there is a fallback, if there is then make it the set fallback, if not null val fallback = if (model.encodedContent.hasFallback()) model.encodedContent.fallback else null @@ -23,7 +23,7 @@ class DecodedMessageWrapper { "contentTypeId" to model.encodedContent.type.description, "content" to ContentJson(model.encodedContent).toJsonMap(), "senderAddress" to model.senderAddress, - "sent" to model.sentAt.time, + "sent" to model.sent.time, "fallback" to fallback, "deliveryStatus" to model.deliveryStatus.toString() ) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt index 0ba98eb8e..f27d8a8a9 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt @@ -9,7 +9,7 @@ import org.xmtp.android.library.Group class DmWrapper { companion object { - suspend fun encodeToObj( + fun encodeToObj( client: Client, dm: Dm, dmParams: ConversationParamsWrapper = ConversationParamsWrapper(), @@ -25,7 +25,7 @@ class DmWrapper { put("consentState", consentStateToString(dm.consentState())) } if (dmParams.lastMessage) { - val lastMessage = dm.decryptedMessages(limit = 1).firstOrNull() + val lastMessage = dm.messages(limit = 1).firstOrNull() if (lastMessage != null) { put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) } @@ -33,7 +33,7 @@ class DmWrapper { } } - suspend fun encode( + fun encode( client: Client, dm: Dm, dmParams: ConversationParamsWrapper = ConversationParamsWrapper(), diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index 33aaa101d..a4832fe44 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -9,7 +9,7 @@ import org.xmtp.android.library.Group class GroupWrapper { companion object { - suspend fun encodeToObj( + fun encodeToObj( client: Client, group: Group, groupParams: ConversationParamsWrapper = ConversationParamsWrapper(), @@ -29,7 +29,7 @@ class GroupWrapper { put("consentState", consentStateToString(group.consentState())) } if (groupParams.lastMessage) { - val lastMessage = group.decryptedMessages(limit = 1).firstOrNull() + val lastMessage = group.messages(limit = 1).firstOrNull() if (lastMessage != null) { put("lastMessage", DecodedMessageWrapper.encode(lastMessage)) } @@ -37,7 +37,7 @@ class GroupWrapper { } } - suspend fun encode( + fun encode( client: Client, group: Group, groupParams: ConversationParamsWrapper = ConversationParamsWrapper(), diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PreparedLocalMessage.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PreparedLocalMessage.kt deleted file mode 100644 index 7f5f0b043..000000000 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PreparedLocalMessage.kt +++ /dev/null @@ -1,27 +0,0 @@ -package expo.modules.xmtpreactnativesdk.wrappers - -import com.google.gson.GsonBuilder -import com.google.gson.JsonParser - -class PreparedLocalMessage( - val messageId: String, - val preparedFileUri: String, - val preparedAt: Long, -) { - companion object { - fun fromJson(json: String): PreparedLocalMessage { - val obj = JsonParser.parseString(json).asJsonObject - return PreparedLocalMessage( - obj.get("messageId").asString, - obj.get("preparedFileUri").asString, - obj.get("preparedAt").asNumber.toLong(), - ) - } - } - - fun toJson(): String = GsonBuilder().create().toJson(mapOf( - "messageId" to messageId, - "preparedFileUri" to preparedFileUri, - "preparedAt" to preparedAt, - )) -} diff --git a/src/index.ts b/src/index.ts index f774f1a1b..521f0a190 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { Conversation } from './lib/Conversation' import { ConversationContainer, ConversationVersion, -} from './lib/ConversationContainer' +} from './lib/Conversation' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' import { Dm } from './lib/Dm' import { Group, PermissionUpdateOption } from './lib/Group' @@ -1522,7 +1522,7 @@ export { Conversation } from './lib/Conversation' export { ConversationContainer, ConversationVersion, -} from './lib/ConversationContainer' +} from './lib/Conversation' export { Query } from './lib/Query' export { XMTPPush } from './lib/XMTPPush' export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus } diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 8e1c764fa..e4770db4e 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,336 +1,39 @@ -import { invitation } from '@xmtp/proto' -import { Buffer } from 'buffer' - import { ConsentState } from './ConsentListEntry' -import { - ConversationContainerBase, - ConversationVersion, -} from './ConversationContainer' -import { DecodedMessage } from './DecodedMessage' -import { MessagesOptions } from './types' -import { ConversationSendPayload } from './types/ConversationCodecs' +import { ConversationSendPayload, MessagesOptions } from './types' import { DefaultContentTypes } from './types/DefaultContentType' -import { EventTypes } from './types/EventTypes' -import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' -import { ConversationContext, PreparedLocalMessage } from '../index' +import { Conversation, DecodedMessage, Member, Dm, Group } from '../index' -export interface ConversationParams { - createdAt: number - context?: ConversationContext - topic: string - peerAddress?: string - version: string - conversationID?: string - keyMaterial?: string - consentProof?: string +export enum ConversationVersion { + GROUP = 'GROUP', + DM = 'DM', } -export class Conversation - implements ConversationContainerBase -{ +export interface ConversationBase { client: XMTP.Client createdAt: number - context?: ConversationContext topic: string - peerAddress: string - version = ConversationVersion.DIRECT as const - conversationID?: string | undefined + version: ConversationVersion id: string state: ConsentState - - /** - * Base64 encoded key material for the conversation. - */ - keyMaterial?: string | undefined - /** - * Proof of consent for the conversation, used when a user is subscribing to broadcasts. - */ - consentProof?: invitation.ConsentProofPayload | undefined - - constructor(client: XMTP.Client, params: ConversationParams) { - this.client = client - this.createdAt = params.createdAt - this.context = params.context - this.topic = params.topic - this.peerAddress = params.peerAddress ?? '' - this.conversationID = params.conversationID - this.keyMaterial = params.keyMaterial - this.id = params.topic - this.state = 'unknown' - try { - if (params?.consentProof) { - this.consentProof = invitation.ConsentProofPayload.decode( - new Uint8Array(Buffer.from(params.consentProof, 'base64')) - ) - } - } catch {} - } - lastMessage?: DecodedMessage | undefined - - async exportTopicData(): Promise { - return await XMTP.exportConversationTopicData( - this.client.inboxId, - this.topic - ) - } - - /** - * Lists messages in a conversation with optional filters. - * - * @param {number} limit - Optional limit to the number of messages to return. - * @param {number | Date} before - Optional timestamp to filter messages before. - * @param {number | Date} after - Optional timestamp to filter messages after. - * @param {"SORT_DIRECTION_ASCENDING" | "SORT_DIRECTION_DESCENDING"} direction - Optional sorting direction for messages. - * @returns {Promise} A Promise that resolves to an array of decoded messages. - * @throws {Error} Throws an error if there is an issue with listing messages. - * - * @todo Support pagination and conversation ID in future implementations. - */ - async messages( - opts?: MessagesOptions - ): Promise[]> { - try { - const messages = await XMTP.listMessages( - this.client, - this.topic, - opts?.limit, - opts?.before, - opts?.after, - opts?.direction - ) - - return messages - } catch (e) { - console.info('ERROR in listMessages', e) - return [] - } - } - - private async _sendWithJSCodec( - content: T, - contentType: XMTP.ContentTypeId - ): Promise { - const codec = - this.client.codecRegistry[ - `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}` - ] - - if (!codec) { - throw new Error(`no codec found for: ${contentType}`) - } - - return await XMTP.sendWithContentType( - this.client.inboxId, - this.topic, - content, - codec - ) - } - - /** - * Sends a message to the current conversation. - * - * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. - * @returns {Promise} A Promise that resolves to a string identifier for the sent message. - * @throws {Error} Throws an error if there is an issue with sending the message. - * - * @todo Support specifying a conversation ID in future implementations. - */ - async send( - content: ConversationSendPayload, - opts?: SendOptions - ): Promise { - if (opts && opts.contentType) { - return await this._sendWithJSCodec(content, opts.contentType) - } - - try { - if (typeof content === 'string') { - content = { text: content } - } - - return await XMTP.sendMessage(this.client.inboxId, this.topic, content) - } catch (e) { - console.info('ERROR in send()', e) - throw e - } - } - - private async _prepareWithJSCodec( - content: T, - contentType: XMTP.ContentTypeId - ): Promise { - const codec = - this.client.codecRegistry[ - `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}` - ] - - if (!codec) { - throw new Error(`no codec found for: ${contentType}`) - } - - return await XMTP.prepareMessageWithContentType( - this.client.inboxId, - this.topic, - content, - codec - ) - } - - /** - * Prepares a message to be sent, yielding a `PreparedLocalMessage` object. - * - * Instead of immediately sending a message, you can prepare it first using this method. - * This yields a `PreparedLocalMessage` object, which you can send later. - * This is useful to help construct a robust pending-message queue - * that can survive connectivity outages and app restarts. - * - * Note: the {@linkcode Conversation.sendPreparedMessage | sendPreparedMessage} method is available on both this {@linkcode Conversation} - * or the top-level `Client` (when you don't have the `Conversation` handy). - * - * @param {string | MessageContent} content - The content of the message. It can be either a string or a structured MessageContent object. - * @returns {Promise} A Promise that resolves to a `PreparedLocalMessage` object. - * @throws {Error} Throws an error if there is an issue with preparing the message. - */ - async prepareMessage< - PrepareContentTypes extends DefaultContentTypes = ContentTypes, - >( - content: ConversationSendPayload, - opts?: SendOptions - ): Promise { - if (opts && opts.contentType) { - return await this._prepareWithJSCodec(content, opts.contentType) - } - try { - if (typeof content === 'string') { - content = { text: content } - } - return await XMTP.prepareMessage(this.client.inboxId, this.topic, content) - } catch (e) { - console.info('ERROR in prepareMessage()', e) - throw e - } - } - - /** - * Sends a prepared local message. - * - * This asynchronous method takes a `PreparedLocalMessage` and sends it. - * Prepared messages are created using the {@linkcode Conversation.prepareMessage | prepareMessage} method. - * - * @param {PreparedLocalMessage} prepared - The prepared local message to be sent. - * @returns {Promise} A Promise that resolves to a string identifier for the sent message. - * @throws {Error} Throws an error if there is an issue with sending the prepared message. - */ - async sendPreparedMessage(prepared: PreparedLocalMessage): Promise { - try { - return await XMTP.sendPreparedMessage(this.client.inboxId, prepared) - } catch (e) { - console.info('ERROR in sendPreparedMessage()', e) - throw e - } - } - - /** - * Decodes an encrypted message, yielding a `DecodedMessage` object. - * - * This asynchronous method takes an encrypted message and decodes it. - * The result is a `DecodedMessage` object containing the decoded content and metadata. - * - * @param {string} encryptedMessage - The encrypted message to be decoded. - * @returns {Promise} A Promise that resolves to a `DecodedMessage` object. - * @throws {Error} Throws an error if there is an issue with decoding the message. - */ - async decodeMessage( - encryptedMessage: string - ): Promise> { - try { - return await XMTP.decodeMessage( - this.client.inboxId, - this.topic, - encryptedMessage - ) - } catch (e) { - console.info('ERROR in decodeMessage()', e) - throw e - } - } - - /** - * Retrieves the consent state for the current conversation. - * - * This asynchronous method determine the consent state - * for the current conversation, indicating whether the user has allowed, denied, - * or is yet to provide consent. - * - * @returns {Promise<"allowed" | "denied" | "unknown">} A Promise that resolves to the consent state, which can be "allowed," "denied," or "unknown." - */ - async consentState(): Promise<'allowed' | 'denied' | 'unknown'> { - return await XMTP.conversationConsentState(this.client.inboxId, this.topic) - } - /** - * Sets up a real-time message stream for the current conversation. - * - * This method subscribes to incoming messages in real-time and listens for new message events. - * When a new message is detected, the provided callback function is invoked with the details of the message. - * Additionally, this method returns a function that can be called to unsubscribe and end the message stream. - * - * @param {Function} callback - A callback function that will be invoked with the new DecodedMessage when a message is received. - * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. - */ - async streamMessages( + lastMessage?: DecodedMessage + + send( + content: ConversationSendPayload + ): Promise + sync() + messages(opts?: MessagesOptions): Promise[]> + streamMessages( callback: (message: DecodedMessage) => Promise - ): Promise<() => void> { - await XMTP.subscribeToMessages(this.client.inboxId, this.topic) - const hasSeen = {} - const messageSubscription = XMTP.emitter.addListener( - EventTypes.ConversationMessage, - async ({ - inboxId, - message, - topic, - }: { - inboxId: string - message: DecodedMessage - topic: string - }) => { - // Long term these checks should be able to be done on the native layer as well, but additional checks in JS for safety - if (inboxId !== this.client.inboxId) { - return - } - if (topic !== this.topic) { - return - } - if (hasSeen[message.id]) { - return - } - - hasSeen[message.id] = true - - message.client = this.client - await callback(DecodedMessage.fromObject(message, this.client)) - } - ) - - return async () => { - messageSubscription.remove() - await XMTP.unsubscribeFromMessages(this.client.inboxId, this.topic) - } - } - - sync() { - throw new Error('V3 only') - } - updateConsent(state: ConsentState): Promise { - throw new Error('V3 only') - } + ): Promise<() => void> + consentState(): Promise + updateConsent(state: ConsentState): Promise processMessage( encryptedMessage: string - ): Promise> { - throw new Error('V3 only') - } - members(): Promise { - throw new Error('V3 only') - } + ): Promise> + members(): Promise } + +export type Conversation< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +> = Group | Dm diff --git a/src/lib/ConversationContainer.ts b/src/lib/ConversationContainer.ts deleted file mode 100644 index b9b17c84e..000000000 --- a/src/lib/ConversationContainer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ConsentState } from './ConsentListEntry' -import { ConversationSendPayload, MessagesOptions } from './types' -import { DefaultContentTypes } from './types/DefaultContentType' -import * as XMTP from '../index' -import { Conversation, DecodedMessage, Member, Dm, Group } from '../index' - -export enum ConversationVersion { - DIRECT = 'DIRECT', - GROUP = 'GROUP', - DM = 'DM', -} - -export interface ConversationContainerBase< - ContentTypes extends DefaultContentTypes, -> { - client: XMTP.Client - createdAt: number - topic: string - version: ConversationVersion - id: string - state: ConsentState - lastMessage?: DecodedMessage - - send( - content: ConversationSendPayload - ): Promise - sync() - messages(opts?: MessagesOptions): Promise[]> - streamMessages( - callback: (message: DecodedMessage) => Promise - ): Promise<() => void> - consentState(): Promise - updateConsent(state: ConsentState): Promise - processMessage( - encryptedMessage: string - ): Promise> - members(): Promise -} - -export type ConversationContainer< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> = Group | Dm | Conversation diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index f62a88850..50a86ffcf 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -1,11 +1,10 @@ import type { invitation, keystore } from '@xmtp/proto' import { Client } from './Client' -import { Conversation, ConversationParams } from './Conversation' import { ConversationVersion, ConversationContainer, -} from './ConversationContainer' +} from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Dm, DmParams } from './Dm' import { Group, GroupParams } from './Group' @@ -23,62 +22,25 @@ export default class Conversations< ContentTypes extends ContentCodec[] = [], > { client: Client - private known = {} as { [topic: string]: boolean } private subscriptions: { [key: string]: { remove: () => void } } = {} constructor(client: Client) { this.client = client } - /** - * This method returns a list of all conversations that the client is a member of. - * - * @returns {Promise} A Promise that resolves to an array of Conversation objects. - */ - async list(): Promise[]> { - const result = await XMTPModule.listConversations(this.client) - - for (const conversation of result) { - this.known[conversation.topic] = true - } - - return result - } - - async getHmacKeys(): Promise { - return await XMTPModule.getHmacKeys(this.client.inboxId) - } - - async importTopicData( - topicData: string - ): Promise> { - const conversation = await XMTPModule.importConversationTopicData( - this.client, - topicData - ) - this.known[conversation.topic] = true - return conversation - } /** * Creates a new conversation. * * This method creates a new conversation with the specified peer address and context. * * @param {string} peerAddress - The address of the peer to create a conversation with. - * @param {ConversationContext} context - Optional context to associate with the conversation. * @returns {Promise} A Promise that resolves to a Conversation object. */ - async newConversation( - peerAddress: string, - context?: ConversationContext, - consentProof?: invitation.ConsentProofPayload - ): Promise> { + async newConversation(peerAddress: string): Promise> { const checksumAddress = getAddress(peerAddress) return await XMTPModule.createConversation( this.client, checksumAddress, - context, - consentProof ) } diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index 4f4d0b1a5..dfd7f3cf2 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -2,8 +2,8 @@ import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' import { ConversationVersion, - ConversationContainerBase, -} from './ConversationContainer' + ConversationBase, +} from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' @@ -21,7 +21,7 @@ export interface DmParams { } export class Dm - implements ConversationContainerBase + implements ConversationBase { client: XMTP.Client id: string diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 592b4729f..32c652613 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,9 +1,9 @@ import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' import { - ConversationContainerBase, + ConversationBase, ConversationVersion, -} from './ConversationContainer' +} from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' @@ -31,7 +31,7 @@ export interface GroupParams { export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> implements ConversationContainerBase +> implements ConversationBase { client: XMTP.Client id: string From 323e24905ab4d304a1720106004026d74d97e608 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 17:10:19 -0800 Subject: [PATCH 03/21] get android dialed --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 32 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 9ea79223c..bd15b1aa7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.0" + implementation "org.xmtp:android:3.0.1" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 1f4d010e5..8fc75330b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -35,10 +35,13 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import org.xmtp.android.library.Client import org.xmtp.android.library.ClientOptions +import org.xmtp.android.library.ConsentListEntry import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation -import org.xmtp.android.library.Conversations.ConversationOrder +import org.xmtp.android.library.Conversations +import org.xmtp.android.library.Conversations.* import org.xmtp.android.library.Dm +import org.xmtp.android.library.EntryType import org.xmtp.android.library.Group import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.SendOptions @@ -1099,10 +1102,18 @@ class XMTPModule : Module() { } } - AsyncFunction("setConsentState") Coroutine { inboxId: String -> + AsyncFunction("setConsentState") Coroutine { inboxId: String, value: String, entryType: String, consentType: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") - val consentList = client.preferences.consentList.setConsentState() + val consentList = client.preferences.consentList.setConsentState( + listOf( + ConsentListEntry( + value, + getEntryType(entryType), + getConsentState(consentType) + ) + ) + ) } } @@ -1228,9 +1239,9 @@ class XMTPModule : Module() { private fun getStreamType(typeString: String): ConversationType { return when (typeString) { - "groups" -> GROUPS - "dms" -> DMS - else -> ALL + "groups" -> ConversationType.GROUPS + "dms" -> ConversationType.DMS + else -> ConversationType.ALL } } @@ -1242,6 +1253,15 @@ class XMTPModule : Module() { } } + private fun getEntryType(entryString: String): EntryType { + return when (entryString) { + "address" -> EntryType.ADDRESS + "conversation_id" -> EntryType.CONVERSATION_ID + "inbox_id" -> EntryType.INBOX_ID + else -> throw XMTPException("Invalid entry type: $entryString") + } + } + private fun getConversationSortOrder(order: String): ConversationOrder { return when (order) { "lastMessage" -> ConversationOrder.LAST_MESSAGE From 2bbd1713721e544cff0a7f330b2676888c8ad419 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 17:50:07 -0800 Subject: [PATCH 04/21] clean up code --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 8fc75330b..93e9f78ca 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -135,17 +135,9 @@ fun Conversation.cacheKey(inboxId: String): String { return "${inboxId}:${topic}" } -fun Group.cacheKey(inboxId: String): String { - return "${inboxId}:${id}" -} - -fun Dm.cacheKey(inboxId: String): String { - return "${inboxId}:${id}" -} - class XMTPModule : Module() { - val context: Context + private val context: Context get() = appContext.reactContext ?: throw Exceptions.ReactContextLost() private fun apiEnvironments(env: String, appVersion: String?): ClientOptions.Api { @@ -330,7 +322,6 @@ class XMTPModule : Module() { AsyncFunction("build") Coroutine { address: String, dbEncryptionKey: List, authParams: String -> withContext(Dispatchers.IO) { logV("build") - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) val options = clientOptions( dbEncryptionKey, authParams, @@ -391,7 +382,6 @@ class XMTPModule : Module() { AsyncFunction("encryptAttachment") { inboxId: String, fileJson: String -> logV("encryptAttachment") - val client = clients[inboxId] ?: throw XMTPException("No client") val file = DecryptedLocalAttachment.fromJson(fileJson) val uri = Uri.parse(file.fileUri) val data = appContext.reactContext?.contentResolver @@ -418,7 +408,6 @@ class XMTPModule : Module() { AsyncFunction("decryptAttachment") { inboxId: String, encryptedFileJson: String -> logV("decryptAttachment") - val client = clients[inboxId] ?: throw XMTPException("No client") val encryptedFile = EncryptedLocalAttachment.fromJson(encryptedFileJson) val encryptedData = appContext.reactContext?.contentResolver ?.openInputStream(Uri.parse(encryptedFile.encryptedLocalFileUri)) @@ -1042,12 +1031,8 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") - val permissionPolicySet = group?.permissionPolicySet() - if (permissionPolicySet != null) { - PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) - } else { - throw XMTPException("Permission policy set not found for group: $groupId") - } + val permissionPolicySet = group.permissionPolicySet() + PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) } } @@ -1090,7 +1075,6 @@ class XMTPModule : Module() { if (xmtpPush == null) { throw XMTPException("Push server not registered") } - val client = clients[inboxId] ?: throw XMTPException("No client") val subscriptions = topics.map { Service.Subscription.newBuilder().also { sub -> @@ -1105,7 +1089,7 @@ class XMTPModule : Module() { AsyncFunction("setConsentState") Coroutine { inboxId: String, value: String, entryType: String, consentType: String -> withContext(Dispatchers.IO) { val client = clients[inboxId] ?: throw XMTPException("No client") - val consentList = client.preferences.consentList.setConsentState( + client.preferences.consentList.setConsentState( listOf( ConsentListEntry( value, From edb73307a249dab4b31db3d79f499fe251684ab6 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 17:57:40 -0800 Subject: [PATCH 05/21] clean up warnings and get android compiling git st --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 18 +++++++---- .../wrappers/ClientWrapper.kt | 7 ---- .../wrappers/ConsentWrapper.kt | 32 ------------------- .../wrappers/ContentJson.kt | 2 +- .../wrappers/ConversationWrapper.kt | 2 +- .../wrappers/DecryptedLocalAttachment.kt | 4 +-- .../xmtpreactnativesdk/wrappers/DmWrapper.kt | 3 -- .../wrappers/EncryptedLocalAttachment.kt | 2 +- .../wrappers/GroupWrapper.kt | 10 +++++- .../wrappers/InboxStateWrapper.kt | 2 +- .../wrappers/MemberWrapper.kt | 5 +-- .../wrappers/PermissionPolicySetWrapper.kt | 6 ++-- 12 files changed, 32 insertions(+), 61 deletions(-) delete mode 100644 android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 93e9f78ca..242bb5fc0 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -13,7 +13,6 @@ import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.xmtpreactnativesdk.wrappers.AuthParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.ClientWrapper -import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString import expo.modules.xmtpreactnativesdk.wrappers.ContentJson import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper @@ -38,11 +37,8 @@ import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.ConsentListEntry import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation -import org.xmtp.android.library.Conversations import org.xmtp.android.library.Conversations.* -import org.xmtp.android.library.Dm import org.xmtp.android.library.EntryType -import org.xmtp.android.library.Group import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.SendOptions import org.xmtp.android.library.SigningKey @@ -380,7 +376,7 @@ class XMTPModule : Module() { } } - AsyncFunction("encryptAttachment") { inboxId: String, fileJson: String -> + AsyncFunction("encryptAttachment") { fileJson: String -> logV("encryptAttachment") val file = DecryptedLocalAttachment.fromJson(fileJson) val uri = Uri.parse(file.fileUri) @@ -406,7 +402,7 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("decryptAttachment") { inboxId: String, encryptedFileJson: String -> + AsyncFunction("decryptAttachment") { encryptedFileJson: String -> logV("decryptAttachment") val encryptedFile = EncryptedLocalAttachment.fromJson(encryptedFileJson) val encryptedData = appContext.reactContext?.contentResolver @@ -1069,7 +1065,7 @@ class XMTPModule : Module() { xmtpPush?.register(token) } - Function("subscribePushTopics") { inboxId: String, topics: List -> + Function("subscribePushTopics") { topics: List -> logV("subscribePushTopics") if (topics.isNotEmpty()) { if (xmtpPush == null) { @@ -1253,6 +1249,14 @@ class XMTPModule : Module() { } } + private fun consentStateToString(state: ConsentState): String { + return when (state) { + ConsentState.ALLOWED -> "allowed" + ConsentState.DENIED -> "denied" + ConsentState.UNKNOWN -> "unknown" + } + } + private fun subscribeToConversations(inboxId: String, type: ConversationType) { val client = clients[inboxId] ?: throw XMTPException("No client") diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ClientWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ClientWrapper.kt index 9cbe85ea8..aaa41fd56 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ClientWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ClientWrapper.kt @@ -1,6 +1,5 @@ package expo.modules.xmtpreactnativesdk.wrappers -import com.google.gson.GsonBuilder import org.xmtp.android.library.Client class ClientWrapper { @@ -13,11 +12,5 @@ class ClientWrapper { "dbPath" to client.dbPath ) } - - fun encode(client: Client): String { - val gson = GsonBuilder().create() - val obj = encodeToObj(client) - return gson.toJson(obj) - } } } \ No newline at end of file diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt deleted file mode 100644 index c80eef381..000000000 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt +++ /dev/null @@ -1,32 +0,0 @@ -package expo.modules.xmtpreactnativesdk.wrappers - -import com.google.gson.GsonBuilder -import org.xmtp.android.library.ConsentListEntry -import org.xmtp.android.library.ConsentState -import org.xmtp.android.library.codecs.description -import org.xmtp.android.library.messages.DecryptedMessage - -class ConsentWrapper { - - companion object { - fun encode(model: ConsentListEntry): String { - val gson = GsonBuilder().create() - val message = encodeMap(model) - return gson.toJson(message) - } - - fun encodeMap(model: ConsentListEntry): Map = mapOf( - "type" to model.entryType.name.lowercase(), - "value" to model.value.lowercase(), - "state" to consentStateToString(model.consentType), - ) - - fun consentStateToString(state: ConsentState): String { - return when (state) { - ConsentState.ALLOWED -> "allowed" - ConsentState.DENIED -> "denied" - ConsentState.UNKNOWN -> "unknown" - } - } - } -} diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt index 2ea93c690..8fdc95c24 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ContentJson.kt @@ -123,7 +123,7 @@ class ContentJson( return fromJsonObject(obj); } - fun bytesFrom64(bytes64: String): ByteArray = Base64.decode(bytes64, Base64.NO_WRAP) + private fun bytesFrom64(bytes64: String): ByteArray = Base64.decode(bytes64, Base64.NO_WRAP) fun bytesTo64(bytes: ByteArray): String = Base64.encodeToString(bytes, Base64.NO_WRAP) } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt index 9e4e1caa6..8d85ccb9b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConversationWrapper.kt @@ -32,7 +32,7 @@ class ConversationWrapper { ): String { val gson = GsonBuilder().create() val obj = - ConversationWrapper.encodeToObj(client, conversation, conversationParams) + encodeToObj(client, conversation, conversationParams) return gson.toJson(obj) } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecryptedLocalAttachment.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecryptedLocalAttachment.kt index 770e7dac6..2da397f10 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecryptedLocalAttachment.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecryptedLocalAttachment.kt @@ -13,7 +13,7 @@ class DecryptedLocalAttachment( val filename: String, ) { companion object { - fun fromJsonObject(obj: JsonObject) = DecryptedLocalAttachment( + private fun fromJsonObject(obj: JsonObject) = DecryptedLocalAttachment( obj.get("fileUri").asString, obj.get("mimeType")?.asString ?: "", obj.get("filename")?.asString ?: "", @@ -25,7 +25,7 @@ class DecryptedLocalAttachment( } } - fun toJsonMap(): Map = mapOf( + private fun toJsonMap(): Map = mapOf( "fileUri" to fileUri, "mimeType" to mimeType, "filename" to filename, diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt index f27d8a8a9..9a690fbec 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DmWrapper.kt @@ -1,11 +1,8 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder -import com.google.gson.JsonParser -import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString import org.xmtp.android.library.Client import org.xmtp.android.library.Dm -import org.xmtp.android.library.Group class DmWrapper { companion object { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncryptedLocalAttachment.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncryptedLocalAttachment.kt index 87a92805b..860b03914 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncryptedLocalAttachment.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/EncryptedLocalAttachment.kt @@ -39,7 +39,7 @@ class EncryptedLocalAttachment( } } - fun toJsonMap(): Map = mapOf( + private fun toJsonMap(): Map = mapOf( "encryptedLocalFileUri" to encryptedLocalFileUri, "metadata" to metadata.toJsonMap() ) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt index a4832fe44..6a0e14403 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/GroupWrapper.kt @@ -2,8 +2,8 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder import com.google.gson.JsonParser -import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper.Companion.consentStateToString import org.xmtp.android.library.Client +import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Group class GroupWrapper { @@ -49,6 +49,14 @@ class GroupWrapper { } } +fun consentStateToString(state: ConsentState): String { + return when (state) { + ConsentState.ALLOWED -> "allowed" + ConsentState.DENIED -> "denied" + ConsentState.UNKNOWN -> "unknown" + } +} + class ConversationParamsWrapper( val isActive: Boolean = true, val addedByInboxId: Boolean = true, diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt index 0302b5180..609c273b9 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/InboxStateWrapper.kt @@ -7,7 +7,7 @@ import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState class InboxStateWrapper { companion object { val gson: Gson = GsonBuilder().create() - fun encodeToObj(inboxState: InboxState): Map { + private fun encodeToObj(inboxState: InboxState): Map { return mapOf( "inboxId" to inboxState.inboxId, "addresses" to inboxState.addresses, diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt index b241292d2..769ae0377 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/MemberWrapper.kt @@ -1,12 +1,13 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder +import org.xmtp.android.library.ConsentState import org.xmtp.android.library.libxmtp.Member import org.xmtp.android.library.libxmtp.PermissionLevel class MemberWrapper { companion object { - fun encodeToObj(member: Member): Map { + private fun encodeToObj(member: Member): Map { val permissionString = when (member.permissionLevel) { PermissionLevel.MEMBER -> "member" PermissionLevel.ADMIN -> "admin" @@ -16,7 +17,7 @@ class MemberWrapper { "inboxId" to member.inboxId, "addresses" to member.addresses, "permissionLevel" to permissionString, - "consentState" to ConsentWrapper.consentStateToString(member.consentState) + "consentState" to consentStateToString(member.consentState) ) } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt index a6f1b9783..1e5c10b38 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt @@ -8,7 +8,7 @@ import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet class PermissionPolicySetWrapper { companion object { - fun fromPermissionOption(permissionOption: PermissionOption): String { + private fun fromPermissionOption(permissionOption: PermissionOption): String { return when (permissionOption) { PermissionOption.Allow -> "allow" PermissionOption.Deny -> "deny" @@ -18,7 +18,7 @@ class PermissionPolicySetWrapper { } } - fun createPermissionOptionFromString(permissionOptionString: String): PermissionOption { + private fun createPermissionOptionFromString(permissionOptionString: String): PermissionOption { return when (permissionOptionString) { "allow" -> PermissionOption.Allow "deny" -> PermissionOption.Deny @@ -27,7 +27,7 @@ class PermissionPolicySetWrapper { else -> PermissionOption.Unknown } } - fun encodeToObj(policySet: PermissionPolicySet): Map { + private fun encodeToObj(policySet: PermissionPolicySet): Map { return mapOf( "addMemberPolicy" to fromPermissionOption(policySet.addMemberPolicy), "removeMemberPolicy" to fromPermissionOption(policySet.removeMemberPolicy), From 86d0e587479003e754de77c609b777fd039dd52e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Fri, 8 Nov 2024 18:56:39 -0800 Subject: [PATCH 06/21] get all the types lined up in the index file --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 7 +- src/XMTP.types.ts | 8 - src/index.ts | 1064 ++++------------- src/lib/Client.ts | 18 +- src/lib/Conversations.ts | 3 +- src/lib/InboxState.ts | 9 +- src/lib/Member.ts | 5 +- src/lib/Query.ts | 10 - src/lib/XMTPPush.ts | 5 +- src/lib/types/ContentCodec.ts | 29 - ...GroupOptions.ts => ConversationOptions.ts} | 7 +- src/lib/types/EventTypes.ts | 39 +- src/lib/types/MessagesOptions.ts | 8 +- src/utils/address.ts | 2 + 14 files changed, 283 insertions(+), 931 deletions(-) delete mode 100644 src/XMTP.types.ts delete mode 100644 src/lib/Query.ts rename src/lib/types/{GroupOptions.ts => ConversationOptions.ts} (58%) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 242bb5fc0..1a61f4790 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -201,13 +201,12 @@ class XMTPModule : Module() { override fun definition() = ModuleDefinition { Name("XMTP") Events( - // Auth "sign", "authed", "preAuthenticateToInboxCallback", "conversation", - "allMessages", "message", + "conversationMessage", ) Function("address") { inboxId: String -> @@ -1291,7 +1290,7 @@ class XMTPModule : Module() { try { client.conversations.streamAllMessages(type).collect { message -> sendEvent( - "allMessages", + "message", mapOf( "inboxId" to inboxId, "message" to DecodedMessageWrapper.encodeMap(message), @@ -1315,7 +1314,7 @@ class XMTPModule : Module() { try { conversation.streamMessages().collect { message -> sendEvent( - "message", + "conversationMessage", mapOf( "inboxId" to inboxId, "message" to DecodedMessageWrapper.encodeMap(message), diff --git a/src/XMTP.types.ts b/src/XMTP.types.ts deleted file mode 100644 index 8bf39b622..000000000 --- a/src/XMTP.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This contains the contents of a message. -// Each of these corresponds to a codec supported by the native libraries. -// This is a one-of or union type: only one of these fields will be present. - -export type ConversationContext = { - conversationID: string - metadata: { [key: string]: string } -} diff --git a/src/index.ts b/src/index.ts index 521f0a190..1d0adda46 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,35 @@ -import { content, invitation, keystore } from '@xmtp/proto' import { EventEmitter, NativeModulesProxy } from 'expo-modules-core' import { Client } from '.' -import { ConversationContext } from './XMTP.types' import XMTPModule from './XMTPModule' -import { InboxId } from './lib/Client' -import { ConsentListEntry, ConsentState } from './lib/ConsentListEntry' +import { InboxId, XMTPEnvironment } from './lib/Client' +import { + ConsentListEntry, + ConsentListEntryType, + ConsentState, +} from './lib/ConsentListEntry' import { - ContentCodec, DecryptedLocalAttachment, EncryptedLocalAttachment, - PreparedLocalMessage, } from './lib/ContentCodec' -import { Conversation } from './lib/Conversation' -import { - ConversationContainer, - ConversationVersion, -} from './lib/Conversation' +import { Conversation, ConversationVersion } from './lib/Conversation' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' import { Dm } from './lib/Dm' import { Group, PermissionUpdateOption } from './lib/Group' import { InboxState } from './lib/InboxState' import { Member } from './lib/Member' -import type { Query } from './lib/Query' import { WalletType } from './lib/Signer' -import { ConversationSendPayload } from './lib/types' +import { + ConversationOrder, + ConversationOptions, + ConversationType, + ConversationId, + ConversationTopic, +} from './lib/types/ConversationOptions' import { DefaultContentTypes } from './lib/types/DefaultContentType' -import { ConversationOrder, GroupOptions } from './lib/types/GroupOptions' +import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' -import { getAddress } from './utils/address' +import { Address, getAddress } from './utils/address' export * from './context' export * from './hooks' @@ -41,84 +42,49 @@ export { StaticAttachmentCodec } from './lib/NativeCodecs/StaticAttachmentCodec' export { TextCodec } from './lib/NativeCodecs/TextCodec' export * from './lib/Signer' -const EncodedContent = content.EncodedContent - export function address(): string { return XMTPModule.address() } -export function inboxId(): string { +export function inboxId(): InboxId { return XMTPModule.inboxId() } export async function findInboxIdFromAddress( - inboxId: string, + inboxId: InboxId, address: string ): Promise { return XMTPModule.findInboxIdFromAddress(inboxId, address) } -export async function deleteLocalDatabase(inboxId: string) { +export async function deleteLocalDatabase(inboxId: InboxId) { return XMTPModule.deleteLocalDatabase(inboxId) } -export async function dropLocalDatabaseConnection(inboxId: string) { +export async function dropLocalDatabaseConnection(inboxId: InboxId) { return XMTPModule.dropLocalDatabaseConnection(inboxId) } -export async function reconnectLocalDatabase(inboxId: string) { +export async function reconnectLocalDatabase(inboxId: InboxId) { return XMTPModule.reconnectLocalDatabase(inboxId) } -export async function requestMessageHistorySync(inboxId: string) { +export async function requestMessageHistorySync(inboxId: InboxId) { return XMTPModule.requestMessageHistorySync(inboxId) } export async function getInboxState( - inboxId: string, + inboxId: InboxId, refreshFromNetwork: boolean ): Promise { const inboxState = await XMTPModule.getInboxState(inboxId, refreshFromNetwork) return InboxState.from(inboxState) } -export async function revokeAllOtherInstallations(inboxId: string) { +export async function revokeAllOtherInstallations(inboxId: InboxId) { return XMTPModule.revokeAllOtherInstallations(inboxId) } -export async function auth( - address: string, - environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined, - hasCreateIdentityCallback?: boolean | undefined, - hasEnableIdentityCallback?: boolean | undefined, - hasPreAuthenticateToInboxCallback?: boolean | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbDirectory?: string | undefined, - historySyncUrl?: string | undefined -) { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - - const authParams: AuthParams = { - environment, - appVersion, - enableV3, - dbDirectory, - historySyncUrl, - } - return await XMTPModule.auth( - address, - hasCreateIdentityCallback, - hasEnableIdentityCallback, - hasPreAuthenticateToInboxCallback, - encryptionKey, - JSON.stringify(authParams) - ) -} - export async function receiveSignature(requestID: string, signature: string) { return await XMTPModule.receiveSignature(requestID, signature) } @@ -132,189 +98,76 @@ export async function receiveSCWSignature( export async function createRandom( environment: 'local' | 'dev' | 'production', + dbEncryptionKey: Uint8Array, appVersion?: string | undefined, - hasCreateIdentityCallback?: boolean | undefined, - hasEnableIdentityCallback?: boolean | undefined, hasPreAuthenticateToInboxCallback?: boolean | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, dbDirectory?: string | undefined, historySyncUrl?: string | undefined ): Promise { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - const authParams: AuthParams = { environment, appVersion, - enableV3, dbDirectory, historySyncUrl, } return await XMTPModule.createRandom( - hasCreateIdentityCallback, - hasEnableIdentityCallback, - hasPreAuthenticateToInboxCallback, - encryptionKey, - JSON.stringify(authParams) - ) -} - -export async function createFromKeyBundle( - keyBundle: string, - environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbDirectory?: string | undefined, - historySyncUrl?: string | undefined -): Promise { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - - const authParams: AuthParams = { - environment, - appVersion, - enableV3, - dbDirectory, - historySyncUrl, - } - return await XMTPModule.createFromKeyBundle( - keyBundle, - encryptionKey, - JSON.stringify(authParams) - ) -} - -export async function createFromKeyBundleWithSigner( - address: string, - keyBundle: string, - environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbDirectory?: string | undefined, - historySyncUrl?: string | undefined -): Promise { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - - const authParams: AuthParams = { - environment, - appVersion, - enableV3, - dbDirectory, - historySyncUrl, - } - return await XMTPModule.createFromKeyBundleWithSigner( - address, - keyBundle, - encryptionKey, - JSON.stringify(authParams) - ) -} - -export async function createRandomV3( - environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined, - hasCreateIdentityCallback?: boolean | undefined, - hasEnableIdentityCallback?: boolean | undefined, - hasPreAuthenticateToInboxCallback?: boolean | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, - dbDirectory?: string | undefined, - historySyncUrl?: string | undefined -): Promise { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - - const authParams: AuthParams = { - environment, - appVersion, - enableV3, - dbDirectory, - historySyncUrl, - } - return await XMTPModule.createRandomV3( - hasCreateIdentityCallback, - hasEnableIdentityCallback, hasPreAuthenticateToInboxCallback, - encryptionKey, + Array.from(dbEncryptionKey), JSON.stringify(authParams) ) } -export async function createV3( - address: string, +export async function create( + address: Address, environment: 'local' | 'dev' | 'production', + dbEncryptionKey: Uint8Array, appVersion?: string | undefined, - hasCreateIdentityCallback?: boolean | undefined, - hasEnableIdentityCallback?: boolean | undefined, hasPreAuthenticateToInboxCallback?: boolean | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, dbDirectory?: string | undefined, historySyncUrl?: string | undefined, walletType?: WalletType | undefined, chainId?: number | undefined, blockNumber?: number | undefined -) { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - +): Promise { const authParams: AuthParams = { environment, appVersion, - enableV3, dbDirectory, historySyncUrl, walletType, chainId, blockNumber, } - return await XMTPModule.createV3( + return await XMTPModule.create( address, - hasCreateIdentityCallback, - hasEnableIdentityCallback, hasPreAuthenticateToInboxCallback, - encryptionKey, + Array.from(dbEncryptionKey), JSON.stringify(authParams) ) } -export async function buildV3( - address: string, +export async function build( + address: Address, environment: 'local' | 'dev' | 'production', + dbEncryptionKey: Uint8Array, appVersion?: string | undefined, - enableV3?: boolean | undefined, - dbEncryptionKey?: Uint8Array | undefined, dbDirectory?: string | undefined, historySyncUrl?: string | undefined -) { - const encryptionKey = dbEncryptionKey - ? Array.from(dbEncryptionKey) - : undefined - +): Promise { const authParams: AuthParams = { environment, appVersion, - enableV3, dbDirectory, historySyncUrl, } - return await XMTPModule.buildV3( + return await XMTPModule.build( address, - encryptionKey, + Array.from(dbEncryptionKey), JSON.stringify(authParams) ) } -export async function dropClient(inboxId: string) { +export async function dropClient(inboxId: InboxId) { return await XMTPModule.dropClient(inboxId) } @@ -322,7 +175,7 @@ export async function findOrCreateDm< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - peerAddress: string + peerAddress: Address ): Promise> { const dm = JSON.parse( await XMTPModule.findOrCreateDm(client.inboxId, peerAddress) @@ -334,7 +187,7 @@ export async function createGroup< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - peerAddresses: string[], + peerAddresses: Address[], permissionLevel: 'all_members' | 'admin_only' = 'all_members', name: string = '', imageUrlSquare: string = '', @@ -363,7 +216,7 @@ export async function createGroupCustomPermissions< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - peerAddresses: string[], + peerAddresses: Address[], permissionPolicySet: PermissionPolicySet, name: string = '', imageUrlSquare: string = '', @@ -392,7 +245,7 @@ export async function listGroups< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - opts?: GroupOptions | undefined, + opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, limit?: number | undefined ): Promise[]> { @@ -413,16 +266,36 @@ export async function listGroups< }) } -export async function listV3Conversations< +export async function listDms< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined +): Promise[]> { + return ( + await XMTPModule.listDms(client.inboxId, JSON.stringify(opts), order, limit) + ).map((json: string) => { + const group = JSON.parse(json) + + const lastMessage = group['lastMessage'] + ? DecodedMessage.from(group['lastMessage'], client) + : undefined + return new Dm(client, group, lastMessage) + }) +} + +export async function listConversations< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - opts?: GroupOptions | undefined, + opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, limit?: number | undefined -): Promise[]> { +): Promise[]> { return ( - await XMTPModule.listV3Conversations( + await XMTPModule.listConversations( client.inboxId, JSON.stringify(opts), order, @@ -445,19 +318,19 @@ export async function listV3Conversations< export async function listMemberInboxIds< ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->(client: Client, id: string): Promise { +>(client: Client, id: ConversationId): Promise { return XMTPModule.listMemberInboxIds(client.inboxId, id) } export async function listPeerInboxId< ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->(client: Client, dmId: string): Promise { +>(client: Client, dmId: ConversationId): Promise { return XMTPModule.listPeerInboxId(client.inboxId, dmId) } export async function listConversationMembers( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { const members = await XMTPModule.listConversationMembers(inboxId, id) @@ -466,24 +339,20 @@ export async function listConversationMembers( }) } -export async function prepareConversationMessage( - inboxId: string, - conversationId: string, +export async function prepareMessage( + inboxId: InboxId, + conversationId: ConversationId, content: any -): Promise { +): Promise { const contentJson = JSON.stringify(content) - return await XMTPModule.prepareConversationMessage( - inboxId, - conversationId, - contentJson - ) + return await XMTPModule.prepareMessage(inboxId, conversationId, contentJson) } export async function sendMessageToConversation( - inboxId: string, - conversationId: string, + inboxId: InboxId, + conversationId: ConversationId, content: any -): Promise { +): Promise { const contentJson = JSON.stringify(content) return await XMTPModule.sendMessageToConversation( inboxId, @@ -492,32 +361,29 @@ export async function sendMessageToConversation( ) } -export async function publishPreparedGroupMessages( - inboxId: string, - groupId: string +export async function publishPreparedMessages( + inboxId: InboxId, + conversationId: ConversationId ) { - return await XMTPModule.publishPreparedGroupMessages(inboxId, groupId) + return await XMTPModule.publishPreparedMessages(inboxId, conversationId) } export async function conversationMessages< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - conversationId: string, + conversationId: ConversationId, limit?: number | undefined, - before?: number | Date | undefined, - after?: number | Date | undefined, - direction?: - | 'SORT_DIRECTION_ASCENDING' - | 'SORT_DIRECTION_DESCENDING' - | undefined + beforeNs?: number | undefined, + afterNs?: number | undefined, + direction?: MessageOrder | undefined ): Promise[]> { const messages = await XMTPModule.conversationMessages( client.inboxId, conversationId, limit, - before, - after, + beforeNs, + afterNs, direction ) return messages.map((json: string) => { @@ -529,7 +395,7 @@ export async function findGroup< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - groupId: string + groupId: ConversationId ): Promise | undefined> { const json = await XMTPModule.findGroup(client.inboxId, groupId) const group = JSON.parse(json) @@ -544,8 +410,8 @@ export async function findConversation< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - conversationId: string -): Promise | undefined> { + conversationId: ConversationId +): Promise | undefined> { const json = await XMTPModule.findConversation(client.inboxId, conversationId) const conversation = JSON.parse(json) if (!conversation || Object.keys(conversation).length === 0) { @@ -563,8 +429,8 @@ export async function findConversationByTopic< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - topic: string -): Promise | undefined> { + topic: ConversationTopic +): Promise | undefined> { const json = await XMTPModule.findConversationByTopic(client.inboxId, topic) const conversation = JSON.parse(json) if (!conversation || Object.keys(conversation).length === 0) { @@ -582,7 +448,7 @@ export async function findDm< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - address: string + address: Address ): Promise | undefined> { const json = await XMTPModule.findDm(client.inboxId, address) const dm = JSON.parse(json) @@ -593,223 +459,135 @@ export async function findDm< return new Dm(client, dm) } -export async function findV3Message< +export async function findMessage< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - messageId: string + messageId: MessageId ): Promise | undefined> { - const message = await XMTPModule.findV3Message(client.inboxId, messageId) + const message = await XMTPModule.findMessage(client.inboxId, messageId) return DecodedMessage.from(message, client) } -export async function syncConversations(inboxId: string) { +export async function syncConversations(inboxId: InboxId) { await XMTPModule.syncConversations(inboxId) } -export async function syncAllConversations(inboxId: string): Promise { +export async function syncAllConversations(inboxId: InboxId): Promise { return await XMTPModule.syncAllConversations(inboxId) } -export async function syncConversation(inboxId: string, id: string) { +export async function syncConversation(inboxId: InboxId, id: ConversationId) { await XMTPModule.syncConversation(inboxId, id) } -export async function subscribeToGroupMessages(inboxId: string, id: string) { - return await XMTPModule.subscribeToGroupMessages(inboxId, id) -} - -export async function unsubscribeFromGroupMessages( - inboxId: string, - id: string -) { - return await XMTPModule.unsubscribeFromGroupMessages(inboxId, id) -} - export async function addGroupMembers( - inboxId: string, - id: string, - addresses: string[] + inboxId: InboxId, + id: ConversationId, + addresses: Address[] ): Promise { return XMTPModule.addGroupMembers(inboxId, id, addresses) } export async function removeGroupMembers( - inboxId: string, - id: string, - addresses: string[] + inboxId: InboxId, + id: ConversationId, + addresses: Address[] ): Promise { return XMTPModule.removeGroupMembers(inboxId, id, addresses) } export async function addGroupMembersByInboxId( - inboxId: string, - id: string, - inboxIds: string[] + inboxId: InboxId, + id: ConversationId, + inboxIds: InboxId[] ): Promise { return XMTPModule.addGroupMembersByInboxId(inboxId, id, inboxIds) } export async function removeGroupMembersByInboxId( - inboxId: string, - id: string, - inboxIds: string[] + inboxId: InboxId, + id: ConversationId, + inboxIds: InboxId[] ): Promise { return XMTPModule.removeGroupMembersByInboxId(inboxId, id, inboxIds) } export function groupDescription( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): string | PromiseLike { return XMTPModule.groupDescription(inboxId, id) } export function updateGroupDescription( - inboxId: string, - id: string, + inboxId: InboxId, + id: ConversationId, description: string ): Promise { return XMTPModule.updateGroupDescription(inboxId, id, description) } export function groupImageUrlSquare( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): string | PromiseLike { return XMTPModule.groupImageUrlSquare(inboxId, id) } export function updateGroupImageUrlSquare( - inboxId: string, - id: string, + inboxId: InboxId, + id: ConversationId, imageUrlSquare: string ): Promise { return XMTPModule.updateGroupImageUrlSquare(inboxId, id, imageUrlSquare) } export function groupName( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): string | PromiseLike { return XMTPModule.groupName(inboxId, id) } export function updateGroupName( - inboxId: string, - id: string, + inboxId: InboxId, + id: ConversationId, groupName: string ): Promise { return XMTPModule.updateGroupName(inboxId, id, groupName) } export function groupPinnedFrameUrl( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): string | PromiseLike { return XMTPModule.groupPinnedFrameUrl(inboxId, id) } export function updateGroupPinnedFrameUrl( - inboxId: string, - id: string, + inboxId: InboxId, + id: ConversationId, pinnedFrameUrl: string ): Promise { return XMTPModule.updateGroupPinnedFrameUrl(inboxId, id, pinnedFrameUrl) } -export async function sign( - inboxId: string, - digest: Uint8Array, - keyType: string, - preKeyIndex: number = 0 -): Promise { - const signatureArray = await XMTPModule.sign( - inboxId, - Array.from(digest), - keyType, - preKeyIndex - ) - return new Uint8Array(signatureArray) -} - -export async function exportPublicKeyBundle( - inboxId: string -): Promise { - const publicBundleArray = await XMTPModule.exportPublicKeyBundle(inboxId) - return new Uint8Array(publicBundleArray) -} - -export async function exportKeyBundle(inboxId: string): Promise { - return await XMTPModule.exportKeyBundle(inboxId) -} - -export async function exportConversationTopicData( - inboxId: string, - conversationTopic: string -): Promise { - return await XMTPModule.exportConversationTopicData( - inboxId, - conversationTopic - ) -} - -export async function getHmacKeys( - inboxId: string -): Promise { - const hmacKeysArray = await XMTPModule.getHmacKeys(inboxId) - const array = new Uint8Array(hmacKeysArray) - return keystore.GetConversationHmacKeysResponse.decode(array) -} - -export async function importConversationTopicData< - ContentTypes extends ContentCodec[], ->( - client: Client, - topicData: string -): Promise> { - const json = await XMTPModule.importConversationTopicData( - client.inboxId, - topicData - ) - return new Conversation(client, JSON.parse(json)) -} - export async function canMessage( - inboxId: string, - peerAddress: string -): Promise { - return await XMTPModule.canMessage(inboxId, getAddress(peerAddress)) -} - -export async function canGroupMessage( - inboxId: string, - peerAddresses: string[] -): Promise<{ [key: string]: boolean }> { - return await XMTPModule.canGroupMessage(inboxId, peerAddresses) -} - -export async function staticCanMessage( - peerAddress: string, - environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined -): Promise { - return await XMTPModule.staticCanMessage( - getAddress(peerAddress), - environment, - appVersion - ) + inboxId: InboxId, + peerAddresses: Address[] +): Promise<{ [key: Address]: boolean }> { + return await XMTPModule.canMessage(inboxId, peerAddresses) } export async function getOrCreateInboxId( - address: string, - environment: 'local' | 'dev' | 'production' + address: InboxId, + environment: XMTPEnvironment ): Promise { return await XMTPModule.getOrCreateInboxId(getAddress(address), environment) } export async function encryptAttachment( - inboxId: string, file: DecryptedLocalAttachment ): Promise { const fileJson = JSON.stringify(file) @@ -821,7 +599,6 @@ export async function encryptAttachment( } export async function decryptAttachment( - inboxId: string, encryptedFile: EncryptedLocalAttachment ): Promise { const encryptedFileJson = JSON.stringify(encryptedFile) @@ -832,360 +609,90 @@ export async function decryptAttachment( return JSON.parse(fileJson) } -export async function listConversations< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->(client: Client): Promise[]> { - return (await XMTPModule.listConversations(client.inboxId)).map( - (json: string) => { - return new Conversation(client, JSON.parse(json)) - } - ) -} - -export async function listAll< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - client: Client -): Promise[]> { - const list = await XMTPModule.listAll(client.inboxId) - return list.map((json: string) => { - const jsonObj = JSON.parse(json) - if (jsonObj.version === ConversationVersion.GROUP) { - return new Group(client, jsonObj) - } else { - return new Conversation(client, jsonObj) - } - }) -} - -export async function listMessages< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - client: Client, - conversationTopic: string, - limit?: number | undefined, - before?: number | Date | undefined, - after?: number | Date | undefined, - direction?: - | 'SORT_DIRECTION_ASCENDING' - | 'SORT_DIRECTION_DESCENDING' - | undefined -): Promise[]> { - const messages = await XMTPModule.loadMessages( - client.inboxId, - conversationTopic, - limit, - typeof before === 'number' ? before : before?.getTime(), - typeof after === 'number' ? after : after?.getTime(), - direction || 'SORT_DIRECTION_DESCENDING' - ) - - return messages.map((json: string) => { - return DecodedMessage.from(json, client) - }) -} - -export async function listBatchMessages< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - client: Client, - queries: Query[] -): Promise[]> { - const topics = queries.map((item) => { - return JSON.stringify({ - limit: item.pageSize || 0, - topic: item.contentTopic, - after: - (typeof item.startTime === 'number' - ? item.startTime - : item.startTime?.getTime()) || 0, - before: - (typeof item.endTime === 'number' - ? item.endTime - : item.endTime?.getTime()) || 0, - direction: item.direction || 'SORT_DIRECTION_DESCENDING', - }) - }) - const messages = await XMTPModule.loadBatchMessages(client.inboxId, topics) - - return messages.map((json: string) => { - return DecodedMessage.from(json, client) - }) -} - -// TODO: support conversation ID -export async function createConversation< - ContentTypes extends ContentCodec[], ->( - client: Client, - peerAddress: string, - context?: ConversationContext, - consentProofPayload?: invitation.ConsentProofPayload -): Promise> { - const consentProofData = consentProofPayload - ? Array.from( - invitation.ConsentProofPayload.encode(consentProofPayload).finish() - ) - : [] - return new Conversation( - client, - JSON.parse( - await XMTPModule.createConversation( - client.inboxId, - getAddress(peerAddress), - JSON.stringify(context || {}), - consentProofData - ) - ) - ) -} - -export async function sendWithContentType( - inboxId: string, - conversationTopic: string, - content: T, - codec: ContentCodec -): Promise { - if ('contentKey' in codec) { - const contentJson = JSON.stringify(content) - return await XMTPModule.sendMessage(inboxId, conversationTopic, contentJson) - } else { - const encodedContent = codec.encode(content) - encodedContent.fallback = codec.fallback(content) - const encodedContentData = EncodedContent.encode(encodedContent).finish() - - return await XMTPModule.sendEncodedContent( - inboxId, - conversationTopic, - Array.from(encodedContentData) - ) - } -} - -export async function sendMessage< - SendContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - inboxId: string, - conversationTopic: string, - content: ConversationSendPayload -): Promise { - // TODO: consider eager validating of `MessageContent` here - // instead of waiting for native code to validate - const contentJson = JSON.stringify(content) - return await XMTPModule.sendMessage(inboxId, conversationTopic, contentJson) -} - -export async function prepareMessage< - PrepareContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - inboxId: string, - conversationTopic: string, - content: ConversationSendPayload -): Promise { - // TODO: consider eager validating of `MessageContent` here - // instead of waiting for native code to validate - const contentJson = JSON.stringify(content) - const preparedJson = await XMTPModule.prepareMessage( - inboxId, - conversationTopic, - contentJson - ) - return JSON.parse(preparedJson) -} - -export async function prepareMessageWithContentType( - inboxId: string, - conversationTopic: string, - content: any, - codec: ContentCodec -): Promise { - if ('contentKey' in codec) { - return prepareMessage(inboxId, conversationTopic, content) - } - const encodedContent = codec.encode(content) - encodedContent.fallback = codec.fallback(content) - const encodedContentData = EncodedContent.encode(encodedContent).finish() - const preparedJson = await XMTPModule.prepareEncodedMessage( - inboxId, - conversationTopic, - Array.from(encodedContentData) - ) - return JSON.parse(preparedJson) -} - -export async function sendPreparedMessage( - inboxId: string, - preparedLocalMessage: PreparedLocalMessage -): Promise { - const preparedLocalMessageJson = JSON.stringify(preparedLocalMessage) - return await XMTPModule.sendPreparedMessage(inboxId, preparedLocalMessageJson) -} - -export function subscribeToConversations(inboxId: string) { - return XMTPModule.subscribeToConversations(inboxId) -} - -export function subscribeToAll(inboxId: string) { - return XMTPModule.subscribeToAll(inboxId) -} - -export function subscribeToGroups(inboxId: string) { - return XMTPModule.subscribeToGroups(inboxId) +export function subscribeToConversations( + inboxId: InboxId, + type: ConversationType +) { + return XMTPModule.subscribeToConversations(inboxId, type) } export function subscribeToAllMessages( - inboxId: string, - includeGroups: boolean + inboxId: InboxId, + type: ConversationType ) { - return XMTPModule.subscribeToAllMessages(inboxId, includeGroups) + return XMTPModule.subscribeToAllMessages(inboxId, type) } -export function subscribeToAllGroupMessages(inboxId: string) { - return XMTPModule.subscribeToAllGroupMessages(inboxId) -} - -export async function subscribeToMessages(inboxId: string, topic: string) { - return await XMTPModule.subscribeToMessages(inboxId, topic) +export async function subscribeToMessages( + inboxId: InboxId, + id: ConversationId +) { + return await XMTPModule.subscribeToMessages(inboxId, id) } -export function unsubscribeFromConversations(inboxId: string) { +export function unsubscribeFromConversations(inboxId: InboxId) { return XMTPModule.unsubscribeFromConversations(inboxId) } -export function unsubscribeFromGroups(inboxId: string) { - return XMTPModule.unsubscribeFromGroups(inboxId) -} - -export function unsubscribeFromAllMessages(inboxId: string) { +export function unsubscribeFromAllMessages(inboxId: InboxId) { return XMTPModule.unsubscribeFromAllMessages(inboxId) } -export function unsubscribeFromAllGroupMessages(inboxId: string) { - return XMTPModule.unsubscribeFromAllGroupMessages(inboxId) -} - -export async function unsubscribeFromMessages(inboxId: string, topic: string) { - return await XMTPModule.unsubscribeFromMessages(inboxId, topic) -} - -export function subscribeToV3Conversations(inboxId: string) { - return XMTPModule.subscribeToV3Conversations(inboxId) -} - -export function subscribeToAllConversationMessages(inboxId: string) { - return XMTPModule.subscribeToAllConversationMessages(inboxId) -} - -export async function subscribeToConversationMessages( - inboxId: string, - id: string +export async function unsubscribeFromMessages( + inboxId: InboxId, + id: ConversationId ) { - return await XMTPModule.subscribeToConversationMessages(inboxId, id) -} - -export function unsubscribeFromAllConversationMessages(inboxId: string) { - return XMTPModule.unsubscribeFromAllConversationMessages(inboxId) -} - -export function unsubscribeFromV3Conversations(inboxId: string) { - return XMTPModule.unsubscribeFromV3Conversations(inboxId) -} - -export async function unsubscribeFromConversationMessages( - inboxId: string, - id: string -) { - return await XMTPModule.unsubscribeFromConversationMessages(inboxId, id) + return await XMTPModule.unsubscribeFromMessages(inboxId, id) } export function registerPushToken(pushServer: string, token: string) { return XMTPModule.registerPushToken(pushServer, token) } -export function subscribePushTopics(inboxId: string, topics: string[]) { - return XMTPModule.subscribePushTopics(inboxId, topics) -} - -export async function decodeMessage< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - inboxId: string, - topic: string, - encryptedMessage: string -): Promise> { - return JSON.parse( - await XMTPModule.decodeMessage(inboxId, topic, encryptedMessage) - ) +export function subscribePushTopics(topics: ConversationTopic[]) { + return XMTPModule.subscribePushTopics(topics) } export async function conversationConsentState( - inboxId: string, - conversationTopic: string -): Promise { - return await XMTPModule.conversationConsentState(inboxId, conversationTopic) -} - -export async function conversationV3ConsentState( - inboxId: string, - conversationId: string + inboxId: InboxId, + conversationId: ConversationId ): Promise { return await XMTPModule.conversationV3ConsentState(inboxId, conversationId) } -export async function isAllowed( - inboxId: string, - address: string -): Promise { - return await XMTPModule.isAllowed(inboxId, address) +export async function consentConversationIdState( + inboxId: InboxId, + conversationId: ConversationId +): Promise { + return await XMTPModule.consentConversationIdState(inboxId, conversationId) } -export async function isDenied( - inboxId: string, - address: string -): Promise { - return await XMTPModule.isDenied(inboxId, address) +export async function consentInboxIdState( + inboxId: InboxId, + peerInboxId: InboxId +): Promise { + return await XMTPModule.consentInboxIdState(inboxId, peerInboxId) } -export async function denyContacts( - inboxId: string, - addresses: string[] -): Promise { - return await XMTPModule.denyContacts(inboxId, addresses) +export async function consentAddressState( + inboxId: InboxId, + address: Address +): Promise { + return await XMTPModule.consentAddressState(inboxId, address) } -export async function allowContacts( - inboxId: string, - addresses: string[] +export async function setConsentState( + inboxId: InboxId, + value: string, + entryType: ConsentListEntryType, + consentType: ConsentState ): Promise { - return await XMTPModule.allowContacts(inboxId, addresses) -} - -export async function refreshConsentList( - inboxId: string -): Promise { - const consentList = await XMTPModule.refreshConsentList(inboxId) - - return consentList.map((json: string) => { - return ConsentListEntry.from(json) - }) -} - -export async function consentList( - inboxId: string -): Promise { - const consentList = await XMTPModule.consentList(inboxId) - - return consentList.map((json: string) => { - return ConsentListEntry.from(json) - }) -} - -export function preEnableIdentityCallbackCompleted() { - XMTPModule.preEnableIdentityCallbackCompleted() -} - -export function preCreateIdentityCallbackCompleted() { - XMTPModule.preCreateIdentityCallbackCompleted() + return await XMTPModule.setConsentState( + inboxId, + value, + entryType, + consentType + ) } export function preAuthenticateToInboxCallbackCompleted() { @@ -1193,91 +700,91 @@ export function preAuthenticateToInboxCallbackCompleted() { } export async function isGroupActive( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { return XMTPModule.isGroupActive(inboxId, id) } export async function addedByInboxId( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { return XMTPModule.addedByInboxId(inboxId, id) as InboxId } export async function creatorInboxId( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { return XMTPModule.creatorInboxId(inboxId, id) as InboxId } export async function isAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.isAdmin(clientInboxId, id, inboxId) } export async function isSuperAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.isSuperAdmin(clientInboxId, id, inboxId) } export async function listAdmins( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { return XMTPModule.listAdmins(inboxId, id) } export async function listSuperAdmins( - inboxId: string, - id: string + inboxId: InboxId, + id: ConversationId ): Promise { return XMTPModule.listSuperAdmins(inboxId, id) } export async function addAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.addAdmin(clientInboxId, id, inboxId) } export async function addSuperAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.addSuperAdmin(clientInboxId, id, inboxId) } export async function removeAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.removeAdmin(clientInboxId, id, inboxId) } export async function removeSuperAdmin( - clientInboxId: string, - id: string, - inboxId: string + clientInboxId: InboxId, + id: ConversationId, + inboxId: InboxId ): Promise { return XMTPModule.removeSuperAdmin(clientInboxId, id, inboxId) } export async function updateAddMemberPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateAddMemberPermission( @@ -1288,8 +795,8 @@ export async function updateAddMemberPermission( } export async function updateRemoveMemberPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateRemoveMemberPermission( @@ -1300,8 +807,8 @@ export async function updateRemoveMemberPermission( } export async function updateAddAdminPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateAddAdminPermission( @@ -1312,8 +819,8 @@ export async function updateAddAdminPermission( } export async function updateRemoveAdminPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateRemoveAdminPermission( @@ -1324,8 +831,8 @@ export async function updateRemoveAdminPermission( } export async function updateGroupNamePermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupNamePermission( @@ -1336,8 +843,8 @@ export async function updateGroupNamePermission( } export async function updateGroupImageUrlSquarePermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupImageUrlSquarePermission( @@ -1348,8 +855,8 @@ export async function updateGroupImageUrlSquarePermission( } export async function updateGroupDescriptionPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupDescriptionPermission( @@ -1360,8 +867,8 @@ export async function updateGroupDescriptionPermission( } export async function updateGroupPinnedFrameUrlPermission( - clientInboxId: string, - id: string, + clientInboxId: InboxId, + id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupPinnedFrameUrlPermission( @@ -1372,89 +879,29 @@ export async function updateGroupPinnedFrameUrlPermission( } export async function permissionPolicySet( - clientInboxId: string, - id: string + clientInboxId: InboxId, + id: ConversationId ): Promise { const json = await XMTPModule.permissionPolicySet(clientInboxId, id) return JSON.parse(json) } -export async function allowGroups( - inboxId: string, - groupIds: string[] -): Promise { - return XMTPModule.allowGroups(inboxId, groupIds) -} - -export async function denyGroups( - inboxId: string, - groupIds: string[] -): Promise { - return XMTPModule.denyGroups(inboxId, groupIds) -} - -export async function isGroupAllowed( - inboxId: string, - groupId: string -): Promise { - return XMTPModule.isGroupAllowed(inboxId, groupId) -} - -export async function isGroupDenied( - inboxId: string, - groupId: string -): Promise { - return XMTPModule.isGroupDenied(inboxId, groupId) -} - export async function updateConversationConsent( - inboxId: string, - conversationId: string, - state: string + inboxId: InboxId, + conversationId: ConversationId, + state: ConsentState ): Promise { return XMTPModule.updateConversationConsent(inboxId, conversationId, state) } -export async function allowInboxes( - inboxId: string, - inboxIds: string[] -): Promise { - return XMTPModule.allowInboxes(inboxId, inboxIds) -} - -export async function denyInboxes( - inboxId: string, - inboxIds: string[] -): Promise { - return XMTPModule.denyInboxes(inboxId, inboxIds) -} - -export async function isInboxAllowed( - clientInboxId: string, - inboxId: string -): Promise { - return XMTPModule.isInboxAllowed(clientInboxId, inboxId) -} - -export async function isInboxDenied( - clientInboxId: string, - inboxId: string -): Promise { - return XMTPModule.isInboxDenied(clientInboxId, inboxId) -} - -export async function processConversationMessage< +export async function processMessage< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >( client: Client, - id: string, + id: ConversationId, encryptedMessage: string ): Promise> { - const json = XMTPModule.processConversationMessage( - client.inboxId, - id, - encryptedMessage - ) + const json = XMTPModule.processMessage(client.inboxId, id, encryptedMessage) return DecodedMessage.from(json, client) } @@ -1463,21 +910,7 @@ export async function processWelcomeMessage< >( client: Client, encryptedMessage: string -): Promise> { - const json = await XMTPModule.processWelcomeMessage( - client.inboxId, - encryptedMessage - ) - const group = JSON.parse(json) - return new Group(client, group) -} - -export async function processConversationWelcomeMessage< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, ->( - client: Client, - encryptedMessage: string -): Promise>> { +): Promise>> { const json = await XMTPModule.processConversationWelcomeMessage( client.inboxId, encryptedMessage @@ -1500,7 +933,6 @@ export const emitter = new EventEmitter(XMTPModule ?? NativeModulesProxy.XMTP) interface AuthParams { environment: string appVersion?: string - enableV3?: boolean dbDirectory?: string historySyncUrl?: string walletType?: string @@ -1515,19 +947,21 @@ interface CreateGroupParams { pinnedFrameUrl: string } -export * from './XMTP.types' export { Client } from './lib/Client' export * from './lib/ContentCodec' -export { Conversation } from './lib/Conversation' -export { - ConversationContainer, - ConversationVersion, -} from './lib/Conversation' -export { Query } from './lib/Query' +export { Conversation, ConversationVersion } from './lib/Conversation' export { XMTPPush } from './lib/XMTPPush' -export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus } +export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus, ConsentState } export { Group } from './lib/Group' export { Dm } from './lib/Dm' export { Member } from './lib/Member' -export { InboxId } from './lib/Client' -export { GroupOptions, ConversationOrder } from './lib/types/GroupOptions' +export { InboxId, XMTPEnvironment } from './lib/Client' +export { + ConversationOptions, + ConversationOrder, + ConversationId, + ConversationTopic, + ConversationType, +} from './lib/types/ConversationOptions' +export { Address } from './utils/address' +export { MessageId, MessageOrder } from './lib/types/MessagesOptions' diff --git a/src/lib/Client.ts b/src/lib/Client.ts index abadeeb13..429ffb7fe 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -883,12 +883,17 @@ export class Client< } } } +export type XMTPEnvironment = 'local' | 'dev' | 'production' export type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ - env: 'local' | 'dev' | 'production' + env: XMTPEnvironment + /** + * REQUIRED specify the encryption key for the database. The encryption key must be exactly 32 bytes. + */ + dbEncryptionKey: Uint8Array /** * identifier that's included with API requests. * @@ -900,21 +905,10 @@ export type ClientOptions = { * SDK updates, including deprecations and required upgrades. */ appVersion?: string - /** * Set optional callbacks for handling identity setup */ - preCreateIdentityCallback?: () => Promise | void - preEnableIdentityCallback?: () => Promise | void preAuthenticateToInboxCallback?: () => Promise | void - /** - * Specify whether to enable V3 version of MLS (Group Chat) - */ - enableV3?: boolean - /** - * REQUIRED specify the encryption key for the database. The encryption key must be exactly 32 bytes. - */ - dbEncryptionKey?: Uint8Array /** * OPTIONAL specify the XMTP managed database directory */ diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 50a86ffcf..c1794ed81 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -11,9 +11,8 @@ import { Group, GroupParams } from './Group' import { Member } from './Member' import { CreateGroupOptions } from './types/CreateGroupOptions' import { EventTypes } from './types/EventTypes' -import { ConversationOrder, GroupOptions } from './types/GroupOptions' +import { ConversationOrder, GroupOptions } from './types/ConversationOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' -import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' import { getAddress } from '../utils/address' diff --git a/src/lib/InboxState.ts b/src/lib/InboxState.ts index 5181d83d1..cef9593f4 100644 --- a/src/lib/InboxState.ts +++ b/src/lib/InboxState.ts @@ -1,16 +1,17 @@ +import { Address } from '../utils/address' import { InboxId } from './Client' export class InboxState { inboxId: InboxId - addresses: string[] + addresses: Address[] installations: Installation[] - recoveryAddress: string + recoveryAddress: Address constructor( inboxId: InboxId, - addresses: string[], + addresses: Address[], installations: Installation[], - recoveryAddress: string + recoveryAddress: Address ) { this.inboxId = inboxId this.addresses = addresses diff --git a/src/lib/Member.ts b/src/lib/Member.ts index 443034194..fcd9d5e25 100644 --- a/src/lib/Member.ts +++ b/src/lib/Member.ts @@ -1,3 +1,4 @@ +import { Address } from '../utils/address' import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' @@ -5,13 +6,13 @@ export type PermissionLevel = 'member' | 'admin' | 'super_admin' export class Member { inboxId: InboxId - addresses: string[] + addresses: Address[] permissionLevel: PermissionLevel consentState: ConsentState constructor( inboxId: InboxId, - addresses: string[], + addresses: Address[], permissionLevel: PermissionLevel, consentState: ConsentState ) { diff --git a/src/lib/Query.ts b/src/lib/Query.ts deleted file mode 100644 index 89273ae30..000000000 --- a/src/lib/Query.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Query = { - startTime?: number | Date | undefined - endTime?: number | Date | undefined - contentTopic: string - pageSize?: number | undefined - direction?: - | 'SORT_DIRECTION_ASCENDING' - | 'SORT_DIRECTION_DESCENDING' - | undefined -} diff --git a/src/lib/XMTPPush.ts b/src/lib/XMTPPush.ts index 58680b7cc..ac0a3832d 100644 --- a/src/lib/XMTPPush.ts +++ b/src/lib/XMTPPush.ts @@ -1,5 +1,6 @@ import { Client } from './Client' import * as XMTPModule from '../index' +import { ConversationTopic } from '../index' export class XMTPPush { client: Client @@ -11,7 +12,7 @@ export class XMTPPush { XMTPModule.registerPushToken(server, token) } - subscribe(topics: string[]) { - XMTPModule.subscribePushTopics(this.client.address, topics) + subscribe(topics: ConversationTopic[]) { + XMTPModule.subscribePushTopics(topics) } } diff --git a/src/lib/types/ContentCodec.ts b/src/lib/types/ContentCodec.ts index 6dd5dcda3..43b53773b 100644 --- a/src/lib/types/ContentCodec.ts +++ b/src/lib/types/ContentCodec.ts @@ -71,35 +71,6 @@ export type GroupUpdatedContent = { metadataFieldsChanged: GroupUpdatedMetadatEntry[] } -// This contains a message that has been prepared for sending. -// It contains the message ID and the URI of a local file -// containing the payload that needs to be published. -// See Conversation.sendPreparedMessage() and Client.sendPreparedMessage() -// -// For native integrations (e.g. if you have native code for a robust -// pending-message queue in a background task) you can load the referenced -// `preparedFileUri` as a serialized `PreparedMessage` with the native SDKs. -// The contained `envelopes` can then be directly `.publish()`ed with the native `Client`. -// e.g. on iOS: -// let preparedFileUrl = URL(string: preparedFileUri) -// let preparedData = try Data(contentsOf: preparedFileUrl) -// let prepared = try PreparedMessage.fromSerializedData(preparedData) -// try await client.publish(envelopes: prepared.envelopes) -// e.g. on Android: -// val preparedFileUri = Uri.parse(preparedFileUri) -// val preparedData = contentResolver.openInputStream(preparedFileUrl)!! -// .use { it.buffered().readBytes() } -// val prepared = PreparedMessage.fromSerializedData(preparedData) -// client.publish(envelopes = prepared.envelopes) -// -// You can also stuff the `preparedData` elsewhere (e.g. in a database) if that -// is more convenient for your use case. -export type PreparedLocalMessage = { - messageId: string - preparedFileUri: `file://${string}` - preparedAt: number // timestamp in milliseconds -} - export type NativeMessageContent = { text?: string unknown?: UnknownContent diff --git a/src/lib/types/GroupOptions.ts b/src/lib/types/ConversationOptions.ts similarity index 58% rename from src/lib/types/GroupOptions.ts rename to src/lib/types/ConversationOptions.ts index 5bfa36660..b9bc4d89f 100644 --- a/src/lib/types/GroupOptions.ts +++ b/src/lib/types/ConversationOptions.ts @@ -1,4 +1,4 @@ -export type GroupOptions = { +export type ConversationOptions = { isActive?: boolean addedByInboxId?: boolean name?: boolean @@ -11,3 +11,8 @@ export type GroupOptions = { export type ConversationOrder = | 'lastMessage' // Ordered by the last message that was sent | 'createdAt' // DEFAULT: Ordered by the date the conversation was created + +export type ConversationType = 'all' | 'groups' | 'dms' + +export type ConversationId = string & { readonly brand: unique symbol } +export type ConversationTopic = string & { readonly brand: unique symbol } diff --git a/src/lib/types/EventTypes.ts b/src/lib/types/EventTypes.ts index d02e22966..f6cbed4b3 100644 --- a/src/lib/types/EventTypes.ts +++ b/src/lib/types/EventTypes.ts @@ -2,56 +2,19 @@ export enum EventTypes { // Auth Sign = 'sign', Authed = 'authed', - AuthedV3 = 'authedV3', - BundleAuthed = 'bundleAuthed', - PreCreateIdentityCallback = 'preCreateIdentityCallback', - PreEnableIdentityCallback = 'preEnableIdentityCallback', + PreAuthenticateToInboxCallback = 'preAuthenticateToInboxCallback', // Conversations Events /** * Current user is in a newly created conversation */ Conversation = 'conversation', - /** - * Current user is in a newly created group - */ - Group = 'group', - /** - * Current user is in a newly created group or conversation - */ - ConversationContainer = 'conversationContainer', /** * Current user receives a new message in any conversation */ Message = 'message', - /** - * Current user receives a new message in any group - * Current limitation, only groups created before listener is added will be received - */ - AllGroupMessage = 'allGroupMessage', - // Conversation Events /** * A new message is sent to a specific conversation */ ConversationMessage = 'conversationMessage', - // Group Events - /** - * A new message is sent to a specific group - */ - GroupMessage = 'groupMessage', - // Conversation Events - /** - * A new message is sent to a specific conversation - */ - ConversationV3 = 'conversationV3', - // All Conversation Message Events - /** - * A new message is sent to any V3 conversation - */ - AllConversationMessages = 'allConversationMessages', - // Conversation Events - /** - * A new V3 conversation is created - */ - ConversationV3Message = 'conversationV3Message', } diff --git a/src/lib/types/MessagesOptions.ts b/src/lib/types/MessagesOptions.ts index 91b2967ef..a8688ebc3 100644 --- a/src/lib/types/MessagesOptions.ts +++ b/src/lib/types/MessagesOptions.ts @@ -2,8 +2,8 @@ export type MessagesOptions = { limit?: number | undefined before?: number | Date | undefined after?: number | Date | undefined - direction?: - | 'SORT_DIRECTION_ASCENDING' - | 'SORT_DIRECTION_DESCENDING' - | undefined + direction?: MessageOrder | undefined } + +export type MessageOrder = 'ASCENDING' | 'DESCENDING' +export type MessageId = string & { readonly brand: unique symbol } diff --git a/src/utils/address.ts b/src/utils/address.ts index cbca82c70..ec3045e82 100644 --- a/src/utils/address.ts +++ b/src/utils/address.ts @@ -4,6 +4,8 @@ import { TextEncoder } from 'text-encoding' const addressRegex = /^0x[a-fA-F0-9]{40}$/ const encoder = new TextEncoder() +export type Address = string & { readonly brand: unique symbol } + export function stringToBytes(value: string): Uint8Array { const bytes = encoder.encode(value) return bytes From fb606b5151078d8ae42fd4dc44a9e1bc2c5e5220 Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 10 Nov 2024 08:37:45 -0800 Subject: [PATCH 07/21] get the RN side all compiling --- .../wrappers/DecodedMessageWrapper.kt | 2 +- src/index.ts | 2 +- src/lib/Client.ts | 526 ++---------------- src/lib/Contacts.ts | 76 --- src/lib/Conversation.ts | 4 +- src/lib/Conversations.ts | 414 +++----------- src/lib/DecodedMessage.ts | 12 +- src/lib/Dm.ts | 56 +- src/lib/Group.ts | 74 +-- src/lib/InboxState.ts | 2 +- src/lib/Member.ts | 2 +- src/lib/PrivatePreferences.ts | 42 ++ src/lib/types/MessagesOptions.ts | 4 +- src/utils/address.ts | 2 +- 14 files changed, 213 insertions(+), 1005 deletions(-) delete mode 100644 src/lib/Contacts.ts create mode 100644 src/lib/PrivatePreferences.ts diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt index d2a7e6f54..d0f1662f7 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -23,7 +23,7 @@ class DecodedMessageWrapper { "contentTypeId" to model.encodedContent.type.description, "content" to ContentJson(model.encodedContent).toJsonMap(), "senderAddress" to model.senderAddress, - "sent" to model.sent.time, + "sentNs" to model.sent.time, "fallback" to fallback, "deliveryStatus" to model.deliveryStatus.toString() ) diff --git a/src/index.ts b/src/index.ts index 1d0adda46..48df34727 100644 --- a/src/index.ts +++ b/src/index.ts @@ -581,7 +581,7 @@ export async function canMessage( } export async function getOrCreateInboxId( - address: InboxId, + address: Address, environment: XMTPEnvironment ): Promise { return await XMTPModule.getOrCreateInboxId(getAddress(address), environment) diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 429ffb7fe..accbe1844 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -2,21 +2,19 @@ import { splitSignature } from '@ethersproject/bytes' import { Subscription } from 'expo-modules-core' import type { WalletClient } from 'viem' -import Contacts from './Contacts' import type { DecryptedLocalAttachment, EncryptedLocalAttachment, - PreparedLocalMessage, } from './ContentCodec' import Conversations from './Conversations' import { InboxState } from './InboxState' import { TextCodec } from './NativeCodecs/TextCodec' -import { Query } from './Query' +import PrivatePreferences from './PrivatePreferences' import { Signer, getSigner } from './Signer' import { DefaultContentTypes } from './types/DefaultContentType' import { hexToBytes } from './util' import * as XMTPModule from '../index' -import { DecodedMessage } from '../index' +import { Address } from '../index' declare const Buffer @@ -36,130 +34,19 @@ export class Client< installationId: string dbPath: string conversations: Conversations - contacts: Contacts + preferences: PrivatePreferences codecRegistry: { [key: string]: XMTPModule.ContentCodec } private static signSubscription: Subscription | null = null private static authSubscription: Subscription | null = null - /** - * Creates a new instance of the Client class using the provided signer. - * - * @param {Signer} signer - The signer object used for authentication and message signing. - * @param {Partial} opts - Optional configuration options for the Client. - * @returns {Promise} A Promise that resolves to a new Client instance. - * - * See {@link https://xmtp.org/docs/build/authentication#create-a-client | XMTP Docs} for more information. - */ - static async create< - ContentCodecs extends DefaultContentTypes = DefaultContentTypes, - >( - wallet: Signer | WalletClient | null, - options: ClientOptions & { codecs?: ContentCodecs } - ): Promise> { - if ( - options.enableV3 === true && - (options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32) - ) { - throw new Error('Must pass an encryption key that is exactly 32 bytes.') - } - const { enableSubscription, createSubscription, authInboxSubscription } = - this.setupSubscriptions(options) - const signer = getSigner(wallet) - if (!signer) { - throw new Error('Signer is not configured') - } - return new Promise>((resolve, reject) => { - ;(async () => { - this.signSubscription = XMTPModule.emitter.addListener( - 'sign', - async (message: { id: string; message: string }) => { - const request: { id: string; message: string } = message - try { - const signatureString = await signer.signMessage(request.message) - const eSig = splitSignature(signatureString) - const r = hexToBytes(eSig.r) - const s = hexToBytes(eSig.s) - const sigBytes = new Uint8Array(65) - sigBytes.set(r) - sigBytes.set(s, r.length) - sigBytes[64] = eSig.recoveryParam - - const signature = Buffer.from(sigBytes).toString('base64') - - await XMTPModule.receiveSignature(request.id, signature) - } catch (e) { - const errorMessage = 'ERROR in create. User rejected signature' - console.info(errorMessage, e) - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) - reject(errorMessage) - } - } - ) - - this.authSubscription = XMTPModule.emitter.addListener( - 'authed', - async (message: { - inboxId: string - address: string - installationId: string - dbPath: string - }) => { - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) - resolve( - new Client( - message.address, - message.inboxId as InboxId, - message.installationId, - message.dbPath, - options.codecs || [] - ) - ) - } - ) - await XMTPModule.auth( - await signer.getAddress(), - options.env, - options.appVersion, - Boolean(createSubscription), - Boolean(enableSubscription), - Boolean(authInboxSubscription), - Boolean(options.enableV3), - options.dbEncryptionKey, - options.dbDirectory, - options.historySyncUrl - ) - })().catch((error) => { - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) - console.error('ERROR in create: ', error) - }) - }) - } - static async exportNativeLogs() { return XMTPModule.exportNativeLogs() } private static removeAllSubscriptions( - createSubscription?: Subscription, - enableSubscription?: Subscription, authInboxSubscription?: Subscription ): void { ;[ - createSubscription, - enableSubscription, authInboxSubscription, this.signSubscription, this.authSubscription, @@ -172,192 +59,24 @@ export class Client< /** * Creates a new instance of the XMTP Client with a randomly generated address. * - * @param {Partial} opts - Optional configuration options for the Client. + * @param {Partial} opts - Configuration options for the Client. Must include encryption key. * @returns {Promise} A Promise that resolves to a new Client instance with a random address. */ static async createRandom( options: ClientOptions & { codecs?: ContentTypes } ): Promise> { - if ( - options.enableV3 === true && - (options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32) - ) { + if (options.dbEncryptionKey.length !== 32) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } - const { createSubscription, enableSubscription, authInboxSubscription } = - this.setupSubscriptions(options) + const { authInboxSubscription } = this.setupSubscriptions(options) const client = await XMTPModule.createRandom( options.env, - options.appVersion, - Boolean(createSubscription), - Boolean(enableSubscription), - Boolean(authInboxSubscription), - Boolean(options.enableV3), options.dbEncryptionKey, - options.dbDirectory, - options.historySyncUrl - ) - this.removeSubscription(createSubscription) - this.removeSubscription(enableSubscription) - this.removeSubscription(authInboxSubscription) - - return new Client( - client['address'], - client['inboxId'], - client['installationId'], - client['dbPath'], - options?.codecs || [] - ) - } - - /** - * Creates a new instance of the Client class from a provided key bundle. - * - * This method is useful for scenarios where you want to manually handle private key storage, - * allowing the application to have access to XMTP keys without exposing wallet keys. - * - * @param {string} keyBundle - The key bundle used for address generation. - * @param {Partial} opts - Optional configuration options for the Client. - * @returns {Promise} A Promise that resolves to a new Client instance based on the provided key bundle. - */ - static async createFromKeyBundle< - ContentCodecs extends DefaultContentTypes = [], - >( - keyBundle: string, - options: ClientOptions & { codecs?: ContentCodecs }, - wallet?: Signer | WalletClient | undefined - ): Promise> { - if ( - options.enableV3 === true && - (options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32) - ) { - throw new Error('Must pass an encryption key that is exactly 32 bytes.') - } - - if (!wallet) { - const client = await XMTPModule.createFromKeyBundle( - keyBundle, - options.env, - options.appVersion, - Boolean(options.enableV3), - options.dbEncryptionKey, - options.dbDirectory, - options.historySyncUrl - ) - - return new Client( - client['address'], - client['inboxId'], - client['installationId'], - client['dbPath'], - options.codecs || [] - ) - } else { - const signer = getSigner(wallet) - if (!signer) { - throw new Error('Signer is not configured') - } - return new Promise>((resolve, reject) => { - ;(async () => { - this.signSubscription = XMTPModule.emitter.addListener( - 'sign', - async (message: { id: string; message: string }) => { - const request: { id: string; message: string } = message - try { - const signatureString = await signer.signMessage( - request.message - ) - const eSig = splitSignature(signatureString) - const r = hexToBytes(eSig.r) - const s = hexToBytes(eSig.s) - const sigBytes = new Uint8Array(65) - sigBytes.set(r) - sigBytes.set(s, r.length) - sigBytes[64] = eSig.recoveryParam - - const signature = Buffer.from(sigBytes).toString('base64') - - await XMTPModule.receiveSignature(request.id, signature) - } catch (e) { - this.removeAllSubscriptions() - const errorMessage = 'ERROR in create. User rejected signature' - console.info(errorMessage, e) - reject(errorMessage) - } - } - ) - - this.authSubscription = XMTPModule.emitter.addListener( - 'bundleAuthed', - async (message: { - inboxId: string - address: string - installationId: string - dbPath: string - }) => { - this.removeAllSubscriptions() - resolve( - new Client( - message.address, - message.inboxId as InboxId, - message.installationId, - message.dbPath, - options.codecs || [] - ) - ) - } - ) - await XMTPModule.createFromKeyBundleWithSigner( - await signer.getAddress(), - keyBundle, - options.env, - options.appVersion, - Boolean(options.enableV3), - options.dbEncryptionKey, - options.dbDirectory, - options.historySyncUrl - ) - })().catch((error) => { - this.removeAllSubscriptions() - console.error('ERROR in create: ', error) - }) - }) - } - } - - /** - * Creates a new V3 ONLY instance of the XMTP Client with a randomly generated address. - * - * @param {Partial} opts - Configuration options for the Client. Must include encryption key. - * @returns {Promise} A Promise that resolves to a new V3 ONLY Client instance with a random address. - */ - static async createRandomV3( - options: ClientOptions & { codecs?: ContentTypes } - ): Promise> { - options.enableV3 = true - if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 - ) { - throw new Error('Must pass an encryption key that is exactly 32 bytes.') - } - const { createSubscription, enableSubscription, authInboxSubscription } = - this.setupSubscriptions(options) - const client = await XMTPModule.createRandomV3( - options.env, options.appVersion, - Boolean(createSubscription), - Boolean(enableSubscription), Boolean(authInboxSubscription), - Boolean(options.enableV3), - options.dbEncryptionKey, options.dbDirectory, options.historySyncUrl ) - this.removeSubscription(createSubscription) - this.removeSubscription(enableSubscription) this.removeSubscription(authInboxSubscription) return new Client( @@ -370,29 +89,24 @@ export class Client< } /** - * Creates a new V3 ONLY instance of the Client class using the provided signer. + * Creates a new instance of the Client class using the provided signer. * * @param {Signer} signer - The signer object used for authentication and message signing. * @param {Partial} opts - Configuration options for the Client. Must include an encryption key. - * @returns {Promise} A Promise that resolves to a new V3 ONLY Client instance. + * @returns {Promise} A Promise that resolves to a new Client instance. * * See {@link https://xmtp.org/docs/build/authentication#create-a-client | XMTP Docs} for more information. */ - static async createV3< + static async create< ContentCodecs extends DefaultContentTypes = DefaultContentTypes, >( wallet: Signer | WalletClient | null, options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { - options.enableV3 = true - if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 - ) { + if (options.dbEncryptionKey.length !== 32) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } - const { enableSubscription, createSubscription, authInboxSubscription } = - this.setupSubscriptions(options) + const { authInboxSubscription } = this.setupSubscriptions(options) const signer = getSigner(wallet) if (!signer) { throw new Error('Signer is not configured') @@ -426,29 +140,21 @@ export class Client< } catch (e) { const errorMessage = 'ERROR in create. User rejected signature' console.info(errorMessage, e) - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) + this.removeAllSubscriptions(authInboxSubscription) reject(errorMessage) } } ) this.authSubscription = XMTPModule.emitter.addListener( - 'authedV3', + 'authed', async (message: { inboxId: string address: string installationId: string dbPath: string }) => { - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) + this.removeAllSubscriptions(authInboxSubscription) resolve( new Client( message.address, @@ -460,15 +166,12 @@ export class Client< ) } ) - await XMTPModule.createV3( + await XMTPModule.create( await signer.getAddress(), options.env, + options.dbEncryptionKey, options.appVersion, - Boolean(createSubscription), - Boolean(enableSubscription), Boolean(authInboxSubscription), - Boolean(options.enableV3), - options.dbEncryptionKey, options.dbDirectory, options.historySyncUrl, signer.walletType(), @@ -476,44 +179,35 @@ export class Client< signer.getBlockNumber() ) })().catch((error) => { - this.removeAllSubscriptions( - createSubscription, - enableSubscription, - authInboxSubscription - ) + this.removeAllSubscriptions(authInboxSubscription) console.error('ERROR in create: ', error) }) }) } /** - * Builds a V3 ONLY instance of the Client class using the provided address and chainId if SCW. + * Builds a instance of the Client class using the provided address and chainId if SCW. * * @param {string} address - The address of the account to build * @param {Partial} opts - Configuration options for the Client. Must include an encryption key. - * @returns {Promise} A Promise that resolves to a new V3 ONLY Client instance. + * @returns {Promise} A Promise that resolves to a new Client instance. * * See {@link https://xmtp.org/docs/build/authentication#create-a-client | XMTP Docs} for more information. */ - static async buildV3< + static async build< ContentCodecs extends DefaultContentTypes = DefaultContentTypes, >( - address: string, + address: Address, options: ClientOptions & { codecs?: ContentCodecs } ): Promise> { - options.enableV3 = true - if ( - options.dbEncryptionKey === undefined || - options.dbEncryptionKey.length !== 32 - ) { + if (options.dbEncryptionKey.length !== 32) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') } - const client = await XMTPModule.buildV3( + const client = await XMTPModule.build( address, options.env, - options.appVersion, - Boolean(options.enableV3), options.dbEncryptionKey, + options.appVersion, options.dbDirectory, options.historySyncUrl ) @@ -530,31 +224,10 @@ export class Client< /** * Drop the client from memory. Use when you want to remove the client from memory and are done with it. */ - static async dropClient(inboxId: string) { + static async dropClient(inboxId: InboxId) { return await XMTPModule.dropClient(inboxId) } - /** - * Static method to determine if the address is currently in our network. - * - * This method checks if the specified peer has signed up for XMTP. - * - * @param {string} peerAddress - The address of the peer to check for messaging eligibility. - * @param {Partial} opts - Optional configuration options for the Client. - * @returns {Promise} - */ - static async canMessage( - peerAddress: string, - opts?: Partial - ): Promise { - const options = defaultOptions(opts) - return await XMTPModule.staticCanMessage( - peerAddress, - options.env, - options.appVersion - ) - } - private static addSubscription( event: string, opts: ClientOptions, @@ -583,28 +256,8 @@ export class Client< } private static setupSubscriptions(opts: ClientOptions): { - createSubscription?: Subscription - enableSubscription?: Subscription authInboxSubscription?: Subscription } { - const enableSubscription = this.addSubscription( - 'preEnableIdentityCallback', - opts, - async () => { - await this.executeCallback(opts?.preEnableIdentityCallback) - XMTPModule.preEnableIdentityCallbackCompleted() - } - ) - - const createSubscription = this.addSubscription( - 'preCreateIdentityCallback', - opts, - async () => { - await this.executeCallback(opts?.preCreateIdentityCallback) - XMTPModule.preCreateIdentityCallbackCompleted() - } - ) - const authInboxSubscription = this.addSubscription( 'preAuthenticateToInboxCallback', opts, @@ -614,26 +267,25 @@ export class Client< } ) - return { createSubscription, enableSubscription, authInboxSubscription } + return { authInboxSubscription } } /** * Static method to determine the inboxId for the address. * - * @param {string} peerAddress - The address of the peer to check for messaging eligibility. + * @param {Address} peerAddress - The address of the peer to check for messaging eligibility. * @param {Partial} opts - Optional configuration options for the Client. * @returns {Promise} */ static async getOrCreateInboxId( - address: string, - opts?: Partial + address: Address, + env: XMTPEnvironment ): Promise { - const options = defaultOptions(opts) - return await XMTPModule.getOrCreateInboxId(address, options.env) + return await XMTPModule.getOrCreateInboxId(address, env) } constructor( - address: string, + address: Address, inboxId: InboxId, installationId: string, dbPath: string, @@ -644,7 +296,7 @@ export class Client< this.installationId = installationId this.dbPath = dbPath this.conversations = new Conversations(this) - this.contacts = new Contacts(this) + this.preferences = new PrivatePreferences(this) this.codecRegistry = {} this.register(new TextCodec()) @@ -659,52 +311,14 @@ export class Client< this.codecRegistry[id] = contentCodec } - async sign(digest: Uint8Array, keyType: KeyType): Promise { - return XMTPModule.sign( - this.inboxId, - digest, - keyType.kind, - keyType.prekeyIndex - ) - } - - async exportPublicKeyBundle(): Promise { - return XMTPModule.exportPublicKeyBundle(this.inboxId) - } - - /** - * Exports the key bundle associated with the current XMTP address. - * - * This method allows you to obtain the unencrypted key bundle for the current XMTP address. - * Ensure the exported keys are stored securely and encrypted. - * - * @returns {Promise} A Promise that resolves to the unencrypted key bundle for the current XMTP address. - */ - async exportKeyBundle(): Promise { - return XMTPModule.exportKeyBundle(this.inboxId) - } - /** - * Determines whether the current user can send messages to a specified peer over 1:1 conversations. - * - * This method checks if the specified peer has signed up for XMTP - * and ensures that the message is not addressed to the sender (no self-messaging). - * - * @param {string} peerAddress - The address of the peer to check for messaging eligibility. - * @returns {Promise} A Promise resolving to true if messaging is allowed, and false otherwise. - */ - async canMessage(peerAddress: string): Promise { - return await XMTPModule.canMessage(this.inboxId, peerAddress) - } - - /** - * Find the inboxId associated with this address + * Find the Address associated with this address * * @param {string} peerAddress - The address of the peer to check for inboxId. * @returns {Promise} A Promise resolving to the InboxId. */ async findInboxIdFromAddress( - peerAddress: string + peerAddress: Address ): Promise { return await XMTPModule.findInboxIdFromAddress(this.inboxId, peerAddress) } @@ -798,38 +412,11 @@ export class Client< * * This method checks if the specified peers are using clients that support group messaging. * - * @param {string[]} addresses - The addresses of the peers to check for messaging eligibility. - * @returns {Promise<{ [key: string]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the V3 network. - */ - async canGroupMessage( - addresses: string[] - ): Promise<{ [key: string]: boolean }> { - return await XMTPModule.canGroupMessage(this.inboxId, addresses) - } - - // TODO: support persisting conversations for quick lookup - // async importConversation(exported: string): Promise { ... } - // async exportConversation(topic: string): Promise { ... } - - /** - * Retrieves a list of batch messages based on the provided queries. - * - * This method pulls messages associated from multiple conversation with the current address - * and specified queries. - * - * @param {Query[]} queries - An array of queries to filter the batch messages. - * @returns {Promise} A Promise that resolves to a list of batch messages. - * @throws {Error} The error is logged, and the method gracefully returns an empty array. + * @param {Address[]} addresses - The addresses of the peers to check for messaging eligibility. + * @returns {Promise<{ [key: Address]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the V3 network. */ - async listBatchMessages( - queries: Query[] - ): Promise[]> { - try { - return await XMTPModule.listBatchMessages(this, queries) - } catch (e) { - console.info('ERROR in listBatchMessages', e) - return [] - } + async canMessage(addresses: Address[]): Promise<{ [key: Address]: boolean }> { + return await XMTPModule.canMessage(this.inboxId, addresses) } /** @@ -847,7 +434,7 @@ export class Client< if (!file.fileUri?.startsWith('file://')) { throw new Error('the attachment must be a local file:// uri') } - return await XMTPModule.encryptAttachment(this.inboxId, file) + return await XMTPModule.encryptAttachment(file) } /** @@ -864,23 +451,7 @@ export class Client< if (!encryptedFile.encryptedLocalFileUri?.startsWith('file://')) { throw new Error('the attachment must be a local file:// uri') } - return await XMTPModule.decryptAttachment(this.inboxId, encryptedFile) - } - - /** - * Sends a prepared message. - * - * @param {PreparedLocalMessage} prepared - The prepared local message to be sent. - * @returns {Promise} A Promise that resolves to a string identifier for the sent message. - * @throws {Error} Throws an error if there is an issue with sending the prepared message. - */ - async sendPreparedMessage(prepared: PreparedLocalMessage): Promise { - try { - return await XMTPModule.sendPreparedMessage(this.inboxId, prepared) - } catch (e) { - console.info('ERROR in sendPreparedMessage()', e) - throw e - } + return await XMTPModule.decryptAttachment(encryptedFile) } } export type XMTPEnvironment = 'local' | 'dev' | 'production' @@ -923,20 +494,3 @@ export type KeyType = { kind: 'identity' | 'prekey' prekeyIndex?: number } - -/** - * Provide a default client configuration. These settings can be used on their own, or as a starting point for custom configurations - * - * @param opts additional options to override the default settings - */ -export function defaultOptions(opts?: Partial): ClientOptions { - const _defaultOptions: ClientOptions = { - env: 'dev', - enableV3: false, - dbEncryptionKey: undefined, - dbDirectory: undefined, - historySyncUrl: undefined, - } - - return { ..._defaultOptions, ...opts } as ClientOptions -} diff --git a/src/lib/Contacts.ts b/src/lib/Contacts.ts deleted file mode 100644 index 97d8b9a12..000000000 --- a/src/lib/Contacts.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Client, InboxId } from './Client' -import { ConsentListEntry } from './ConsentListEntry' -import * as XMTPModule from '../index' -import { getAddress } from '../utils/address' - -export default class Contacts { - client: Client - - constructor(client: Client) { - this.client = client - } - - async isAllowed(address: string): Promise { - return await XMTPModule.isAllowed(this.client.inboxId, getAddress(address)) - } - - async isDenied(address: string): Promise { - return await XMTPModule.isDenied(this.client.inboxId, getAddress(address)) - } - - async deny(addresses: string[]): Promise { - const checkSummedAddresses = addresses.map((address) => getAddress(address)) - return await XMTPModule.denyContacts( - this.client.inboxId, - checkSummedAddresses - ) - } - - async allow(addresses: string[]): Promise { - const checkSummedAddresses = addresses.map((address) => getAddress(address)) - return await XMTPModule.allowContacts( - this.client.inboxId, - checkSummedAddresses - ) - } - - async refreshConsentList(): Promise { - return await XMTPModule.refreshConsentList(this.client.inboxId) - } - - async consentList(): Promise { - return await XMTPModule.consentList(this.client.inboxId) - } - - async allowGroups(groupIds: string[]): Promise { - return await XMTPModule.allowGroups(this.client.inboxId, groupIds) - } - - async denyGroups(groupIds: string[]): Promise { - return await XMTPModule.denyGroups(this.client.inboxId, groupIds) - } - - async isGroupAllowed(groupId: string): Promise { - return await XMTPModule.isGroupAllowed(this.client.inboxId, groupId) - } - - async isGroupDenied(groupId: string): Promise { - return await XMTPModule.isGroupDenied(this.client.inboxId, groupId) - } - - async allowInboxes(inboxIds: InboxId[]): Promise { - return await XMTPModule.allowInboxes(this.client.inboxId, inboxIds) - } - - async denyInboxes(inboxIds: InboxId[]): Promise { - return await XMTPModule.denyInboxes(this.client.inboxId, inboxIds) - } - - async isInboxAllowed(inboxId: InboxId): Promise { - return await XMTPModule.isInboxAllowed(this.client.inboxId, inboxId) - } - - async isInboxDenied(inboxId: InboxId): Promise { - return await XMTPModule.isInboxDenied(this.client.inboxId, inboxId) - } -} diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index e4770db4e..1e9c5626a 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -2,7 +2,7 @@ import { ConsentState } from './ConsentListEntry' import { ConversationSendPayload, MessagesOptions } from './types' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' -import { Conversation, DecodedMessage, Member, Dm, Group } from '../index' +import { DecodedMessage, Member, Dm, Group } from '../index' export enum ConversationVersion { GROUP = 'GROUP', @@ -36,4 +36,4 @@ export interface ConversationBase { export type Conversation< ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> = Group | Dm +> = Group | Dm \ No newline at end of file diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index c1794ed81..4bdfeee20 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -1,20 +1,25 @@ -import type { invitation, keystore } from '@xmtp/proto' - import { Client } from './Client' -import { - ConversationVersion, - ConversationContainer, -} from './Conversation' +import { ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Dm, DmParams } from './Dm' import { Group, GroupParams } from './Group' -import { Member } from './Member' +import { + ConversationOrder, + ConversationOptions, +} from './types/ConversationOptions' import { CreateGroupOptions } from './types/CreateGroupOptions' import { EventTypes } from './types/EventTypes' -import { ConversationOrder, GroupOptions } from './types/ConversationOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTPModule from '../index' -import { ContentCodec } from '../index' +import { + Address, + ContentCodec, + Conversation, + ConversationId, + ConversationTopic, + ConversationType, + MessageId, +} from '../index' import { getAddress } from '../utils/address' export default class Conversations< @@ -32,50 +37,60 @@ export default class Conversations< * * This method creates a new conversation with the specified peer address and context. * - * @param {string} peerAddress - The address of the peer to create a conversation with. + * @param {Address} peerAddress - The address of the peer to create a conversation with. * @returns {Promise} A Promise that resolves to a Conversation object. */ - async newConversation(peerAddress: string): Promise> { + async newConversation( + peerAddress: Address + ): Promise> { const checksumAddress = getAddress(peerAddress) - return await XMTPModule.createConversation( - this.client, - checksumAddress, - ) + return await XMTPModule.findOrCreateDm(this.client, checksumAddress) } /** - * Creates a new V3 conversation. + * Creates a new conversation. * * This method creates a new conversation with the specified peer address. * - * @param {string} peerAddress - The address of the peer to create a conversation with. + * @param {Address} peerAddress - The address of the peer to create a conversation with. * @returns {Promise} A Promise that resolves to a Dm object. */ - async findOrCreateDm(peerAddress: string): Promise> { + async findOrCreateDm(peerAddress: Address): Promise> { return await XMTPModule.findOrCreateDm(this.client, peerAddress) } /** * This method returns a list of all groups that the client is a member of. * To get the latest list of groups from the network, call syncGroups() first. - * @param {GroupOptions} opts - The options to specify what fields you want returned for the groups in the list. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the groups in the list. * @param {ConversationOrder} order - The order to specify if you want groups listed by last message or by created at. * @param {number} limit - Limit the number of groups returned in the list. * * @returns {Promise} A Promise that resolves to an array of Group objects. */ async listGroups( - opts?: GroupOptions | undefined, + opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, limit?: number | undefined ): Promise[]> { - const result = await XMTPModule.listGroups(this.client, opts, order, limit) - - for (const group of result) { - this.known[group.id] = true - } + return await XMTPModule.listGroups(this.client, opts, order, limit) + } - return result + /** + * This method returns a list of all groups that the client is a member of. + * To get the latest list of groups from the network, call syncGroups() first. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the groups in the list. + * @param {ConversationOrder} order - The order to specify if you want groups listed by last message or by created at. + * @param {number} limit - Limit the number of groups returned in the list. + * + * @returns {Promise} A Promise that resolves to an array of Group objects. + */ + async listDms( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined + ): Promise[]> { + return await XMTPModule.listDms(this.client, opts, order, limit) } /** @@ -84,7 +99,9 @@ export default class Conversations< * * @returns {Promise} A Promise that resolves to a Group or undefined if not found. */ - async findGroup(groupId: string): Promise | undefined> { + async findGroup( + groupId: ConversationId + ): Promise | undefined> { return await XMTPModule.findGroup(this.client, groupId) } @@ -94,7 +111,7 @@ export default class Conversations< * * @returns {Promise} A Promise that resolves to a Group or undefined if not found. */ - async findDm(address: string): Promise | undefined> { + async findDm(address: Address): Promise | undefined> { return await XMTPModule.findDm(this.client, address) } @@ -102,11 +119,11 @@ export default class Conversations< * This method returns a conversation by the topic if that conversation exists in the local database. * To get the latest list of groups from the network, call syncConversations() first. * - * @returns {Promise} A Promise that resolves to a Group or undefined if not found. + * @returns {Promise} A Promise that resolves to a Group or undefined if not found. */ async findConversationByTopic( - topic: string - ): Promise | undefined> { + topic: ConversationTopic + ): Promise | undefined> { return await XMTPModule.findConversationByTopic(this.client, topic) } @@ -114,11 +131,11 @@ export default class Conversations< * This method returns a conversation by the conversation id if that conversation exists in the local database. * To get the latest list of groups from the network, call syncConversations() first. * - * @returns {Promise} A Promise that resolves to a Group or undefined if not found. + * @returns {Promise} A Promise that resolves to a Group or undefined if not found. */ async findConversation( - conversationId: string - ): Promise | undefined> { + conversationId: ConversationId + ): Promise | undefined> { return await XMTPModule.findConversation(this.client, conversationId) } @@ -128,93 +145,48 @@ export default class Conversations< * * @returns {Promise} A Promise that resolves to a DecodedMessage or undefined if not found. */ - async findV3Message( - messageId: string + async findMessage( + messageId: MessageId ): Promise | undefined> { - return await XMTPModule.findV3Message(this.client, messageId) - } - - /** - * This method returns a list of all conversations and groups that the client is a member of. - * To include the latest groups from the network in the returned list, call syncGroups() first. - * - * @returns {Promise} A Promise that resolves to an array of ConversationContainer objects. - */ - async listAll(): Promise[]> { - const result = await XMTPModule.listAll(this.client) - - for (const conversationContainer of result) { - this.known[conversationContainer.topic] = true - } - - return result + return await XMTPModule.findMessage(this.client, messageId) } /** * This method returns a list of all V3 conversations that the client is a member of. * To include the latest groups from the network in the returned list, call syncGroups() first. * - * @returns {Promise} A Promise that resolves to an array of ConversationContainer objects. + * @returns {Promise} A Promise that resolves to an array of Conversation objects. */ - async listConversations( - opts?: GroupOptions | undefined, + async list( + opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listV3Conversations(this.client, opts, order, limit) + ): Promise[]> { + return await XMTPModule.listConversations(this.client, opts, order, limit) } /** - * This method streams groups that the client is a member of. - * - * @returns {Promise} A Promise that resolves to an array of Group objects. + * This method streams conversations that the client is a member of. + * @param {type} ConversationType - Whether to stream groups, dms, or both + * @returns {Promise} A Promise that resolves to an array of Conversation objects. */ - async streamGroups( - callback: (group: Group) => Promise - ): Promise<() => void> { - XMTPModule.subscribeToGroups(this.client.inboxId) - const groupsSubscription = XMTPModule.emitter.addListener( - EventTypes.Group, - async ({ inboxId, group }: { inboxId: string; group: GroupParams }) => { - if (this.client.inboxId !== inboxId) { - return - } - this.known[group.id] = true - await callback(new Group(this.client, group)) - } - ) - this.subscriptions[EventTypes.Group] = groupsSubscription - return () => { - groupsSubscription.remove() - XMTPModule.unsubscribeFromGroups(this.client.inboxId) - } - } - - /** - * This method streams V3 conversations that the client is a member of. - * - * @returns {Promise} A Promise that resolves to an array of ConversationContainer objects. - */ - async streamConversations( - callback: ( - conversation: ConversationContainer - ) => Promise + async stream( + callback: (conversation: Conversation) => Promise, + type: ConversationType = 'all' ): Promise<() => void> { - XMTPModule.subscribeToV3Conversations(this.client.inboxId) + XMTPModule.subscribeToConversations(this.client.inboxId, type) const subscription = XMTPModule.emitter.addListener( - EventTypes.ConversationV3, + EventTypes.Conversation, async ({ inboxId, conversation, }: { inboxId: string - conversation: ConversationContainer + conversation: Conversation }) => { if (inboxId !== this.client.inboxId) { return } - - this.known[conversation.topic] = true if (conversation.version === ConversationVersion.GROUP) { return await callback( new Group(this.client, conversation as unknown as GroupParams) @@ -228,7 +200,7 @@ export default class Conversations< ) return () => { subscription.remove() - XMTPModule.unsubscribeFromV3Conversations(this.client.inboxId) + XMTPModule.unsubscribeFromConversations(this.client.inboxId) } } @@ -237,12 +209,12 @@ export default class Conversations< * * This method creates a new group with the specified peer addresses and options. * - * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {Address[]} peerAddresses - The addresses of the peers to create a group with. * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ async newGroup( - peerAddresses: string[], + peerAddresses: Address[], opts?: CreateGroupOptions | undefined ): Promise> { return await XMTPModule.createGroup( @@ -261,13 +233,13 @@ export default class Conversations< * * This method creates a new group with the specified peer addresses and options. * - * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {Address[]} peerAddresses - The addresses of the peers to create a group with. * @param {PermissionPolicySet} permissionPolicySet - The permission policy set to use for the group. * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ async newGroupCustomPermissions( - peerAddresses: string[], + peerAddresses: Address[], permissionPolicySet: PermissionPolicySet, opts?: CreateGroupOptions | undefined ): Promise> { @@ -283,134 +255,35 @@ export default class Conversations< } /** - * Executes a network request to fetch the latest list of groups associated with the client + * Executes a network request to fetch the latest list of conversations associated with the client * and save them to the local state. */ - async syncGroups() { - await XMTPModule.syncConversations(this.client.inboxId) - } - async syncConversations() { await XMTPModule.syncConversations(this.client.inboxId) } /** - * Executes a network request to sync all active groups associated with the client + * Executes a network request to sync all active conversations associated with the client * - * @returns {Promise} A Promise that resolves to the number of groups synced. + * @returns {Promise} A Promise that resolves to the number of conversations synced. */ - async syncAllGroups(): Promise { - return await XMTPModule.syncAllConversations(this.client.inboxId) - } - async syncAllConversations(): Promise { return await XMTPModule.syncAllConversations(this.client.inboxId) } - /** - * Sets up a real-time stream to listen for new conversations being started. - * - * This method subscribes to conversations in real-time and listens for incoming conversation events. - * When a new conversation is detected, the provided callback function is invoked with the details of the conversation. - * @param {Function} callback - A callback function that will be invoked with the new Conversation when a conversation is started. - * @returns {Promise} A Promise that resolves when the stream is set up. - * @warning This stream will continue infinitely. To end the stream, you can call {@linkcode Conversations.cancelStream | cancelStream()}. - */ - async stream( - callback: (conversation: Conversation) => Promise - ) { - XMTPModule.subscribeToConversations(this.client.inboxId) - const subscription = XMTPModule.emitter.addListener( - EventTypes.Conversation, - async ({ - inboxId, - conversation, - }: { - inboxId: string - conversation: ConversationParams - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (this.known[conversation.topic]) { - return - } - - this.known[conversation.topic] = true - await callback(new Conversation(this.client, conversation)) - } - ) - this.subscriptions[EventTypes.Conversation] = subscription - } - - /** - * Sets up a real-time stream to listen for new conversations and groups being started. - * - * This method subscribes to conversations in real-time and listens for incoming conversation and group events. - * When a new conversation is detected, the provided callback function is invoked with the details of the conversation. - * @param {Function} callback - A callback function that will be invoked with the new Conversation when a conversation is started. - * @returns {Promise} A Promise that resolves when the stream is set up. - * @warning This stream will continue infinitely. To end the stream, you can call the function returned by this streamAll. - */ - async streamAll( - callback: ( - conversation: ConversationContainer - ) => Promise - ) { - XMTPModule.subscribeToAll(this.client.inboxId) - const subscription = XMTPModule.emitter.addListener( - EventTypes.ConversationContainer, - async ({ - inboxId, - conversationContainer, - }: { - inboxId: string - conversationContainer: ConversationContainer - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (this.known[conversationContainer.topic]) { - return - } - - this.known[conversationContainer.topic] = true - if (conversationContainer.version === ConversationVersion.GROUP) { - return await callback( - new Group( - this.client, - conversationContainer as unknown as GroupParams, - ) - ) - } else { - return await callback( - new Conversation( - this.client, - conversationContainer as ConversationParams - ) - ) - } - } - ) - return () => { - subscription.remove() - this.cancelStream() - } - } - /** * Listen for new messages in all conversations. * * This method subscribes to all conversations in real-time and listens for incoming and outgoing messages. - * @param {boolean} includeGroups - Whether or not to include group messages in the stream. + * @param {type} ConversationType - Whether to stream messages from groups, dms, or both * @param {Function} callback - A callback function that will be invoked when a message is sent or received. * @returns {Promise} A Promise that resolves when the stream is set up. */ async streamAllMessages( callback: (message: DecodedMessage) => Promise, - includeGroups: boolean = false + type: ConversationType = 'all' ): Promise { - XMTPModule.subscribeToAllMessages(this.client.inboxId, includeGroups) + XMTPModule.subscribeToAllMessages(this.client.inboxId, type) const subscription = XMTPModule.emitter.addListener( EventTypes.Message, async ({ @@ -423,102 +296,17 @@ export default class Conversations< if (inboxId !== this.client.inboxId) { return } - if (this.known[message.id]) { - return - } - - this.known[message.id] = true await callback(DecodedMessage.fromObject(message, this.client)) } ) this.subscriptions[EventTypes.Message] = subscription } - /** - * Listen for new messages in all groups. - * - * This method subscribes to all groups in real-time and listens for incoming and outgoing messages. - * @param {Function} callback - A callback function that will be invoked when a message is sent or received. - * @returns {Promise} A Promise that resolves when the stream is set up. - */ - async streamAllGroupMessages( - callback: (message: DecodedMessage) => Promise - ): Promise { - XMTPModule.subscribeToAllGroupMessages(this.client.inboxId) - const subscription = XMTPModule.emitter.addListener( - EventTypes.AllGroupMessage, - async ({ - inboxId, - message, - }: { - inboxId: string - message: DecodedMessage - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (this.known[message.id]) { - return - } - - this.known[message.id] = true - await callback(DecodedMessage.fromObject(message, this.client)) - } - ) - this.subscriptions[EventTypes.AllGroupMessage] = subscription - } - - /** - * Listen for new messages in all v3 conversations. - * - * This method subscribes to all groups in real-time and listens for incoming and outgoing messages. - * @param {Function} callback - A callback function that will be invoked when a message is sent or received. - * @returns {Promise} A Promise that resolves when the stream is set up. - */ - async streamAllConversationMessages( - callback: (message: DecodedMessage) => Promise - ): Promise { - XMTPModule.subscribeToAllConversationMessages(this.client.inboxId) - const subscription = XMTPModule.emitter.addListener( - EventTypes.AllConversationMessages, - async ({ - inboxId, - message, - }: { - inboxId: string - message: DecodedMessage - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (this.known[message.id]) { - return - } - - this.known[message.id] = true - await callback(DecodedMessage.fromObject(message, this.client)) - } - ) - this.subscriptions[EventTypes.AllConversationMessages] = subscription - } - - async fromWelcome(encryptedMessage: string): Promise> { - try { - return await XMTPModule.processWelcomeMessage( - this.client, - encryptedMessage - ) - } catch (e) { - console.info('ERROR in processWelcomeMessage()', e) - throw e - } - } - - async conversationFromWelcome( + async fromWelcome( encryptedMessage: string - ): Promise> { + ): Promise> { try { - return await XMTPModule.processConversationWelcomeMessage( + return await XMTPModule.processWelcomeMessage( this.client, encryptedMessage ) @@ -539,25 +327,6 @@ export default class Conversations< XMTPModule.unsubscribeFromConversations(this.client.inboxId) } - /** - * Cancels the stream for new conversations. - */ - cancelStreamGroups() { - if (this.subscriptions[EventTypes.Group]) { - this.subscriptions[EventTypes.Group].remove() - delete this.subscriptions[EventTypes.Group] - } - XMTPModule.unsubscribeFromGroups(this.client.inboxId) - } - - cancelStreamConversations() { - if (this.subscriptions[EventTypes.ConversationV3]) { - this.subscriptions[EventTypes.ConversationV3].remove() - delete this.subscriptions[EventTypes.ConversationV3] - } - XMTPModule.unsubscribeFromV3Conversations(this.client.inboxId) - } - /** * Cancels the stream for new messages in all conversations. */ @@ -568,23 +337,4 @@ export default class Conversations< } XMTPModule.unsubscribeFromAllMessages(this.client.inboxId) } - - /** - * Cancels the stream for new messages in all groups. - */ - cancelStreamAllGroupMessages() { - if (this.subscriptions[EventTypes.AllGroupMessage]) { - this.subscriptions[EventTypes.AllGroupMessage].remove() - delete this.subscriptions[EventTypes.AllGroupMessage] - } - XMTPModule.unsubscribeFromAllGroupMessages(this.client.inboxId) - } - - cancelStreamAllConversations() { - if (this.subscriptions[EventTypes.AllConversationMessages]) { - this.subscriptions[EventTypes.AllConversationMessages].remove() - delete this.subscriptions[EventTypes.AllConversationMessages] - } - XMTPModule.unsubscribeFromAllConversationMessages(this.client.inboxId) - } } diff --git a/src/lib/DecodedMessage.ts b/src/lib/DecodedMessage.ts index 4eea4ad29..c8644f782 100644 --- a/src/lib/DecodedMessage.ts +++ b/src/lib/DecodedMessage.ts @@ -28,7 +28,7 @@ export class DecodedMessage< topic: string contentTypeId: string senderAddress: string - sent: number // timestamp in milliseconds + sentNs: number // timestamp in nanoseconds nativeContent: NativeMessageContent fallback: string | undefined deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED @@ -44,7 +44,7 @@ export class DecodedMessage< decoded.topic, decoded.contentTypeId, decoded.senderAddress, - decoded.sent, + decoded.sentNs, decoded.content, decoded.fallback, decoded.deliveryStatus @@ -59,7 +59,7 @@ export class DecodedMessage< topic: string contentTypeId: string senderAddress: string - sent: number // timestamp in milliseconds + sentNs: number // timestamp in nanoseconds content: any fallback: string | undefined deliveryStatus: MessageDeliveryStatus | undefined @@ -72,7 +72,7 @@ export class DecodedMessage< object.topic, object.contentTypeId, object.senderAddress, - object.sent, + object.sentNs, object.content, object.fallback, object.deliveryStatus @@ -85,7 +85,7 @@ export class DecodedMessage< topic: string, contentTypeId: string, senderAddress: string, - sent: number, + sentNs: number, content: any, fallback: string | undefined, deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED @@ -95,7 +95,7 @@ export class DecodedMessage< this.topic = topic this.contentTypeId = contentTypeId this.senderAddress = senderAddress - this.sent = sent + this.sentNs = sentNs this.nativeContent = content // undefined comes back as null when bridged, ensure undefined so integrators don't have to add a new check for null as well this.fallback = fallback ?? undefined diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index dfd7f3cf2..de0f8c218 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -1,9 +1,6 @@ import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' -import { - ConversationVersion, - ConversationBase, -} from './Conversation' +import { ConversationVersion, ConversationBase } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' @@ -11,11 +8,12 @@ import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessagesOptions } from './types/MessagesOptions' import * as XMTP from '../index' +import { ConversationId, ConversationTopic } from '../index' export interface DmParams { - id: string + id: ConversationId createdAt: number - topic: string + topic: ConversationTopic consentState: ConsentState lastMessage?: DecodedMessage } @@ -24,7 +22,7 @@ export class Dm implements ConversationBase { client: XMTP.Client - id: string + id: ConversationId createdAt: number version = ConversationVersion.DM as const topic: string @@ -104,13 +102,9 @@ export class Dm content = { text: content } } - return await XMTP.prepareConversationMessage( - this.client.inboxId, - this.id, - content - ) + return await XMTP.prepareMessage(this.client.inboxId, this.id, content) } catch (e) { - console.info('ERROR in prepareGroupMessage()', e.message) + console.info('ERROR in prepareMessage()', e.message) throw e } } @@ -122,10 +116,7 @@ export class Dm */ async publishPreparedMessages() { try { - return await XMTP.publishPreparedGroupMessages( - this.client.inboxId, - this.id - ) + return await XMTP.publishPreparedMessages(this.client.inboxId, this.id) } catch (e) { console.info('ERROR in publishPreparedMessages()', e.message) throw e @@ -137,8 +128,8 @@ export class Dm * To get the latest messages from the network, call sync() first. * * @param {number | undefined} limit - Optional maximum number of messages to return. - * @param {number | Date | undefined} before - Optional filter for specifying the maximum timestamp of messages to return. - * @param {number | Date | undefined} after - Optional filter for specifying the minimum timestamp of messages to return. + * @param {number | undefined} before - Optional filter for specifying the maximum timestamp of messages to return. + * @param {number | undefined} after - Optional filter for specifying the minimum timestamp of messages to return. * @param direction - Optional parameter to specify the time ordering of the messages to return. * @returns {Promise[]>} A Promise that resolves to an array of DecodedMessage objects. */ @@ -149,8 +140,8 @@ export class Dm this.client, this.id, opts?.limit, - opts?.before, - opts?.after, + opts?.beforeNs, + opts?.afterNs, opts?.direction ) } @@ -176,10 +167,9 @@ export class Dm async streamMessages( callback: (message: DecodedMessage) => Promise ): Promise<() => void> { - await XMTP.subscribeToConversationMessages(this.client.inboxId, this.id) - const hasSeen = {} + await XMTP.subscribeToMessages(this.client.inboxId, this.id) const messageSubscription = XMTP.emitter.addListener( - EventTypes.ConversationV3Message, + EventTypes.ConversationMessage, async ({ inboxId, message, @@ -196,11 +186,6 @@ export class Dm if (conversationId !== this.id) { return } - if (hasSeen[message.id]) { - return - } - - hasSeen[message.id] = true message.client = this.client await callback(DecodedMessage.fromObject(message, this.client)) @@ -208,10 +193,7 @@ export class Dm ) return async () => { messageSubscription.remove() - await XMTP.unsubscribeFromConversationMessages( - this.client.inboxId, - this.id - ) + await XMTP.unsubscribeFromMessages(this.client.inboxId, this.id) } } @@ -219,11 +201,7 @@ export class Dm encryptedMessage: string ): Promise> { try { - return await XMTP.processConversationMessage( - this.client, - this.id, - encryptedMessage - ) + return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { console.info('ERROR in processConversationMessage()', e) throw e @@ -231,7 +209,7 @@ export class Dm } async consentState(): Promise { - return await XMTP.conversationV3ConsentState(this.client.inboxId, this.id) + return await XMTP.conversationConsentState(this.client.inboxId, this.id) } async updateConsent(state: ConsentState): Promise { diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 32c652613..419a44beb 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,9 +1,6 @@ import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' -import { - ConversationBase, - ConversationVersion, -} from './Conversation' +import { ConversationBase, ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' @@ -12,14 +9,14 @@ import { EventTypes } from './types/EventTypes' import { MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTP from '../index' +import { Address, ConversationId, ConversationTopic } from '../index' export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' export interface GroupParams { - id: string + id: ConversationId createdAt: number - members: string[] - topic: string + topic: ConversationTopic name: string isActive: boolean addedByInboxId: InboxId @@ -34,10 +31,10 @@ export class Group< > implements ConversationBase { client: XMTP.Client - id: string + id: ConversationId createdAt: number version = ConversationVersion.GROUP as const - topic: string + topic: ConversationTopic name: string isGroupActive: boolean addedByInboxId: InboxId @@ -132,11 +129,7 @@ export class Group< content = { text: content } } - return await XMTP.prepareConversationMessage( - this.client.inboxId, - this.id, - content - ) + return await XMTP.prepareMessage(this.client.inboxId, this.id, content) } catch (e) { console.info('ERROR in prepareGroupMessage()', e.message) throw e @@ -150,10 +143,7 @@ export class Group< */ async publishPreparedMessages() { try { - return await XMTP.publishPreparedGroupMessages( - this.client.inboxId, - this.id - ) + return await XMTP.publishPreparedMessages(this.client.inboxId, this.id) } catch (e) { console.info('ERROR in publishPreparedMessages()', e.message) throw e @@ -177,8 +167,8 @@ export class Group< this.client, this.id, opts?.limit, - opts?.before, - opts?.after, + opts?.beforeNs, + opts?.afterNs, opts?.direction ) } @@ -204,10 +194,9 @@ export class Group< async streamMessages( callback: (message: DecodedMessage) => Promise ): Promise<() => void> { - await XMTP.subscribeToGroupMessages(this.client.inboxId, this.id) - const hasSeen = {} + await XMTP.subscribeToMessages(this.client.inboxId, this.id) const messageSubscription = XMTP.emitter.addListener( - EventTypes.GroupMessage, + EventTypes.ConversationMessage, async ({ inboxId, message, @@ -224,11 +213,6 @@ export class Group< if (groupId !== this.id) { return } - if (hasSeen[message.id]) { - return - } - - hasSeen[message.id] = true message.client = this.client await callback(DecodedMessage.fromObject(message, this.client)) @@ -236,22 +220,16 @@ export class Group< ) return async () => { messageSubscription.remove() - await XMTP.unsubscribeFromGroupMessages(this.client.inboxId, this.id) + await XMTP.unsubscribeFromMessages(this.client.inboxId, this.id) } } - async streamGroupMessages( - callback: (message: DecodedMessage) => Promise - ): Promise<() => void> { - return this.streamMessages(callback) - } - /** * * @param addresses addresses to add to the group * @returns */ - async addMembers(addresses: string[]): Promise { + async addMembers(addresses: Address[]): Promise { return XMTP.addGroupMembers(this.client.inboxId, this.id, addresses) } @@ -260,7 +238,7 @@ export class Group< * @param addresses addresses to remove from the group * @returns */ - async removeMembers(addresses: string[]): Promise { + async removeMembers(addresses: Address[]): Promise { return XMTP.removeGroupMembers(this.client.inboxId, this.id, addresses) } @@ -606,11 +584,7 @@ export class Group< encryptedMessage: string ): Promise> { try { - return await XMTP.processConversationMessage( - this.client, - this.id, - encryptedMessage - ) + return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { console.info('ERROR in processGroupMessage()', e) throw e @@ -618,7 +592,7 @@ export class Group< } async consentState(): Promise { - return await XMTP.conversationV3ConsentState(this.client.inboxId, this.id) + return await XMTP.conversationConsentState(this.client.inboxId, this.id) } async updateConsent(state: ConsentState): Promise { @@ -629,20 +603,6 @@ export class Group< ) } - /** - * @returns {Promise} a boolean indicating whether the group is allowed by the user. - */ - async isAllowed(): Promise { - return await XMTP.isGroupAllowed(this.client.inboxId, this.id) - } - - /** - * @returns {Promise} a boolean indicating whether the group is denied by the user. - */ - async isDenied(): Promise { - return await XMTP.isGroupDenied(this.client.inboxId, this.id) - } - /** * * @returns {Promise} A Promise that resolves to an array of Member objects. diff --git a/src/lib/InboxState.ts b/src/lib/InboxState.ts index cef9593f4..bff41bbb1 100644 --- a/src/lib/InboxState.ts +++ b/src/lib/InboxState.ts @@ -1,5 +1,5 @@ -import { Address } from '../utils/address' import { InboxId } from './Client' +import { Address } from '../utils/address' export class InboxState { inboxId: InboxId diff --git a/src/lib/Member.ts b/src/lib/Member.ts index fcd9d5e25..7570eb4de 100644 --- a/src/lib/Member.ts +++ b/src/lib/Member.ts @@ -1,6 +1,6 @@ -import { Address } from '../utils/address' import { InboxId } from './Client' import { ConsentState } from './ConsentListEntry' +import { Address } from '../utils/address' export type PermissionLevel = 'member' | 'admin' | 'super_admin' diff --git a/src/lib/PrivatePreferences.ts b/src/lib/PrivatePreferences.ts new file mode 100644 index 000000000..e0b4f2aa9 --- /dev/null +++ b/src/lib/PrivatePreferences.ts @@ -0,0 +1,42 @@ +import { Client, InboxId } from './Client' +import { ConsentListEntry, ConsentState } from './ConsentListEntry' +import * as XMTPModule from '../index' +import { ConversationId } from '../index' +import { Address, getAddress } from '../utils/address' + +export default class PrivatePreferences { + client: Client + + constructor(client: Client) { + this.client = client + } + + async consentConversationIdState( + conversationId: ConversationId + ): Promise { + return await XMTPModule.consentConversationIdState( + this.client.inboxId, + conversationId + ) + } + + async consentInboxIdState(inboxId: InboxId): Promise { + return await XMTPModule.consentInboxIdState(this.client.inboxId, inboxId) + } + + async consentAddressState(address: Address): Promise { + return await XMTPModule.consentAddressState( + this.client.inboxId, + getAddress(address) + ) + } + + async setConsentState(consentEntry: ConsentListEntry): Promise { + return await XMTPModule.setConsentState( + this.client.inboxId, + consentEntry.value, + consentEntry.entryType, + consentEntry.permissionType + ) + } +} diff --git a/src/lib/types/MessagesOptions.ts b/src/lib/types/MessagesOptions.ts index a8688ebc3..fa3b38f27 100644 --- a/src/lib/types/MessagesOptions.ts +++ b/src/lib/types/MessagesOptions.ts @@ -1,7 +1,7 @@ export type MessagesOptions = { limit?: number | undefined - before?: number | Date | undefined - after?: number | Date | undefined + beforeNs?: number | undefined + afterNs?: number | undefined direction?: MessageOrder | undefined } diff --git a/src/utils/address.ts b/src/utils/address.ts index ec3045e82..ca15683e9 100644 --- a/src/utils/address.ts +++ b/src/utils/address.ts @@ -4,7 +4,7 @@ import { TextEncoder } from 'text-encoding' const addressRegex = /^0x[a-fA-F0-9]{40}$/ const encoder = new TextEncoder() -export type Address = string & { readonly brand: unique symbol } +export type Address = string export function stringToBytes(value: string): Uint8Array { const bytes = encoder.encode(value) From 12df7cf0090e6532c1c3a9ddaa36ddec20d6b72e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 10 Nov 2024 09:13:00 -0800 Subject: [PATCH 08/21] get all the tests compiling --- example/src/tests/conversationTests.ts | 40 +- example/src/tests/createdAtTests.ts | 29 +- example/src/tests/groupPerformanceTests.ts | 94 +- example/src/tests/groupPermissionsTests.ts | 28 +- example/src/tests/groupTests.ts | 479 ++---- example/src/tests/restartStreamsTests.ts | 68 +- example/src/tests/test-utils.ts | 46 - example/src/tests/tests.ts | 1535 -------------------- example/src/tests/v3OnlyTests.ts | 305 ---- src/index.ts | 7 +- src/lib/Client.ts | 4 +- src/lib/Conversation.ts | 6 +- src/lib/Dm.ts | 6 +- src/lib/Group.ts | 4 +- src/lib/InboxState.ts | 3 +- src/lib/Member.ts | 3 +- src/lib/PrivatePreferences.ts | 10 +- src/lib/types/index.ts | 2 + src/utils/address.ts | 2 - 19 files changed, 244 insertions(+), 2427 deletions(-) delete mode 100644 example/src/tests/tests.ts delete mode 100644 example/src/tests/v3OnlyTests.ts diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index ff1e0d4cd..09a206920 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,5 +1,5 @@ -import { Test, assert, createV3Clients, delayToPropogate } from './test-utils' -import { ConversationContainer, ConversationVersion } from '../../../src/index' +import { Test, assert, createClients, delayToPropogate } from './test-utils' +import { Conversation, ConversationVersion } from '../../../src/index' export const conversationTests: Test[] = [] let counter = 1 @@ -11,7 +11,7 @@ function test(name: string, perform: () => Promise) { } test('can find a conversations by id', async () => { - const [alixClient, boClient] = await createV3Clients(2) + const [alixClient, boClient] = await createClients(2) const alixGroup = await alixClient.conversations.newGroup([boClient.address]) const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) @@ -36,7 +36,7 @@ test('can find a conversations by id', async () => { }) test('can find a conversation by topic', async () => { - const [alixClient, boClient] = await createV3Clients(2) + const [alixClient, boClient] = await createClients(2) const alixGroup = await alixClient.conversations.newGroup([boClient.address]) const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) @@ -62,7 +62,7 @@ test('can find a conversation by topic', async () => { }) test('can find a dm by address', async () => { - const [alixClient, boClient] = await createV3Clients(2) + const [alixClient, boClient] = await createClients(2) const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) await boClient.conversations.syncConversations() @@ -77,7 +77,7 @@ test('can find a dm by address', async () => { }) test('can list conversations with params', async () => { - const [alixClient, boClient, caroClient] = await createV3Clients(3) + const [alixClient, boClient, caroClient] = await createClients(3) const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) @@ -93,13 +93,13 @@ test('can list conversations with params', async () => { // Order should be [Dm1, Group2, Dm2, Group1] await boClient.conversations.syncAllConversations() - const boConvosOrderCreated = await boClient.conversations.listConversations() + const boConvosOrderCreated = await boClient.conversations.list() const boConvosOrderLastMessage = - await boClient.conversations.listConversations( + await boClient.conversations.list( { lastMessage: true }, 'lastMessage' ) - const boGroupsLimit = await boClient.conversations.listConversations( + const boGroupsLimit = await boClient.conversations.list( {}, undefined, 1 @@ -139,7 +139,7 @@ test('can list conversations with params', async () => { }) test('can list groups', async () => { - const [alixClient, boClient, caroClient] = await createV3Clients(3) + const [alixClient, boClient, caroClient] = await createClients(3) const boGroup = await boClient.conversations.newGroup([alixClient.address]) await boClient.conversations.newGroup([ @@ -149,9 +149,9 @@ test('can list groups', async () => { const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) await boClient.conversations.findOrCreateDm(alixClient.address) - const boConversations = await boClient.conversations.listConversations() + const boConversations = await boClient.conversations.list() await alixClient.conversations.syncConversations() - const alixConversations = await alixClient.conversations.listConversations() + const alixConversations = await alixClient.conversations.list() assert( boConversations.length === 4, @@ -176,15 +176,15 @@ test('can list groups', async () => { }) test('can stream both conversations and messages at same time', async () => { - const [alix, bo] = await createV3Clients(2) + const [alix, bo] = await createClients(2) let conversationCallbacks = 0 let messageCallbacks = 0 - await bo.conversations.streamConversations(async () => { + await bo.conversations.stream(async () => { conversationCallbacks++ }) - await bo.conversations.streamAllConversationMessages(async () => { + await bo.conversations.streamAllMessages(async () => { messageCallbacks++ }) @@ -208,7 +208,7 @@ test('can stream both conversations and messages at same time', async () => { }) test('can stream conversation messages', async () => { - const [alixClient, boClient] = await createV3Clients(2) + const [alixClient, boClient] = await createClients(2) const alixGroup = await alixClient.conversations.newGroup([boClient.address]) const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) @@ -233,11 +233,11 @@ test('can stream conversation messages', async () => { }) test('can stream all groups and conversations', async () => { - const [alixClient, boClient, caroClient] = await createV3Clients(3) + const [alixClient, boClient, caroClient] = await createClients(3) - const containers: ConversationContainer[] = [] - const cancelStreamAll = await alixClient.conversations.streamConversations( - async (conversation: ConversationContainer) => { + const containers: Conversation[] = [] + const cancelStreamAll = await alixClient.conversations.stream( + async (conversation: Conversation) => { containers.push(conversation) } ) diff --git a/example/src/tests/createdAtTests.ts b/example/src/tests/createdAtTests.ts index 77773212e..cdcbed6b8 100644 --- a/example/src/tests/createdAtTests.ts +++ b/example/src/tests/createdAtTests.ts @@ -5,7 +5,7 @@ import { delayToPropogate, isIos, } from './test-utils' -import { Conversation, ConversationContainer, Group } from '../../../src/index' +import { Conversation, Group } from '../../../src/index' export const createdAtTests: Test[] = [] @@ -27,7 +27,7 @@ test('group createdAt matches listGroups', async () => { const boGroup = await bo.conversations.newGroup([alix.address]) // Fetch groups using listGroups method - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroups = await alix.conversations.listGroups() const first = 0 @@ -73,8 +73,8 @@ test('group createdAt matches listAll', async () => { const boGroup = await bo.conversations.newGroup([alix.address]) // Fetch groups using listGroups method - await alix.conversations.syncGroups() - const alixGroups = await alix.conversations.listAll() + await alix.conversations.syncConversations() + const alixGroups = await alix.conversations.list() assert(alixGroups.length === 2, 'alix should have two groups') @@ -123,10 +123,11 @@ test('group createdAt matches streamGroups', async () => { // Start streaming groups const allGroups: Group[] = [] - const cancelStream = await alix.conversations.streamGroups( - async (group: Group) => { - allGroups.push(group) - } + const cancelStream = await alix.conversations.stream( + async (group: Conversation) => { + allGroups.push(group as Group) + }, + 'groups' ) await delayToPropogate() @@ -173,9 +174,9 @@ test('group createdAt matches streamAll', async () => { const [alix, bo, caro] = await createClients(3) // Start streaming groups - const allGroups: ConversationContainer[] = [] - const cancelStream = await alix.conversations.streamAll( - async (group: ConversationContainer) => { + const allGroups: Conversation[] = [] + const cancelStream = await alix.conversations.stream( + async (group: Conversation) => { allGroups.push(group) } ) @@ -274,7 +275,7 @@ test('conversation createdAt matches listAll', async () => { ) // Fetch conversations using list() method - const alixConversations = await alix.conversations.listAll() + const alixConversations = await alix.conversations.list() assert(alixConversations.length === 2, 'alix should have two conversations') const first = 0 @@ -367,8 +368,8 @@ test('conversation createdAt matches streamAll', async () => { const [alix, bo, caro] = await createClients(3) // Start streaming conversations - const allConversations: ConversationContainer[] = [] - const cancel = await alix.conversations.streamAll(async (conversation) => { + const allConversations: Conversation[] = [] + const cancel = await alix.conversations.stream(async (conversation) => { allConversations.push(conversation) }) diff --git a/example/src/tests/groupPerformanceTests.ts b/example/src/tests/groupPerformanceTests.ts index a33f83b7d..cc70d39f4 100644 --- a/example/src/tests/groupPerformanceTests.ts +++ b/example/src/tests/groupPerformanceTests.ts @@ -1,8 +1,7 @@ /* eslint-disable @typescript-eslint/no-extra-non-null-assertion */ import { Client, Conversation, Dm, Group } from 'xmtp-react-native-sdk' -import { DefaultContentTypes } from 'xmtp-react-native-sdk/lib/types/DefaultContentType' -import { Test, assert, createClients, createV3Clients } from './test-utils' +import { Test, assert, createClients } from './test-utils' export const groupPerformanceTests: Test[] = [] let counter = 1 @@ -51,43 +50,21 @@ async function createDms( return dms } -async function createV2Convos( - client: Client, - peers: Client[], - numMessages: number -): Promise[]> { - const convos = [] - for (let i = 0; i < peers.length; i++) { - const convo = await peers[i].conversations.newConversation(client.address) - convos.push(convo) - for (let i = 0; i < numMessages; i++) { - await convo.send({ text: `Alix message ${i}` }) - } - } - return convos -} - let alixClient: Client let boClient: Client -let davonV3Client: Client let initialPeers: Client[] let initialGroups: Group[] -let initialV3Peers: Client[] // let initialDms: Dm[] -// let initialV2Convos: Conversation[] async function beforeAll( groupSize: number = 1, messages: number = 1, peersSize: number = 1, includeDms: boolean = false, - includeV2Convos: boolean = false ) { ;[alixClient] = await createClients(1) - ;[davonV3Client] = await createV3Clients(1) initialPeers = await createClients(peersSize) - initialV3Peers = await createV3Clients(peersSize) boClient = initialPeers[0] initialGroups = await createGroups( @@ -98,67 +75,34 @@ async function beforeAll( ) if (includeDms) { - await createDms(davonV3Client, initialV3Peers, messages) - } - - if (includeV2Convos) { - await createV2Convos(alixClient, initialPeers, messages) + await createDms(alixClient, initialPeers, messages) } } -test('test compare V2 and V3 dms', async () => { - await beforeAll(0, 0, 50, true, true) +test('test compare V3 dms', async () => { + await beforeAll(0, 0, 50, true) let start = Date.now() - let v2Convos = await alixClient.conversations.list() + await alixClient.conversations.syncConversations() let end = Date.now() - console.log(`Alix loaded ${v2Convos.length} v2Convos in ${end - start}ms`) - - start = Date.now() - v2Convos = await alixClient.conversations.list() - end = Date.now() - console.log(`Alix 2nd loaded ${v2Convos.length} v2Convos in ${end - start}ms`) - const v2Load = end - start + console.log(`Davon synced ${50} Dms in ${end - start}ms`) start = Date.now() - await davonV3Client.conversations.syncConversations() - end = Date.now() - console.log(`Davon synced ${v2Convos.length} Dms in ${end - start}ms`) - - start = Date.now() - let dms = await davonV3Client.conversations.listConversations() + let dms = await alixClient.conversations.list() end = Date.now() console.log(`Davon loaded ${dms.length} Dms in ${end - start}ms`) - const v3Load = end - start - - await createDms(davonV3Client, await createV3Clients(5), 1) - await createV2Convos(alixClient, await createClients(5), 1) - - start = Date.now() - v2Convos = await alixClient.conversations.list() - end = Date.now() - console.log(`Alix loaded ${v2Convos.length} v2Convos in ${end - start}ms`) + await createDms(alixClient, await createClients(5), 1) start = Date.now() - v2Convos = await alixClient.conversations.list() + await alixClient.conversations.syncConversations() end = Date.now() - console.log(`Alix 2nd loaded ${v2Convos.length} v2Convos in ${end - start}ms`) + console.log(`Davon synced ${dms.length} Dms in ${end - start}ms`) start = Date.now() - await davonV3Client.conversations.syncConversations() - end = Date.now() - console.log(`Davon synced ${v2Convos.length} Dms in ${end - start}ms`) - - start = Date.now() - dms = await davonV3Client.conversations.listConversations() + dms = await alixClient.conversations.list() end = Date.now() console.log(`Davon loaded ${dms.length} Dms in ${end - start}ms`) - assert( - v3Load < v2Load, - 'v3 conversations should load faster than v2 conversations' - ) - return true }) @@ -196,7 +140,7 @@ test('testing large group listings with ordering', async () => { ) start = Date.now() - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() end = Date.now() console.log(`Alix synced ${groups.length} groups in ${end - start}ms`) assert( @@ -205,12 +149,12 @@ test('testing large group listings with ordering', async () => { ) start = Date.now() - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() end = Date.now() console.log(`Bo synced ${groups.length} groups in ${end - start}ms`) start = Date.now() - await boClient.conversations.syncAllGroups() + await boClient.conversations.syncAllConversations() end = Date.now() console.log(`Bo synced all ${groups.length} groups in ${end - start}ms`) assert( @@ -257,7 +201,7 @@ test('testing large group listings', async () => { ) start = Date.now() - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() end = Date.now() console.log(`Alix synced ${groups.length} groups in ${end - start}ms`) assert( @@ -266,7 +210,7 @@ test('testing large group listings', async () => { ) start = Date.now() - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() end = Date.now() console.log(`Bo synced ${groups.length} groups in ${end - start}ms`) assert( @@ -308,7 +252,7 @@ test('testing large message listings', async () => { 'syncing 2000 self messages should take less than a .1 second' ) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = await boClient.conversations.findGroup(alixGroup.id) start = Date.now() await boGroup!.sync() @@ -353,7 +297,7 @@ test('testing large member listings', async () => { 'syncing 50 members should take less than a .1 second' ) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = await boClient.conversations.findGroup(alixGroup.id) start = Date.now() await boGroup!.sync() @@ -424,7 +368,7 @@ test('testing sending message in large group', async () => { 'sending a message should take less than a .2 second' ) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = await boClient.conversations.findGroup(alixGroup.id) start = Date.now() await boGroup!.prepareMessage({ text: `Bo message` }) diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index bbdc4e81f..7522f980f 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -55,7 +55,7 @@ test('super admin can add a new admin', async () => { assert(!boIsSuperAdmin, `bo should not be a super admin`) // Verify that bo can not add a new admin - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.addAdmin(caro.inboxId) @@ -67,7 +67,7 @@ test('super admin can add a new admin', async () => { // Alix adds bo as an admin await alixGroup.addAdmin(bo.inboxId) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroupIsAdmin = await alixGroup.isAdmin(bo.inboxId) assert(alixGroupIsAdmin, `alix should be an admin`) @@ -98,7 +98,7 @@ test('in admin only group, members can not update group name unless they are an ) // Verify that bo can not update the group name - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.updateGroupName("bo's group") @@ -135,7 +135,7 @@ test('in admin only group, members can update group name once they are an admin' ) // Verify that bo can not update the group name - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.updateGroupName("bo's group") @@ -147,7 +147,7 @@ test('in admin only group, members can update group name once they are an admin' // Alix adds bo as an admin await alixGroup.addAdmin(bo.inboxId) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroupIsAdmin = await alixGroup.isAdmin(bo.inboxId) assert(alixGroupIsAdmin, `alix should be an admin`) @@ -190,12 +190,12 @@ test('in admin only group, members can not update group name after admin status // Alix adds bo as an admin await alixGroup.addAdmin(bo.inboxId) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() let boIsAdmin = await alixGroup.isAdmin(bo.inboxId) assert(boIsAdmin, `bo should be an admin`) // Now bo can update the group name - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] await boGroup.sync() await boGroup.updateGroupName("bo's group") @@ -208,7 +208,7 @@ test('in admin only group, members can not update group name after admin status // Now alix removed bo as an admin await alixGroup.removeAdmin(bo.inboxId) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() boIsAdmin = await alixGroup.isAdmin(bo.inboxId) assert(!boIsAdmin, `bo should not be an admin`) @@ -251,7 +251,7 @@ test('can not remove a super admin from a group', async () => { `number of members should be 2 but was ${numMembers}` ) - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] await boGroup.sync() @@ -327,7 +327,7 @@ test('can commit after invalid permissions commit', async () => { [alix.address, caro.address], { permissionLevel: 'all_members' } ) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroup = (await alix.conversations.listGroups())[0] // Verify that Alix cannot add an admin @@ -373,7 +373,7 @@ test('group with All Members policy has remove function that is admin only', asy [alix.address, caro.address], { permissionLevel: 'all_members' } ) - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroup = (await alix.conversations.listGroups())[0] // Verify that Alix cannot remove a member @@ -429,7 +429,7 @@ test('can update group permissions', async () => { ) // Verify that alix can not update the group description - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroup = (await alix.conversations.listGroups())[0] try { await alixGroup.updateGroupDescription('new description 2') @@ -479,7 +479,7 @@ test('can update group pinned frame', async () => { ) // Verify that alix can not update the group pinned frame - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroup = (await alix.conversations.listGroups())[0] try { await alixGroup.updateGroupPinnedFrameUrl('new pinned frame') @@ -540,7 +540,7 @@ test('can create a group with custom permissions', async () => { ) // Verify that bo can read the correct permissions - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixGroup = (await alix.conversations.listGroups())[0] const permissions = await alixGroup.permissionPolicySet() assert( diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 4b40c2f70..6cb5d4d77 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1,5 +1,4 @@ import { Wallet } from 'ethers' -import { Platform } from 'expo-modules-core' import RNFS from 'react-native-fs' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' @@ -14,10 +13,10 @@ import { Client, Conversation, Group, - ConversationContainer, ConversationVersion, GroupUpdatedContent, GroupUpdatedCodec, + ConsentListEntry, } from '../../../src/index' export const groupTests: Test[] = [] @@ -35,13 +34,10 @@ test('can make a MLS V3 client', async () => { const client = await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: keyBytes, }) - const inboxId = await Client.getOrCreateInboxId(client.address, { - env: 'local', - }) + const inboxId = await Client.getOrCreateInboxId(client.address, 'local') assert( client.inboxId === inboxId, @@ -61,75 +57,40 @@ test('can revoke all other installations', async () => { const alix = await Client.create(alixWallet, { env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: keyBytes, }) await alix.deleteLocalDatabase() - // create a v2 client const alix2 = await Client.create(alixWallet, { env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, }) - const keyBundle = await alix2.exportKeyBundle() - - // create from keybundle a v3 client - const alixKeyBundle = await Client.createFromKeyBundle( - keyBundle, - { - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: keyBytes, - }, - alixWallet - ) - - const inboxState = await alixKeyBundle.inboxState(true) - assert( - inboxState.installations.length === 2, - `installations length should be 2 but was ${inboxState.installations.length}` - ) - - const alix3 = await Client.create(alixWallet, { + const alix2Build = await Client.build(alix2.address, { env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: keyBytes, }) - const keyBundle2 = await alix3.exportKeyBundle() - - const alixKeyBundle2 = await Client.createFromKeyBundle( - keyBundle2, - { - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: keyBytes, - }, - alixWallet - ) - - await alix3.deleteLocalDatabase() + await alix2.deleteLocalDatabase() - const alix4 = await Client.create(alixWallet, { + const alix3 = await Client.create(alixWallet, { env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: keyBytes, }) - const inboxState2 = await alix4.inboxState(true) + const inboxState2 = await alix3.inboxState(true) assert( inboxState2.installations.length === 3, `installations length should be 3 but was ${inboxState2.installations.length}` ) - await alix4.revokeAllOtherInstallations(alixWallet) + await alix3.revokeAllOtherInstallations(alixWallet) - const inboxState3 = await alix4.inboxState(true) + const inboxState3 = await alix3.inboxState(true) assert( inboxState3.installations.length === 1, `installations length should be 1 but was ${inboxState3.installations.length}` @@ -185,7 +146,7 @@ test('can delete a local database', async () => { let [client, anotherClient] = await createClients(2) await client.conversations.newGroup([anotherClient.address]) - await client.conversations.syncGroups() + await client.conversations.syncConversations() assert( (await client.conversations.listGroups()).length === 1, `should have a group size of 1 but was ${ @@ -201,14 +162,13 @@ test('can delete a local database', async () => { client = await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]), }) - await client.conversations.syncGroups() + await client.conversations.syncConversations() assert( (await client.conversations.listGroups()).length === 0, `should have a group size of 0 but was ${ @@ -234,7 +194,6 @@ test('can make a MLS V3 client with encryption key and database directory', asyn const client = await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: key, dbDirectory: dbDirPath, }) @@ -242,7 +201,6 @@ test('can make a MLS V3 client with encryption key and database directory', asyn const anotherClient = await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: key, }) @@ -254,11 +212,9 @@ test('can make a MLS V3 client with encryption key and database directory', asyn }` ) - const bundle = await client.exportKeyBundle() - const clientFromBundle = await Client.createFromKeyBundle(bundle, { + const clientFromBundle = await Client.build(client.address, { env: 'local', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: key, dbDirectory: dbDirPath, }) @@ -281,7 +237,7 @@ test('can drop a local database', async () => { const [client, anotherClient] = await createClients(2) const group = await client.conversations.newGroup([anotherClient.address]) - await client.conversations.syncGroups() + await client.conversations.syncConversations() assert( (await client.conversations.listGroups()).length === 1, `should have a group size of 1 but was ${ @@ -327,73 +283,6 @@ test('can get a inboxId from an address', async () => { return true }) -test('can make a MLS V3 client from bundle', async () => { - const key = new Uint8Array([ - 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, - 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, - ]) - - const client = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: key, - }) - - const anotherClient = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: key, - }) - - const group1 = await client.conversations.newGroup([anotherClient.address]) - - assert( - group1.client.address === client.address, - `clients dont match ${client.address} and ${group1.client.address}` - ) - - const bundle = await client.exportKeyBundle() - const client2 = await Client.createFromKeyBundle(bundle, { - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: key, - }) - - assert( - client.address === client2.address, - `clients dont match ${client2.address} and ${client.address}` - ) - - assert( - client.inboxId === client2.inboxId, - `clients dont match ${client2.inboxId} and ${client.inboxId}` - ) - - assert( - client.installationId === client2.installationId, - `clients dont match ${client2.installationId} and ${client.installationId}` - ) - - const randomClient = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: key, - }) - - const group = await client2.conversations.newGroup([randomClient.address]) - - assert( - group.client.address === client2.address, - `clients dont match ${client2.address} and ${group.client.address}` - ) - - return true -}) - test('production MLS V3 client creation does not error', async () => { const key = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, @@ -404,7 +293,6 @@ test('production MLS V3 client creation does not error', async () => { await Client.createRandom({ env: 'production', appVersion: 'Testing/0.0.0', - enableV3: true, dbEncryptionKey: key, }) // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -420,7 +308,7 @@ test('can cancel streams', async () => { await bo.conversations.streamAllMessages(async () => { messageCallbacks++ - }, true) + }) const group = await alix.conversations.newGroup([bo.address]) await group.send('hello') @@ -447,7 +335,7 @@ test('can cancel streams', async () => { await bo.conversations.streamAllMessages(async () => { messageCallbacks++ - }, true) + }) await delayToPropogate() @@ -488,7 +376,7 @@ test('group message delivery status', async () => { `the message should have a delivery status of PUBLISHED but was ${alixMessages2[0].deliveryStatus}` ) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = (await boClient.conversations.listGroups())[0] await boGroup.sync() const boMessages: DecodedMessage[] = await boGroup.messages() @@ -510,7 +398,7 @@ test('can find a group by id', async () => { const [alixClient, boClient] = await createClients(2) const alixGroup = await alixClient.conversations.newGroup([boClient.address]) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = await boClient.conversations.findGroup(alixGroup.id) assert( @@ -525,10 +413,10 @@ test('can find a message by id', async () => { const alixGroup = await alixClient.conversations.newGroup([boClient.address]) const alixMessageId = await alixGroup.send('Hello') - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = await boClient.conversations.findGroup(alixGroup.id) await boGroup?.sync() - const boMessage = await boClient.conversations.findV3Message(alixMessageId) + const boMessage = await boClient.conversations.findMessage(alixMessageId) assert( boMessage?.id === alixMessageId, @@ -541,7 +429,7 @@ test('who added me to a group', async () => { const [alixClient, boClient] = await createClients(2) await alixClient.conversations.newGroup([boClient.address]) - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = (await boClient.conversations.listGroups())[0] const addedByInboxId = await boGroup.addedByInboxId @@ -603,7 +491,7 @@ test('can message in a group', async () => { ]) // alix's num groups == 1 - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() alixGroups = await alixClient.conversations.listGroups() if (alixGroups.length !== 1) { throw new Error('num groups should be 1') @@ -634,7 +522,7 @@ test('can message in a group', async () => { await alixGroup.send('gm') // bo's num groups == 1 - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroups = await boClient.conversations.listGroups() if (boGroups.length !== 1) { throw new Error( @@ -661,7 +549,7 @@ test('can message in a group', async () => { await boGroups[0].send('hey guys!') // caro's num groups == 1 - await caroClient.conversations.syncGroups() + await caroClient.conversations.syncConversations() const caroGroups = await caroClient.conversations.listGroups() if (caroGroups.length !== 1) { throw new Error( @@ -698,15 +586,17 @@ test('unpublished messages handling', async () => { const boGroup = await boClient.conversations.newGroup([alixClient.address]) // Sync Alice's client to get the new group - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() const alixGroup = await alixClient.conversations.findGroup(boGroup.id) if (!alixGroup) { throw new Error(`Group not found for id: ${boGroup.id}`) } // Check if the group is allowed initially - let isGroupAllowed = await alixClient.contacts.isGroupAllowed(boGroup.id) - if (isGroupAllowed) { + let isGroupAllowed = await alixClient.preferences.conversationIdConsentState( + boGroup.id + ) + if (isGroupAllowed !== 'allowed') { throw new Error('Group should not be allowed initially') } @@ -714,8 +604,10 @@ test('unpublished messages handling', async () => { const preparedMessageId = await alixGroup.prepareMessage('Test text') // Check if the group is allowed after preparing the message - isGroupAllowed = await alixClient.contacts.isGroupAllowed(boGroup.id) - if (!isGroupAllowed) { + isGroupAllowed = await alixClient.preferences.conversationIdConsentState( + boGroup.id + ) + if (isGroupAllowed === 'allowed') { throw new Error('Group should be allowed after preparing a message') } @@ -770,7 +662,7 @@ test('can add members to a group', async () => { const alixGroup = await alixClient.conversations.newGroup([boClient.address]) // alix's num groups == 1 - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() alixGroups = await alixClient.conversations.listGroups() if (alixGroups.length !== 1) { throw new Error('num groups should be 1') @@ -796,7 +688,7 @@ test('can add members to a group', async () => { await alixGroup.send('gm') // bo's num groups == 1 - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() boGroups = await boClient.conversations.listGroups() if (boGroups.length !== 1) { throw new Error( @@ -807,7 +699,7 @@ test('can add members to a group', async () => { await alixGroup.addMembers([caroClient.address]) // caro's num groups == 1 - await caroClient.conversations.syncGroups() + await caroClient.conversations.syncConversations() caroGroups = await caroClient.conversations.listGroups() if (caroGroups.length !== 1) { throw new Error( @@ -856,7 +748,7 @@ test('can remove members from a group', async () => { ]) // alix's num groups == 1 - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() alixGroups = await alixClient.conversations.listGroups() if (alixGroups.length !== 1) { throw new Error('num groups should be 1') @@ -882,7 +774,7 @@ test('can remove members from a group', async () => { await alixGroup.send('gm') // bo's num groups == 1 - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() boGroups = await boClient.conversations.listGroups() if (boGroups.length !== 1) { throw new Error( @@ -891,7 +783,7 @@ test('can remove members from a group', async () => { } // caro's num groups == 1 - await caroClient.conversations.syncGroups() + await caroClient.conversations.syncConversations() caroGroups = await caroClient.conversations.listGroups() if (caroGroups.length !== 1) { throw new Error( @@ -967,13 +859,13 @@ test('can stream both groups and messages at same time', async () => { let groupCallbacks = 0 let messageCallbacks = 0 - await bo.conversations.streamGroups(async () => { + await bo.conversations.stream(async () => { groupCallbacks++ }) await bo.conversations.streamAllMessages(async () => { messageCallbacks++ - }, true) + }) const group = await alix.conversations.newGroup([bo.address]) await group.send('hello') @@ -992,9 +884,9 @@ test('can stream groups', async () => { const [alixClient, boClient, caroClient] = await createClients(3) // Start streaming groups - const groups: Group[] = [] - const cancelStreamGroups = await alixClient.conversations.streamGroups( - async (group: Group) => { + const groups: Conversation[] = [] + const cancelstream = await alixClient.conversations.stream( + async (group: Conversation) => { groups.push(group) } ) @@ -1020,7 +912,7 @@ test('can stream groups', async () => { } // * Note alix creating a group does not trigger alix conversations - // group stream. Workaround is to syncGroups after you create and list manually + // group stream. Workaround is to syncConversations after you create and list manually // See https://github.com/xmtp/libxmtp/issues/504 // alix creates a group @@ -1041,7 +933,7 @@ test('can stream groups', async () => { throw Error('Expected group length 4 but it is: ' + groups.length) } - cancelStreamGroups() + cancelstream() await delayToPropogate() // Creating a group should no longer trigger stream groups @@ -1125,7 +1017,7 @@ test('can list groups', async () => { }) const boGroups = await boClient.conversations.listGroups() - await alixClient.conversations.syncGroups() + await alixClient.conversations.syncConversations() const alixGroups = await alixClient.conversations.listGroups() assert( @@ -1173,7 +1065,7 @@ test('can list all groups and conversations', async () => { caroClient.address ) - const listedContainers = await alixClient.conversations.listAll() + const listedContainers = await alixClient.conversations.list() // Verify information in listed containers is correct // BUG - List All returns in Chronological order on iOS @@ -1183,7 +1075,6 @@ test('can list all groups and conversations', async () => { if ( listedContainers[first].topic !== boGroup.topic || listedContainers[first].version !== ConversationVersion.GROUP || - listedContainers[second].version !== ConversationVersion.DIRECT || listedContainers[second].createdAt !== alixConversation.createdAt ) { throw Error('Listed containers should match streamed containers') @@ -1196,10 +1087,10 @@ test('can stream all groups and conversations', async () => { const [alixClient, boClient, caroClient] = await createClients(3) // Start streaming groups and conversations - const containers: ConversationContainer[] = [] - const cancelStreamAll = await alixClient.conversations.streamAll( - async (conversationContainer: ConversationContainer) => { - containers.push(conversationContainer) + const containers: Conversation[] = [] + const cancelStream = await alixClient.conversations.stream( + async (Conversation: Conversation) => { + containers.push(Conversation) } ) @@ -1221,16 +1112,6 @@ test('can stream all groups and conversations', async () => { throw Error('Unexpected num groups (should be 2): ' + containers.length) } - if ( - containers[1].version === ConversationVersion.DIRECT && - boConversation.conversationID !== - (containers[1] as Conversation).conversationID - ) { - throw Error( - 'Conversation from streamed all should match conversationID with created conversation' - ) - } - // * Note alix creating a v2 Conversation does trigger alix conversations // stream. @@ -1244,7 +1125,7 @@ test('can stream all groups and conversations', async () => { throw Error('Expected group length 3 but it is: ' + containers.length) } - cancelStreamAll() + cancelStream() await delayToPropogate() // Creating a group should no longer trigger stream groups @@ -1264,13 +1145,13 @@ test('can stream groups and messages', async () => { const [alixClient, boClient] = await createClients(2) // Start streaming groups - const groups: Group[] = [] - await alixClient.conversations.streamGroups(async (group: Group) => { + const groups: Conversation[] = [] + await alixClient.conversations.stream(async (group: Conversation) => { groups.push(group) }) // Stream messages twice - await alixClient.conversations.streamAllMessages(async (message) => {}, true) - await alixClient.conversations.streamAllMessages(async (message) => {}, true) + await alixClient.conversations.streamAllMessages(async (message) => {}) + await alixClient.conversations.streamAllMessages(async (message) => {}) // bo creates a group with alix so a stream callback is fired // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1286,12 +1167,7 @@ test('can stream groups and messages', async () => { test('canMessage', async () => { const [bo, alix, caro] = await createClients(3) - const canMessage = await bo.canMessage(alix.address) - if (!canMessage) { - throw new Error('should be able to message v2 client') - } - - const canMessageV3 = await caro.canGroupMessage([ + const canMessageV3 = await caro.canMessage([ caro.address, alix.address, '0x0000000000000000000000000000000000000000', @@ -1334,7 +1210,7 @@ test('can stream group messages', async () => { ) // bo's num groups == 1 - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroup = (await boClient.conversations.listGroups())[0] for (let i = 0; i < 5; i++) { @@ -1407,7 +1283,7 @@ test('can stream all messages', async () => { await alix.conversations.streamAllMessages(async (message) => { allMessages.push(message) - }, true) + }) for (let i = 0; i < 5; i++) { await boConvo.send({ text: `Message ${i}` }) @@ -1453,7 +1329,7 @@ test('can make a group with metadata', async () => { await alixGroup.updateGroupImageUrlSquare('newurl.com') await alixGroup.updateGroupDescription('a new group description') await alixGroup.sync() - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroups = await bo.conversations.listGroups() const boGroup = boGroups[0] await boGroup.sync() @@ -1549,7 +1425,7 @@ test('can paginate group messages', async () => { await alixGroup.send('hello, world') await alixGroup.send('gm') - await boClient.conversations.syncGroups() + await boClient.conversations.syncConversations() const boGroups = await boClient.conversations.listGroups() if (boGroups.length !== 1) { throw new Error( @@ -1584,10 +1460,10 @@ test('can stream all group messages', async () => { // Record message stream across all conversations const allMessages: DecodedMessage[] = [] - // If we don't call syncGroups here, the streamAllGroupMessages will not + // If we don't call syncConversations here, the streamAllGroupMessages will not // stream the first message. Feels like a bug. - await alix.conversations.syncGroups() - await alix.conversations.streamAllGroupMessages(async (message) => { + await alix.conversations.syncConversations() + await alix.conversations.streamAllMessages(async (message) => { allMessages.push(message) }) @@ -1611,9 +1487,9 @@ test('can stream all group messages', async () => { throw Error('Unexpected all messages count second' + allMessages.length) } - alix.conversations.cancelStreamAllGroupMessages() + alix.conversations.cancelStreamAllMessages() await delayToPropogate() - await alix.conversations.streamAllGroupMessages(async (message) => { + await alix.conversations.streamAllMessages(async (message) => { allMessages.push(message) }) @@ -1635,10 +1511,10 @@ test('can streamAll from multiple clients', async () => { const allBoConversations: any[] = [] const allAliConversations: any[] = [] - await bo.conversations.streamAll(async (conversation) => { + await bo.conversations.stream(async (conversation) => { allBoConversations.push(conversation) }) - await alix.conversations.streamAll(async (conversation) => { + await alix.conversations.stream(async (conversation) => { allAliConversations.push(conversation) }) @@ -1668,11 +1544,11 @@ test('can streamAll from multiple clients - swapped orderring', async () => { const allBoConversations: any[] = [] const allAliConversations: any[] = [] - await alix.conversations.streamAll(async (conversation) => { + await alix.conversations.stream(async (conversation) => { allAliConversations.push(conversation) }) - await bo.conversations.streamAll(async (conversation) => { + await bo.conversations.stream(async (conversation) => { allBoConversations.push(conversation) }) @@ -1704,10 +1580,10 @@ test('can streamAllMessages from multiple clients', async () => { await bo.conversations.streamAllMessages(async (conversation) => { allBoMessages.push(conversation) - }, true) + }) await alix.conversations.streamAllMessages(async (conversation) => { allAliMessages.push(conversation) - }, true) + }) // Start Caro starts a new conversation. const caroConversation = await caro.conversations.newConversation( @@ -1738,10 +1614,10 @@ test('can streamAllMessages from multiple clients - swapped', async () => { await alix.conversations.streamAllMessages(async (conversation) => { allAliMessages.push(conversation) - }, true) + }) await bo.conversations.streamAllMessages(async (conversation) => { allBoMessages.push(conversation) - }, true) + }) // Start Caro starts a new conversation. const caroConvo = await caro.conversations.newConversation(alix.address) @@ -1773,10 +1649,10 @@ test('can stream all group Messages from multiple clients', async () => { const alixGroup = await caro.conversations.newGroup([alix.address]) const boGroup = await caro.conversations.newGroup([bo.address]) - await alixGroup.streamGroupMessages(async (message) => { + await alixGroup.streamMessages(async (message) => { allAlixMessages.push(message) }) - await boGroup.streamGroupMessages(async (message) => { + await boGroup.streamMessages(async (message) => { allBoMessages.push(message) }) @@ -1794,7 +1670,7 @@ test('can stream all group Messages from multiple clients', async () => { ) } - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixConv = (await alix.conversations.listGroups())[0] await alixConv.send({ text: `Message` }) await delayToPropogate() @@ -1841,7 +1717,7 @@ test('can stream all group Messages from multiple clients - swapped', async () = ) } - await alix.conversations.syncGroups() + await alix.conversations.syncConversations() const alixConv = (await alix.conversations.listGroups())[0] await alixConv.send({ text: `Message` }) await delayToPropogate() @@ -1862,8 +1738,8 @@ test('creating a group should allow group', async () => { const [alix, bo] = await createClients(2) const group = await alix.conversations.newGroup([bo.address]) - const consent = await alix.contacts.isGroupAllowed(group.id) - const groupConsent = await group.isAllowed() + const consent = await alix.preferences.conversationIdConsentState(group.id) + const groupConsent = await group.consentState() if (!consent || !groupConsent) { throw Error('Group should be allowed') @@ -1875,42 +1751,44 @@ test('creating a group should allow group', async () => { `the message should have a consent state of allowed but was ${state}` ) - const consentList = await alix.contacts.consentList() - assert( - consentList[0].permissionType === 'allowed', - `the message should have a consent state of allowed but was ${consentList[0].permissionType}` - ) - return true }) test('can group consent', async () => { const [alix, bo] = await createClients(2) const group = await bo.conversations.newGroup([alix.address]) - let isAllowed = await alix.contacts.isGroupAllowed(group.id) + let isAllowed = await alix.preferences.conversationIdConsentState(group.id) assert( - isAllowed === false, + isAllowed !== 'allowed', `alix group should NOT be allowed but was ${isAllowed}` ) - isAllowed = await bo.contacts.isGroupAllowed(group.id) - assert(isAllowed === true, `bo group should be allowed but was ${isAllowed}`) + isAllowed = await bo.preferences.conversationIdConsentState(group.id) + assert( + isAllowed === 'allowed', + `bo group should be allowed but was ${isAllowed}` + ) assert( (await group.state) === 'allowed', `the group should have a consent state of allowed but was ${await group.state}` ) - await bo.contacts.denyGroups([group.id]) - const isDenied = await bo.contacts.isGroupDenied(group.id) - assert(isDenied === true, `bo group should be denied but was ${isDenied}`) + await bo.preferences.setConsentState( + new ConsentListEntry(group.id, 'group_id', 'denied') + ) + const isDenied = await bo.preferences.conversationIdConsentState(group.id) + assert(isDenied === 'denied', `bo group should be denied but was ${isDenied}`) assert( (await group.consentState()) === 'denied', `the group should have a consent state of denied but was ${await group.consentState()}` ) await group.updateConsent('allowed') - isAllowed = await bo.contacts.isGroupAllowed(group.id) - assert(isAllowed === true, `bo group should be allowed2 but was ${isAllowed}`) + isAllowed = await bo.preferences.conversationIdConsentState(group.id) + assert( + isAllowed === 'allowed', + `bo group should be allowed2 but was ${isAllowed}` + ) assert( (await group.consentState()) === 'allowed', `the group should have a consent state2 of allowed but was ${await group.consentState()}` @@ -1923,18 +1801,15 @@ test('can allow and deny a inbox id', async () => { const [alix, bo] = await createClients(2) const boGroup = await bo.conversations.newGroup([alix.address]) - let isInboxAllowed = await bo.contacts.isInboxAllowed(alix.inboxId) - let isInboxDenied = await bo.contacts.isInboxDenied(alix.inboxId) - assert( - isInboxAllowed === false, - `isInboxAllowed should be false but was ${isInboxAllowed}` - ) + let isInboxAllowed = await bo.preferences.inboxIdConsentState(alix.inboxId) assert( - isInboxDenied === false, - `isInboxDenied should be false but was ${isInboxDenied}` + isInboxAllowed === 'unknown', + `isInboxAllowed should be unknown but was ${isInboxAllowed}` ) - await bo.contacts.allowInboxes([alix.inboxId]) + await bo.preferences.setConsentState( + new ConsentListEntry(alix.inboxId, 'inbox_id', 'allowed') + ) let alixMember = (await boGroup.members()).find( (member) => member.inboxId === alix.inboxId @@ -1944,29 +1819,21 @@ test('can allow and deny a inbox id', async () => { `alixMember should be allowed but was ${alixMember?.consentState}` ) - isInboxAllowed = await bo.contacts.isInboxAllowed(alix.inboxId) - isInboxDenied = await bo.contacts.isInboxDenied(alix.inboxId) + isInboxAllowed = await bo.preferences.inboxIdConsentState(alix.inboxId) assert( - isInboxAllowed === true, + isInboxAllowed === 'allowed', `isInboxAllowed2 should be true but was ${isInboxAllowed}` ) - assert( - isInboxDenied === false, - `isInboxDenied2 should be false but was ${isInboxDenied}` - ) - let isAddressAllowed = await bo.contacts.isAllowed(alix.address) - let isAddressDenied = await bo.contacts.isDenied(alix.address) + let isAddressAllowed = await bo.preferences.addressConsentState(alix.address) assert( - isAddressAllowed === true, + isAddressAllowed === 'allowed', `isAddressAllowed should be true but was ${isAddressAllowed}` ) - assert( - isAddressDenied === false, - `isAddressDenied should be false but was ${isAddressDenied}` - ) - await bo.contacts.denyInboxes([alix.inboxId]) + await bo.preferences.setConsentState( + new ConsentListEntry(alix.inboxId, 'inbox_id', 'denied') + ) alixMember = (await boGroup.members()).find( (member) => member.inboxId === alix.inboxId @@ -1976,74 +1843,30 @@ test('can allow and deny a inbox id', async () => { `alixMember should be denied but was ${alixMember?.consentState}` ) - isInboxAllowed = await bo.contacts.isInboxAllowed(alix.inboxId) - isInboxDenied = await bo.contacts.isInboxDenied(alix.inboxId) + isInboxAllowed = await bo.preferences.inboxIdConsentState(alix.inboxId) assert( - isInboxAllowed === false, + isInboxAllowed === 'denied', `isInboxAllowed3 should be false but was ${isInboxAllowed}` ) - assert( - isInboxDenied === true, - `isInboxDenied3 should be true but was ${isInboxDenied}` - ) - await bo.contacts.allow([alix.address]) + await bo.preferences.setConsentState( + new ConsentListEntry(alix.address, 'address', 'allowed') + ) - isAddressAllowed = await bo.contacts.isAllowed(alix.address) - isAddressDenied = await bo.contacts.isDenied(alix.address) + isAddressAllowed = await bo.preferences.addressConsentState(alix.address) assert( - isAddressAllowed === true, + isAddressAllowed === 'allowed', `isAddressAllowed2 should be true but was ${isAddressAllowed}` ) + isInboxAllowed = await bo.preferences.inboxIdConsentState(alix.inboxId) assert( - isAddressDenied === false, - `isAddressDenied2 should be false but was ${isAddressDenied}` - ) - isInboxAllowed = await bo.contacts.isInboxAllowed(alix.inboxId) - isInboxDenied = await bo.contacts.isInboxDenied(alix.inboxId) - assert( - isInboxAllowed === true, + isInboxAllowed === 'allowed', `isInboxAllowed4 should be false but was ${isInboxAllowed}` ) - assert( - isInboxDenied === false, - `isInboxDenied4 should be true but was ${isInboxDenied}` - ) return true }) -test('can check if group is allowed', async () => { - const [alix, bo] = await createClients(2) - const alixGroup = await alix.conversations.newGroup([bo.address]) - const startConsent = await bo.contacts.isGroupAllowed(alixGroup.id) - if (startConsent) { - throw Error('Group should not be allowed by default') - } - await bo.contacts.allowGroups([alixGroup.id]) - const consent = await bo.contacts.isGroupAllowed(alixGroup.id) - if (!consent) { - throw Error('Group should be allowed') - } - - return true -}) - -test('can check if group is denied', async () => { - const [alix, bo] = await createClients(2) - const alixGroup = await alix.conversations.newGroup([bo.address]) - const startConsent = await bo.contacts.isGroupDenied(alixGroup.id) - if (startConsent) { - throw Error('Group should not be denied by default') - } - await bo.contacts.denyGroups([alixGroup.id]) - const consent = await bo.contacts.isGroupDenied(alixGroup.id) - if (!consent) { - throw Error('Group should be denied') - } - return true -}) - test('sync function behaves as expected', async () => { const [alix, bo, caro] = await createClients(3) const alixGroup = await alix.conversations.newGroup([bo.address]) @@ -2054,7 +1877,7 @@ test('sync function behaves as expected', async () => { let boGroups = await bo.conversations.listGroups() assert(boGroups.length === 0, 'num groups for bo is 0 until we sync') - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() boGroups = await bo.conversations.listGroups() assert(boGroups.length === 1, 'num groups for bo is 1') @@ -2067,7 +1890,7 @@ test('sync function behaves as expected', async () => { let numMessages = (await boGroups[0].messages()).length assert(numMessages === 0, 'num members should be 1') - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() // Num messages is still 0 because we didnt sync the group itself numMessages = (await boGroups[0].messages()).length @@ -2084,7 +1907,7 @@ test('sync function behaves as expected', async () => { numMembers = (await boGroups[0].memberInboxIds()).length assert(numMembers === 2, 'num members should be 2') - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() // Even though we synced the groups, we need to sync the group itself to see the new member numMembers = (await boGroups[0].memberInboxIds()).length @@ -2100,11 +1923,11 @@ test('sync function behaves as expected', async () => { bo.address, caro.address, ]) - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() boGroups = await bo.conversations.listGroups() assert(boGroups.length === 2, 'num groups for bo is 2') - // Even before syncing the group, syncGroups will return the initial number of members + // Even before syncing the group, syncConversations will return the initial number of members numMembers = (await boGroups[1].memberInboxIds()).length assert(numMembers === 3, 'num members should be 3') @@ -2130,7 +1953,7 @@ test('can read and update group name', async () => { 'group name should be "Test name update 1"' ) - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = (await bo.conversations.listGroups())[0] groupName = await boGroup.groupName() @@ -2146,7 +1969,7 @@ test('can read and update group name', async () => { ) await alixGroup.addMembers([caro.address]) - await caro.conversations.syncGroups() + await caro.conversations.syncConversations() const caroGroup = (await caro.conversations.listGroups())[0] await caroGroup.sync() @@ -2163,14 +1986,14 @@ test('can list groups does not fork', async () => { console.log('created clients') let groupCallbacks = 0 //#region Stream groups - await bo.conversations.streamGroups(async () => { + await bo.conversations.stream(async () => { console.log('group received') groupCallbacks++ }) //#region Stream All Messages await bo.conversations.streamAllMessages(async () => { console.log('message received') - }, true) + }) //#endregion // #region create group const alixGroup = await alix.conversations.newGroup([bo.address]) @@ -2179,7 +2002,7 @@ test('can list groups does not fork', async () => { console.log('sent group message') // #endregion // #region sync groups - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() // #endregion const boGroups = await bo.conversations.listGroups() assert(boGroups.length === 1, 'bo should have 1 group') @@ -2257,8 +2080,8 @@ test('can create new installation without breaking group', async () => { const group = await client1.conversations.newGroup([wallet2.address]) - await client1.conversations.syncGroups() - await client2.conversations.syncGroups() + await client1.conversations.syncConversations() + await client2.conversations.syncConversations() const client1Group = await client1.conversations.findGroup(group.id) const client2Group = await client2.conversations.findGroup(group.id) @@ -2322,7 +2145,7 @@ test('can sync all groups', async () => { const groups: Group[] = await createGroups(alix, [bo], 50) const alixGroup = groups[0] - await bo.conversations.syncGroups() + await bo.conversations.syncConversations() const boGroup = await bo.conversations.findGroup(alixGroup.id) await alixGroup.send('hi') assert( @@ -2330,7 +2153,7 @@ test('can sync all groups', async () => { `messages should be empty before sync but was ${boGroup?.messages?.length}` ) - const numGroupsSynced = await bo.conversations.syncAllGroups() + const numGroupsSynced = await bo.conversations.syncAllConversations() assert( (await boGroup?.messages())?.length === 1, `messages should be 4 after sync but was ${boGroup?.messages?.length}` @@ -2344,15 +2167,15 @@ test('can sync all groups', async () => { await group.removeMembers([bo.address]) } - // First syncAllGroups after removal will still sync each group to set group inactive - const numGroupsSynced2 = await bo.conversations.syncAllGroups() + // First syncAllConversations after removal will still sync each group to set group inactive + const numGroupsSynced2 = await bo.conversations.syncAllConversations() assert( numGroupsSynced2 === 50, `should have synced 50 groups but synced ${numGroupsSynced2}` ) - // Next syncAllGroups will not sync inactive groups - const numGroupsSynced3 = await bo.conversations.syncAllGroups() + // Next syncAllConversations will not sync inactive groups + const numGroupsSynced3 = await bo.conversations.syncAllConversations() assert( numGroupsSynced3 === 0, `should have synced 0 groups but synced ${numGroupsSynced3}` @@ -2363,17 +2186,17 @@ test('can sync all groups', async () => { test('only streams groups that can be decrypted', async () => { // Create three MLS enabled Clients const [alixClient, boClient, caroClient] = await createClients(3) - const alixGroups: Group[] = [] - const boGroups: Group[] = [] - const caroGroups: Group[] = [] + const alixGroups: Conversation[] = [] + const boGroups: Conversation[] = [] + const caroGroups: Conversation[] = [] - await alixClient.conversations.streamGroups(async (group: Group) => { + await alixClient.conversations.stream(async (group: Conversation) => { alixGroups.push(group) }) - await boClient.conversations.streamGroups(async (group: Group) => { + await boClient.conversations.stream(async (group: Conversation) => { boGroups.push(group) }) - await caroClient.conversations.streamGroups(async (group: Group) => { + await caroClient.conversations.stream(async (group: Conversation) => { caroGroups.push(group) }) @@ -2403,19 +2226,13 @@ test('can stream groups and messages', async () => { const [alixClient, boClient] = await createClients(2) // Start streaming groups - const groups: Group[] = [] - await alixClient.conversations.streamGroups(async (group: Group) => { + const groups: Conversation[] = [] + await alixClient.conversations.stream(async (group: Conversation) => { groups.push(group) }) // Stream messages twice - await alixClient.conversations.streamAllMessages( - async (message) => {}, - true - ) - await alixClient.conversations.streamAllMessages( - async (message) => {}, - true - ) + await alixClient.conversations.streamAllMessages(async (message) => {}) + await alixClient.conversations.streamAllMessages(async (message) => {}) // bo creates a group with alix so a stream callback is fired // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/example/src/tests/restartStreamsTests.ts b/example/src/tests/restartStreamsTests.ts index db71a4b80..a352ed462 100644 --- a/example/src/tests/restartStreamsTests.ts +++ b/example/src/tests/restartStreamsTests.ts @@ -51,13 +51,13 @@ test('Can cancel a stream and restart', async () => { // Existing issue, client who started stream, creating groups will not // be streamed -test('Can cancel a streamGroups and restart', async () => { +test('Can cancel a stream and restart', async () => { // Create clients const [alix, bo, caro, davon] = await createClients(4) // Start stream let numEvents1 = 0 - await alix.conversations.streamGroups(async (_) => { + await alix.conversations.stream(async (_) => { numEvents1++ }) await delayToPropogate() @@ -66,14 +66,14 @@ test('Can cancel a streamGroups and restart', async () => { assert(numEvents1 === 1, 'expected 1 event, first stream') // Cancel stream - alix.conversations.cancelStreamGroups() + alix.conversations.cancelStream() await caro.conversations.newGroup([alix.address]) await delayToPropogate() assert(numEvents1 === 1, 'expected 1 event, first stream after cancel') // Start new stream let numEvents2 = 0 - await alix.conversations.streamGroups(async (_) => { + await alix.conversations.stream(async (_) => { numEvents2++ }) await delayToPropogate() @@ -108,7 +108,7 @@ test('Can cancel a streamAllMessages and restart', async () => { let numEvents1 = 0 await alix.conversations.streamAllMessages(async (_) => { numEvents1++ - }, true) + }) await delayToPropogate() // Send one Group message and one Conversation Message @@ -135,7 +135,7 @@ test('Can cancel a streamAllMessages and restart', async () => { let numEvents2 = 0 await alix.conversations.streamAllMessages(async (_) => { numEvents2++ - }, true) + }) await delayToPropogate() await boGroup.send('test') @@ -154,59 +154,3 @@ test('Can cancel a streamAllMessages and restart', async () => { return true }) - -test('Can cancel a streamAllGroupMessages and restart', async () => { - // Create clients - const [alix, bo] = await createClients(2) - - // Create a group - await delayToPropogate() - await bo.conversations.newGroup([alix.address]) - await delayToPropogate() - - // Start stream - let numEvents1 = 0 - await alix.conversations.streamAllGroupMessages(async (_) => { - numEvents1++ - }) - await delayToPropogate() - - // Send one Group message and one Conversation Message - const boGroup = (await bo.conversations.listGroups())[0] - - await boGroup.send('test') - await delayToPropogate() - - assert( - numEvents1 === 1, - 'expected 1 events, first stream, but found ' + numEvents1 - ) - - // Cancel stream - alix.conversations.cancelStreamAllGroupMessages() - await boGroup.send('test') - await delayToPropogate() - assert(numEvents1 === 1, 'expected 1 event, first stream after cancel') - - // Start new stream - let numEvents2 = 0 - await alix.conversations.streamAllGroupMessages(async (_) => { - numEvents2++ - }) - await delayToPropogate() - - await boGroup.send('test') - await delayToPropogate() - - // Verify correct number of events from each stream - assert( - numEvents1 === 1, - 'expected 1 event, first stream after cancel, but found ' + numEvents1 - ) - assert( - numEvents2 === 1, - 'expected 1 event, second stream, but found ' + numEvents2 - ) - - return true -}) diff --git a/example/src/tests/test-utils.ts b/example/src/tests/test-utils.ts index fc70a5887..d920f0aad 100644 --- a/example/src/tests/test-utils.ts +++ b/example/src/tests/test-utils.ts @@ -31,7 +31,6 @@ export async function createClients(numClients: number): Promise { ]) const client = await Client.createRandom({ env: 'local', - enableV3: true, dbEncryptionKey: keyBytes, }) client.register(new GroupUpdatedCodec()) @@ -40,51 +39,6 @@ export async function createClients(numClients: number): Promise { return clients } -export async function createV3Clients(numClients: number): Promise { - const clients = [] - for (let i = 0; i < numClients; i++) { - const keyBytes = new Uint8Array([ - 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, - 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, - 145, - ]) - const client = await Client.createRandomV3({ - env: 'local', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - client.register(new GroupUpdatedCodec()) - clients.push(client) - } - return clients -} - -export async function createV3TestingClients(): Promise { - const clients = [] - const keyBytes = new Uint8Array([ - 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, - 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, - ]) - const alix = await Client.createRandom({ - env: 'local', - }) - const bo = await Client.createRandomV3({ - env: 'local', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - const caro = await Client.createRandom({ - env: 'local', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - bo.register(new GroupUpdatedCodec()) - caro.register(new GroupUpdatedCodec()) - - clients.push(alix, bo, caro) - return clients -} - export async function createGroups( client: Client, peers: Client[], diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts deleted file mode 100644 index 56b588c50..000000000 --- a/example/src/tests/tests.ts +++ /dev/null @@ -1,1535 +0,0 @@ -import { FramesClient } from '@xmtp/frames-client' -import { content, invitation } from '@xmtp/proto' -import { createHmac } from 'crypto' -import ReactNativeBlobUtil from 'react-native-blob-util' -import Config from 'react-native-config' -import { TextEncoder, TextDecoder } from 'text-encoding' -import { PrivateKeyAccount } from 'viem' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' -import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' - -import { Test, assert, createClients, delayToPropogate } from './test-utils' -import { - Query, - JSContentCodec, - Client, - Conversation, - StaticAttachmentCodec, - RemoteAttachmentCodec, - RemoteAttachmentContent, - Signer, -} from '../../../src/index' - -type EncodedContent = content.EncodedContent -type ContentTypeId = content.ContentTypeId - -const { fs } = ReactNativeBlobUtil - -const ContentTypeNumber: ContentTypeId = { - authorityId: 'org', - typeId: 'number', - versionMajor: 1, - versionMinor: 0, -} - -const ContentTypeNumberWithUndefinedFallback: ContentTypeId = { - authorityId: 'org', - typeId: 'number_undefined_fallback', - versionMajor: 1, - versionMinor: 0, -} - -const ContentTypeNumberWithEmptyFallback: ContentTypeId = { - authorityId: 'org', - typeId: 'number_empty_fallback', - versionMajor: 1, - versionMinor: 0, -} - -export type NumberRef = { - topNumber: { - bottomNumber: number - } -} - -class NumberCodec implements JSContentCodec { - contentType = ContentTypeNumber - - // a completely absurd way of encoding number values - encode(content: NumberRef): EncodedContent { - return { - type: ContentTypeNumber, - parameters: { - test: 'test', - }, - content: new TextEncoder().encode(JSON.stringify(content)), - } - } - - decode(encodedContent: EncodedContent): NumberRef { - if (encodedContent.parameters.test !== 'test') { - throw new Error(`parameters should parse ${encodedContent.parameters}`) - } - const contentReceived = JSON.parse( - new TextDecoder().decode(encodedContent.content) - ) as NumberRef - return contentReceived - } - - fallback(content: NumberRef): string | undefined { - return 'a billion' - } -} - -class NumberCodecUndefinedFallback extends NumberCodec { - contentType = ContentTypeNumberWithUndefinedFallback - fallback(content: NumberRef): string | undefined { - return undefined - } -} - -class NumberCodecEmptyFallback extends NumberCodec { - contentType = ContentTypeNumberWithEmptyFallback - fallback(content: NumberRef): string | undefined { - return '' - } -} - -const LONG_STREAM_DELAY = 20000 - -export const tests: Test[] = [] - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -const hkdfNoSalt = new ArrayBuffer(0) - -async function hkdfHmacKey( - secret: Uint8Array, - info: Uint8Array -): Promise { - const key = await window.crypto.subtle.importKey( - 'raw', - secret, - 'HKDF', - false, - ['deriveKey'] - ) - return await window.crypto.subtle.deriveKey( - { name: 'HKDF', hash: 'SHA-256', salt: hkdfNoSalt, info }, - key, - { name: 'HMAC', hash: 'SHA-256', length: 256 }, - true, - ['sign', 'verify'] - ) -} - -export async function importHmacKey(key: Uint8Array): Promise { - return crypto.subtle.importKey( - 'raw', - key, - { name: 'HMAC', hash: 'SHA-256' }, - true, - ['sign', 'verify'] - ) -} - -async function generateHmacSignature( - secret: Uint8Array, - info: Uint8Array, - message: Uint8Array -): Promise { - const key = await hkdfHmacKey(secret, info) - const signed = await window.crypto.subtle.sign('HMAC', key, message) - return new Uint8Array(signed) -} - -function base64ToUint8Array(base64String: string): Uint8Array { - const buffer = Buffer.from(base64String, 'base64') - return new Uint8Array(buffer) -} - -function verifyHmacSignature( - key: Uint8Array, - signature: Uint8Array, - message: Uint8Array -): boolean { - const hmac = createHmac('sha256', Buffer.from(key)) - - hmac.update(message) - - const calculatedSignature = hmac.digest() - const result = Buffer.compare(calculatedSignature, signature) === 0 - - return result -} - -async function exportHmacKey(key: CryptoKey): Promise { - const exported = await window.crypto.subtle.exportKey('raw', key) - return new Uint8Array(exported) -} - -function test(name: string, perform: () => Promise) { - tests.push({ name, run: perform }) -} - -test('can make a client', async () => { - const client = await Client.createRandom({ - env: 'local', - appVersion: 'Testing/0.0.0', - }) - client.register(new RemoteAttachmentCodec()) - if (Object.keys(client.codecRegistry).length !== 2) { - throw new Error( - `Codecs length should be 2 not ${ - Object.keys(client.codecRegistry).length - }` - ) - } - return client.address.length > 0 -}) - -export function convertPrivateKeyAccountToSigner( - privateKeyAccount: PrivateKeyAccount -): Signer { - if (!privateKeyAccount.address) { - throw new Error('WalletClient is not configured') - } - - return { - getAddress: async () => privateKeyAccount.address, - signMessage: async (message: string | Uint8Array) => - privateKeyAccount.signMessage({ - message: typeof message === 'string' ? message : { raw: message }, - }), - getChainId: () => undefined, - getBlockNumber: () => undefined, - walletType: () => undefined, - } -} - -test('can load a client from env "2k lens convos" private key', async () => { - if (!Config.TEST_PRIVATE_KEY) { - throw new Error('Add private key to .env file') - } - const privateKeyHex: `0x${string}` = `0x${Config.TEST_PRIVATE_KEY}` - - const signer = convertPrivateKeyAccountToSigner( - privateKeyToAccount(privateKeyHex) - ) - const xmtpClient = await Client.create(signer, { - env: 'local', - }) - - const keyBundle = await xmtpClient.exportKeyBundle() - - await Client.createFromKeyBundle( - keyBundle, - { - env: 'local', - }, - signer - ) - - return true -}) - -test('can load 1995 conversations from dev network "2k lens convos" account', async () => { - if (!Config.TEST_PRIVATE_KEY) { - throw new Error('Add private key to .env file') - } - - const privateKeyHex: `0x${string}` = `0x${Config.TEST_PRIVATE_KEY}` - - const signer = convertPrivateKeyAccountToSigner( - privateKeyToAccount(privateKeyHex) - ) - const xmtpClient = await Client.create(signer, { - env: 'dev', - }) - - const start = Date.now() - const conversations = await xmtpClient.conversations.list() - const end = Date.now() - console.log( - `Loaded ${conversations.length} conversations in ${end - start}ms` - ) - - return true -}) - -test('can pass a custom filter date and receive message objects with expected dates', async () => { - try { - const bob = await Client.createRandom({ env: 'local' }) - const alice = await Client.createRandom({ env: 'local' }) - - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation( - alice.address - ) - - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - const sentAt = Date.now() - await bobConversation.send({ text: 'hello' }) - - const initialQueryDate = new Date('2023-01-01') - const finalQueryDate = new Date('2025-01-01') - - // Show all messages before date in the past - const messages1: DecodedMessage[] = await aliceConversation.messages({ - before: initialQueryDate, - }) - - // Show all messages before date in the future - const messages2: DecodedMessage[] = await aliceConversation.messages({ - before: finalQueryDate, - }) - - const isAboutRightSendTime = Math.abs(messages2[0].sent - sentAt) < 1000 - if (!isAboutRightSendTime) return false - - const passingDateFieldSuccessful = - !messages1.length && messages2.length === 1 - - if (!passingDateFieldSuccessful) return false - - // repeat the above test with a numeric date value - - // Show all messages before date in the past - const messages3: DecodedMessage[] = await aliceConversation.messages({ - before: initialQueryDate.getTime(), - }) - - // Show all messages before date in the future - const messages4: DecodedMessage[] = await aliceConversation.messages({ - before: finalQueryDate.getTime(), - }) - - const passingTimestampFieldSuccessful = - !messages3.length && messages4.length === 1 - - return passingTimestampFieldSuccessful - } catch { - return false - } -}) - -test('canMessage', async () => { - const bob = await Client.createRandom({ env: 'local' }) - const alice = await Client.createRandom({ env: 'local' }) - - const canMessage = await bob.canMessage(alice.address) - return canMessage -}) - -test('fetch a public key bundle and sign a digest', async () => { - const bob = await Client.createRandom({ env: 'local' }) - const bytes = new Uint8Array([1, 2, 3]) - const signature = await bob.sign(bytes, { kind: 'identity' }) - if (signature.length === 0) { - throw new Error('signature was not returned') - } - const keyBundle = await bob.exportPublicKeyBundle() - if (keyBundle.length === 0) { - throw new Error('key bundle was not returned') - } - return true -}) - -test('createFromKeyBundle throws error for non string value', async () => { - try { - const bytes = [1, 2, 3] - await Client.createFromKeyBundle(JSON.stringify(bytes), { - env: 'local', - }) - } catch { - return true - } - return false -}) - -test('canPrepareMessage', async () => { - const bob = await Client.createRandom({ env: 'local' }) - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - - const bobConversation = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - - const prepared = await bobConversation.prepareMessage('hi') - if (!prepared.preparedAt) { - throw new Error('missing `preparedAt` on prepared message') - } - - // Either of these should work: - await bobConversation.sendPreparedMessage(prepared) - // await bob.sendPreparedMessage(prepared); - - await delayToPropogate() - const messages = await bobConversation.messages() - if (messages.length !== 1) { - throw new Error(`expected 1 message: got ${messages.length}`) - } - const message = messages[0] - - return message?.id === prepared.messageId -}) - -test('can list batch messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - await bobConversation.send({ text: 'Hello world' }) - const bobMessages = await bobConversation.messages() - await bobConversation.send({ - reaction: { - reference: bobMessages[0].id, - action: 'added', - schema: 'unicode', - content: '💖', - }, - }) - - await delayToPropogate() - const messages: DecodedMessage[] = await alice.listBatchMessages([ - { - contentTopic: bobConversation.topic, - } as Query, - { - contentTopic: aliceConversation.topic, - } as Query, - ]) - - if (messages.length < 1) { - throw Error('No message') - } - - if (messages[0].contentTypeId !== 'xmtp.org/reaction:1.0') { - throw Error( - 'Unexpected message content ' + JSON.stringify(messages[0].contentTypeId) - ) - } - - if (messages[0].fallback !== 'Reacted “💖” to an earlier message') { - throw Error('Unexpected message fallback ' + messages[0].fallback) - } - - return true -}) - -test('can paginate batch messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - await bobConversation.send({ text: `Initial Message` }) - - await delayToPropogate() - const testTime = new Date() - await delayToPropogate() - - for (let i = 0; i < 5; i++) { - await bobConversation.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - const messagesLimited: DecodedMessage[] = await alice.listBatchMessages([ - { - contentTopic: bobConversation.topic, - pageSize: 2, - } as Query, - ]) - - const messagesAfter: DecodedMessage[] = await alice.listBatchMessages([ - { - contentTopic: bobConversation.topic, - startTime: testTime, - endTime: new Date(), - } as Query, - ]) - - const messagesBefore: DecodedMessage[] = await alice.listBatchMessages([ - { - contentTopic: bobConversation.topic, - endTime: testTime, - } as Query, - ]) - - await bobConversation.send('') - await delayToPropogate() - - const messagesAsc: DecodedMessage[] = await alice.listBatchMessages([ - { - contentTopic: bobConversation.topic, - direction: 'SORT_DIRECTION_ASCENDING', - } as Query, - ]) - - if (messagesLimited.length !== 2) { - throw Error('Unexpected messagesLimited count ' + messagesLimited.length) - } - - if (messagesLimited[0].content() !== 'Message 4') { - throw Error( - 'Unexpected messagesLimited content ' + messagesLimited[0].content() - ) - } - if (messagesLimited[1].content() !== 'Message 3') { - throw Error( - 'Unexpected messagesLimited content ' + messagesLimited[1].content() - ) - } - - if (messagesBefore.length !== 1) { - throw Error('Unexpected messagesBefore count ' + messagesBefore.length) - } - if (messagesBefore[0].content() !== 'Initial Message') { - throw Error( - 'Unexpected messagesBefore content ' + messagesBefore[0].content() - ) - } - - if (messagesAfter.length !== 5) { - throw Error('Unexpected messagesAfter count ' + messagesAfter.length) - } - if (messagesAfter[0].content() !== 'Message 4') { - throw Error( - 'Unexpected messagesAfter content ' + messagesAfter[0].content() - ) - } - - if (messagesAsc[0].content() !== 'Initial Message') { - throw Error('Unexpected messagesAsc content ' + messagesAsc[0].content()) - } - - if (messagesAsc[6].contentTypeId !== 'xmtp.org/text:1.0') { - throw Error('Unexpected messagesAsc content ' + messagesAsc[6].content()) - } - - return true -}) - -test('can stream messages', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - - // Record new conversation stream - const allConversations: Conversation[] = [] - await alice.conversations.stream(async (conversation) => { - allConversations.push(conversation) - }) - - // Record message stream across all conversations - const allMessages: DecodedMessage[] = [] - await alice.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - // Start Bob starts a new conversation. - const bobConvo = await bob.conversations.newConversation(alice.address, { - conversationID: 'https://example.com/alice-and-bob', - metadata: { - title: 'Alice and Bob', - }, - }) - await delayToPropogate() - - if (bobConvo.client.address !== bob.address) { - throw Error('Unexpected client address ' + bobConvo.client.address) - } - if (!bobConvo.topic) { - throw Error('Missing topic ' + bobConvo.topic) - } - if ( - bobConvo.context?.conversationID !== 'https://example.com/alice-and-bob' - ) { - throw Error('Unexpected conversationID ' + bobConvo.context?.conversationID) - } - if (bobConvo.context?.metadata?.title !== 'Alice and Bob') { - throw Error( - 'Unexpected metadata title ' + bobConvo.context?.metadata?.title - ) - } - if (!bobConvo.createdAt) { - throw Error('Missing createdAt ' + bobConvo.createdAt) - } - - if (allConversations.length !== 1) { - throw Error('Unexpected all conversations count ' + allConversations.length) - } - if (allConversations[0].topic !== bobConvo.topic) { - throw Error( - 'Unexpected all conversations topic ' + allConversations[0].topic - ) - } - - const aliceConvo = (await alice.conversations.list())[0] - if (!aliceConvo) { - throw new Error('missing conversation') - } - - // Record message stream for this conversation - const convoMessages: DecodedMessage[] = [] - await aliceConvo.streamMessages(async (message) => { - convoMessages.push(message) - }) - - for (let i = 0; i < 5; i++) { - await bobConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - if (allMessages.length !== 5) { - throw Error('Unexpected all messages count ' + allMessages.length) - } - if (convoMessages.length !== 5) { - throw Error('Unexpected convo messages count ' + convoMessages.length) - } - for (let i = 0; i < 5; i++) { - if (allMessages[i].content() !== `Message ${i}`) { - throw Error('Unexpected all message content ' + allMessages[i].content()) - } - if (allMessages[i].topic !== bobConvo.topic) { - throw Error('Unexpected all message topic ' + allMessages[i].topic) - } - if (convoMessages[i].content() !== `Message ${i}`) { - throw Error( - 'Unexpected convo message content ' + convoMessages[i].content() - ) - } - if (convoMessages[i].topic !== bobConvo.topic) { - throw Error('Unexpected convo message topic ' + convoMessages[i].topic) - } - } - alice.conversations.cancelStream() - alice.conversations.cancelStreamAllMessages() - - return true -}) - -test('can stream conversations with delay', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - - const allConvos: Conversation[] = [] - await alix.conversations.stream(async (convo) => { - allConvos.push(convo) - }) - - await bo.conversations.newConversation(alix.address) - await delayToPropogate() - - await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-2', - metadata: {}, - }) - await delayToPropogate() - - assert( - allConvos.length === 2, - 'Unexpected all convos count ' + allConvos.length - ) - - await sleep(LONG_STREAM_DELAY) - - await bo.conversations.newConversation(alix.address, { - conversationID: 'convo-3', - metadata: {}, - }) - await delayToPropogate() - - assert( - allConvos.length === 3, - 'Unexpected all convos count ' + allConvos.length - ) - - alix.conversations.cancelStream() - return true -}) - -test('remote attachments should work', async () => { - const alice = await Client.createRandom({ - env: 'local', - codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], - }) - const bob = await Client.createRandom({ - env: 'local', - codecs: [new StaticAttachmentCodec(), new RemoteAttachmentCodec()], - }) - const convo = await alice.conversations.newConversation(bob.address) - - // Alice is sending Bob a file from her phone. - const filename = `${Date.now()}.txt` - const file = `${fs.dirs.CacheDir}/${filename}` - await fs.writeFile(file, 'hello world', 'utf8') - const { encryptedLocalFileUri, metadata } = await alice.encryptAttachment({ - fileUri: `file://${file}`, - mimeType: 'text/plain', - }) - - const encryptedFile = encryptedLocalFileUri.slice('file://'.length) - const originalContent = await fs.readFile(file, 'base64') - const encryptedContent = await fs.readFile(encryptedFile, 'base64') - if (encryptedContent === originalContent) { - throw new Error('encrypted file should not match original') - } - - // This is where the app will upload the encrypted file to a remote server and generate a URL. - // let url = await uploadFile(encryptedLocalFileUri); - const url = 'https://example.com/123' - - // Together with the metadata, we send the URL as a remoteAttachment message to the conversation. - await convo.send({ - remoteAttachment: { - ...metadata, - scheme: 'https://', - url, - }, - }) - await delayToPropogate() - - // Now we should see the remote attachment message. - const messages = await convo.messages() - if (messages.length !== 1) { - throw new Error('Expected 1 message') - } - const message = messages[0] - - if (message.contentTypeId !== 'xmtp.org/remoteStaticAttachment:1.0') { - throw new Error('Expected correctly formatted typeId') - } - if (!message.content()) { - throw new Error('Expected remoteAttachment') - } - if ( - (message.content() as RemoteAttachmentContent).url !== - 'https://example.com/123' - ) { - throw new Error('Expected url to match') - } - - // This is where the app prompts the user to download the encrypted file from `url`. - // TODO: let downloadedFile = await downloadFile(url); - // But to simplify this test, we're just going to copy - // the previously encrypted file and pretend that we just downloaded it. - const downloadedFileUri = `file://${fs.dirs.CacheDir}/${Date.now()}.bin` - await fs.cp( - new URL(encryptedLocalFileUri).pathname, - new URL(downloadedFileUri).pathname - ) - - // Now we can decrypt the downloaded file using the message metadata. - const attached = await alice.decryptAttachment({ - encryptedLocalFileUri: downloadedFileUri, - metadata: message.content() as RemoteAttachmentContent, - }) - if (attached.mimeType !== 'text/plain') { - throw new Error('Expected mimeType to match') - } - if (attached.filename !== filename) { - throw new Error(`Expected ${attached.filename} to equal ${filename}`) - } - const text = await fs.readFile(new URL(attached.fileUri).pathname, 'utf8') - if (text !== 'hello world') { - throw new Error('Expected text to match') - } - return true -}) - -test('can send read receipts', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - await bobConversation.send({ readReceipt: {} }) - - const bobMessages = await bobConversation.messages() - - if (bobMessages.length < 1) { - throw Error('No message') - } - - if (bobMessages[0].contentTypeId !== 'xmtp.org/readReceipt:1.0') { - throw Error('Unexpected message content ' + bobMessages[0].contentTypeId) - } - - if (bobMessages[0].fallback) { - throw Error('Unexpected message fallback ' + bobMessages[0].fallback) - } - - return true -}) - -test('can stream all messages', async () => { - const bo = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - - // Record message stream across all conversations - const allMessages: DecodedMessage[] = [] - await alix.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - // Start Bob starts a new conversation. - const boConvo = await bo.conversations.newConversation(alix.address) - await delayToPropogate() - - for (let i = 0; i < 5; i++) { - await boConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - const count = allMessages.length - if (count !== 5) { - throw Error('Unexpected all messages count ' + allMessages.length) - } - - // Starts a new conversation. - const caro = await Client.createRandom({ env: 'local' }) - const caroConvo = await caro.conversations.newConversation(alix.address) - await delayToPropogate() - for (let i = 0; i < 5; i++) { - await caroConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - if (allMessages.length !== 10) { - throw Error('Unexpected all messages count ' + allMessages.length) - } - - alix.conversations.cancelStreamAllMessages() - - await alix.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - for (let i = 0; i < 5; i++) { - await boConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - if (allMessages.length <= 10) { - throw Error('Unexpected all messages count ' + allMessages.length) - } - - return true -}) - -test('can stream all msgs with delay', async () => { - const bo = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'dev' }) - await delayToPropogate() - - // Record message stream across all conversations - const allMessages: DecodedMessage[] = [] - await alix.conversations.streamAllMessages(async (message) => { - allMessages.push(message) - }) - - // Start Bob starts a new conversation. - const boConvo = await bo.conversations.newConversation(alix.address) - await delayToPropogate() - - for (let i = 0; i < 5; i++) { - await boConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - assert( - allMessages.length === 5, - 'Unexpected all messages count ' + allMessages.length - ) - - await sleep(LONG_STREAM_DELAY) - // Starts a new conversation. - const caro = await Client.createRandom({ env: 'dev' }) - const caroConvo = await caro.conversations.newConversation(alix.address) - await delayToPropogate() - - for (let i = 0; i < 5; i++) { - await caroConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - assert( - allMessages.length === 10, - 'Unexpected all messages count ' + allMessages.length - ) - - await sleep(LONG_STREAM_DELAY) - - for (let i = 0; i < 5; i++) { - await boConvo.send({ text: `Message ${i}` }) - await delayToPropogate() - } - - assert( - allMessages.length === 15, - 'Unexpected all messages count ' + allMessages.length - ) - - alix.conversations.cancelStreamAllMessages() - - return true -}) - -test('canManagePreferences', async () => { - const bo = await Client.createRandom({ env: 'local' }) - const alix = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - - const alixConversation = await bo.conversations.newConversation(alix.address) - await delayToPropogate() - - const initialConvoState = await alixConversation.consentState() - if (initialConvoState !== 'allowed') { - throw new Error( - `conversations created by bo should be allowed by default not ${initialConvoState}` - ) - } - - const initialState = await bo.contacts.isAllowed(alixConversation.peerAddress) - if (!initialState) { - throw new Error( - `contacts created by bo should be allowed by default not ${initialState}` - ) - } - - await bo.contacts.deny([alixConversation.peerAddress]) - await delayToPropogate() - - const deniedState = await bo.contacts.isDenied(alixConversation.peerAddress) - const allowedState = await bo.contacts.isAllowed(alixConversation.peerAddress) - if (!deniedState) { - throw new Error(`contacts denied by bo should be denied not ${deniedState}`) - } - - if (allowedState) { - throw new Error( - `contacts denied by bo should be denied not ${allowedState}` - ) - } - - const convoState = await alixConversation.consentState() - await delayToPropogate() - - if (convoState !== 'denied') { - throw new Error( - `conversations denied by bo should be denied not ${convoState}` - ) - } - - const boConsentList = await bo.contacts.consentList() - await delayToPropogate() - - if (boConsentList.length !== 1) { - throw new Error(`consent list for bo should 1 not ${boConsentList.length}`) - } - - const boConsentListState = boConsentList[0].permissionType - - if (boConsentListState !== 'denied') { - throw new Error( - `conversations denied by bo should be denied in consent list not ${boConsentListState}` - ) - } - - return true -}) - -test('is address on the XMTP network', async () => { - const alix = await Client.createRandom({ env: 'local' }) - const notOnNetwork = '0x0000000000000000000000000000000000000000' - - const isAlixAddressAvailable = await Client.canMessage(alix.address, { - env: 'local', - }) - const isAddressAvailable = await Client.canMessage(notOnNetwork, { - env: 'local', - }) - - if (!isAlixAddressAvailable) { - throw new Error('alix address should be available') - } - - if (isAddressAvailable) { - throw new Error('address not on network should not be available') - } - - return true -}) - -test('register and use custom content types', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - const alice = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - - bob.register(new NumberCodec()) - alice.register(new NumberCodec()) - - await delayToPropogate() - - const bobConvo = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - const aliceConvo = await alice.conversations.newConversation(bob.address) - - await bobConvo.send( - { topNumber: { bottomNumber: 12 } }, - { contentType: ContentTypeNumber } - ) - - const messages = await aliceConvo.messages() - assert(messages.length === 1, 'did not get messages') - - const message = messages[0] - const messageContent = message.content() - - assert( - typeof messageContent === 'object' && - 'topNumber' in messageContent && - messageContent.topNumber.bottomNumber === 12, - 'did not get content properly: ' + JSON.stringify(messageContent) - ) - - return true -}) - -test('register and use custom content types when preparing message', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - const alice = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - }) - - bob.register(new NumberCodec()) - alice.register(new NumberCodec()) - - const bobConvo = await bob.conversations.newConversation(alice.address) - const aliceConvo = await alice.conversations.newConversation(bob.address) - - const prepped = await bobConvo.prepareMessage( - { topNumber: { bottomNumber: 12 } }, - { - contentType: ContentTypeNumber, - } - ) - - await bobConvo.sendPreparedMessage(prepped) - - const messages = await aliceConvo.messages() - assert(messages.length === 1, 'did not get messages') - - const message = messages[0] - const messageContent = message.content() as NumberRef - - assert( - messageContent.topNumber.bottomNumber === 12, - 'did not get content properly: ' + JSON.stringify(messageContent) - ) - - return true -}) - -test('calls preCreateIdentityCallback when supplied', async () => { - let isCallbackCalled = false - const preCreateIdentityCallback = () => { - isCallbackCalled = true - } - await Client.createRandom({ - env: 'local', - preCreateIdentityCallback, - }) - - if (!isCallbackCalled) { - throw new Error('preCreateIdentityCallback not called') - } - - return isCallbackCalled -}) - -test('calls preEnableIdentityCallback when supplied', async () => { - let isCallbackCalled = false - const preEnableIdentityCallback = () => { - isCallbackCalled = true - } - await Client.createRandom({ - env: 'local', - preEnableIdentityCallback, - }) - - if (!isCallbackCalled) { - throw new Error('preEnableIdentityCallback not called') - } - - return isCallbackCalled -}) - -test('returns keyMaterial for conversations', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - if (!aliceConversation.keyMaterial) { - throw new Error('aliceConversation keyMaterial should exist') - } - - if (!bobConversation.keyMaterial) { - throw new Error('bobConversation keyMaterial should exist') - } - - return true -}) - -test('correctly handles lowercase addresses', async () => { - const bob = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alice = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - if (bob.address === alice.address) { - throw new Error('bob and alice should be different') - } - - const bobConversation = await bob.conversations.newConversation( - alice.address.toLocaleLowerCase() - ) - await delayToPropogate() - if (!bobConversation) { - throw new Error('bobConversation should exist') - } - const aliceConversation = (await alice.conversations.list())[0] - if (!aliceConversation) { - throw new Error('aliceConversation should exist') - } - - await bob.contacts.deny([aliceConversation.peerAddress.toLocaleLowerCase()]) - await delayToPropogate() - const deniedState = await bob.contacts.isDenied(aliceConversation.peerAddress) - const allowedState = await bob.contacts.isAllowed( - aliceConversation.peerAddress - ) - if (!deniedState) { - throw new Error(`contacts denied by bo should be denied not ${deniedState}`) - } - - if (allowedState) { - throw new Error( - `contacts denied by bo should be denied not ${allowedState}` - ) - } - const deniedLowercaseState = await bob.contacts.isDenied( - aliceConversation.peerAddress.toLocaleLowerCase() - ) - const allowedLowercaseState = await bob.contacts.isAllowed( - aliceConversation.peerAddress.toLocaleLowerCase() - ) - if (!deniedLowercaseState) { - throw new Error( - `contacts denied by bo should be denied not ${deniedLowercaseState}` - ) - } - - if (allowedLowercaseState) { - throw new Error( - `contacts denied by bo should be denied not ${allowedLowercaseState}` - ) - } - return true -}) - -test('handle fallback types appropriately', async () => { - const bob = await Client.createRandom({ - env: 'local', - codecs: [ - new NumberCodecEmptyFallback(), - new NumberCodecUndefinedFallback(), - ], - }) - const alice = await Client.createRandom({ - env: 'local', - }) - bob.register(new NumberCodecEmptyFallback()) - bob.register(new NumberCodecUndefinedFallback()) - const bobConvo = await bob.conversations.newConversation(alice.address) - const aliceConvo = await alice.conversations.newConversation(bob.address) - - await bobConvo.send(12, { contentType: ContentTypeNumberWithEmptyFallback }) - - await bobConvo.send(12, { - contentType: ContentTypeNumberWithUndefinedFallback, - }) - - const messages = await aliceConvo.messages() - assert(messages.length === 2, 'did not get messages') - - const messageUndefinedFallback = messages[0] - const messageWithDefinedFallback = messages[1] - - let message1Content = undefined - try { - message1Content = messageUndefinedFallback.content() - } catch { - message1Content = messageUndefinedFallback.fallback - } - - assert( - message1Content === undefined, - 'did not get content properly when empty fallback: ' + - JSON.stringify(message1Content) - ) - - let message2Content = undefined - try { - message2Content = messageWithDefinedFallback.content() - } catch { - message2Content = messageWithDefinedFallback.fallback - } - - assert( - message2Content === '', - 'did not get content properly: ' + JSON.stringify(message2Content) - ) - - return true -}) - -test('instantiate frames client correctly', async () => { - const frameUrl = - 'https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8' - const client = await Client.createRandom({ env: 'local' }) - const framesClient = new FramesClient(client) - const metadata = await framesClient.proxy.readMetadata(frameUrl) - if (!metadata) { - throw new Error('metadata should exist') - } - const signedPayload = await framesClient.signFrameAction({ - frameUrl, - buttonIndex: 1, - conversationTopic: 'foo', - participantAccountAddresses: ['amal', 'bola'], - }) - const postUrl = metadata.extractedTags['fc:frame:post_url'] - const response = await framesClient.proxy.post(postUrl, signedPayload) - if (!response) { - throw new Error('response should exist') - } - if (response.extractedTags['fc:frame'] !== 'vNext') { - throw new Error('response should have expected extractedTags') - } - const imageUrl = response.extractedTags['fc:frame:image'] - const mediaUrl = framesClient.proxy.mediaUrl(imageUrl) - - const downloadedMedia = await fetch(mediaUrl) - if (!downloadedMedia.ok) { - throw new Error('downloadedMedia should be ok') - } - if (downloadedMedia.headers.get('content-type') !== 'image/png') { - throw new Error('downloadedMedia should be image/png') - } - return true -}) - -test('generates and validates HMAC', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) - const valid = await verifyHmacSignature( - await exportHmacKey(key), - hmac, - message - ) - return valid -}) - -test('generates and validates HMAC with imported key', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) - const exportedKey = await exportHmacKey(key) - const importedKey = await importHmacKey(exportedKey) - const valid = await verifyHmacSignature( - await exportHmacKey(importedKey), - hmac, - message - ) - return valid -}) - -test('generates different HMAC keys with different infos', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info1 = crypto.getRandomValues(new Uint8Array(32)) - const info2 = crypto.getRandomValues(new Uint8Array(32)) - const key1 = await hkdfHmacKey(secret, info1) - const key2 = await hkdfHmacKey(secret, info2) - - const exported1 = await exportHmacKey(key1) - const exported2 = await exportHmacKey(key2) - return exported1 !== exported2 -}) - -test('fails to validate HMAC with wrong message', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const key = await hkdfHmacKey(secret, info) - const valid = await verifyHmacSignature( - await exportHmacKey(key), - hmac, - crypto.getRandomValues(new Uint8Array(32)) - ) - return !valid -}) - -test('fails to validate HMAC with wrong key', async () => { - const secret = crypto.getRandomValues(new Uint8Array(32)) - const info = crypto.getRandomValues(new Uint8Array(32)) - const message = crypto.getRandomValues(new Uint8Array(32)) - const hmac = await generateHmacSignature(secret, info, message) - const valid = await verifyHmacSignature( - await exportHmacKey( - await hkdfHmacKey( - crypto.getRandomValues(new Uint8Array(32)), - crypto.getRandomValues(new Uint8Array(32)) - ) - ), - hmac, - message - ) - return !valid -}) - -test('get all HMAC keys', async () => { - const alice = await Client.createRandom({ env: 'local' }) - - const conversations: Conversation[] = [] - - for (let i = 0; i < 5; i++) { - const client = await Client.createRandom({ env: 'local' }) - const convo = await alice.conversations.newConversation(client.address, { - conversationID: `https://example.com/${i}`, - metadata: { - title: `Conversation ${i}`, - }, - }) - conversations.push(convo) - } - const thirtyDayPeriodsSinceEpoch = Math.floor( - Date.now() / 1000 / 60 / 60 / 24 / 30 - ) - - const periods = [ - thirtyDayPeriodsSinceEpoch - 1, - thirtyDayPeriodsSinceEpoch, - thirtyDayPeriodsSinceEpoch + 1, - ] - const { hmacKeys } = await alice.conversations.getHmacKeys() - - const topics = Object.keys(hmacKeys) - conversations.forEach((conversation) => { - assert(topics.includes(conversation.topic), 'topic not found') - }) - - const topicHmacs: { - [topic: string]: Uint8Array - } = {} - const headerBytes = crypto.getRandomValues(new Uint8Array(10)) - - for (const conversation of conversations) { - const topic = conversation.topic - - const keyMaterial = conversation.keyMaterial! - const info = `${thirtyDayPeriodsSinceEpoch}-${alice.address}` - const hmac = await generateHmacSignature( - base64ToUint8Array(keyMaterial), - new TextEncoder().encode(info), - headerBytes - ) - - topicHmacs[topic] = hmac - } - - await Promise.all( - Object.keys(hmacKeys).map(async (topic) => { - const hmacData = hmacKeys[topic] - - await Promise.all( - hmacData.values.map( - async ({ hmacKey, thirtyDayPeriodsSinceEpoch }, idx) => { - assert( - thirtyDayPeriodsSinceEpoch === periods[idx], - 'periods not equal' - ) - const valid = await verifyHmacSignature( - hmacKey, - topicHmacs[topic], - headerBytes - ) - assert(valid === (idx === 1), 'key is not valid') - } - ) - ) - }) - ) - - return true -}) - -class ViemSigner { - account: PrivateKeyAccount - - constructor(account: PrivateKeyAccount) { - this.account = account - } - - async getAddress() { - return this.account.address - } - - async signMessage(message: string) { - return this.account.signMessage({ message }) - } -} - -test('can send and receive consent proofs', async () => { - const alixPrivateKey = generatePrivateKey() - const alixAccount = privateKeyToAccount(alixPrivateKey) - const boPrivateKey = generatePrivateKey() - const boAccount = privateKeyToAccount(boPrivateKey) - const alixSigner = new ViemSigner(alixAccount) - const boSigner = new ViemSigner(boAccount) - const alix = await Client.create(alixSigner, { env: 'local' }) - const bo = await Client.create(boSigner, { env: 'local' }) - - const timestamp = Date.now() - const consentMessage = - 'XMTP : Grant inbox consent to sender\n' + - '\n' + - `Current Time: ${new Date(timestamp).toUTCString()}\n` + - `From Address: ${bo.address}\n` + - '\n' + - 'For more info: https://xmtp.org/signatures/' - const sig = await alixSigner.signMessage(consentMessage) - const consentProof = invitation.ConsentProofPayload.fromPartial({ - payloadVersion: - invitation.ConsentProofPayloadVersion.CONSENT_PROOF_PAYLOAD_VERSION_1, - signature: sig, - timestamp, - }) - - const boConvo = await bo.conversations.newConversation( - alix.address, - undefined, - consentProof - ) - await delayToPropogate() - assert(!!boConvo?.consentProof, 'bo consentProof should exist') - const convos = await alix.conversations.list() - const alixConvo = convos.find((convo) => convo.topic === boConvo.topic) - await delayToPropogate() - assert(!!alixConvo?.consentProof, ' alix consentProof should not exist') - await delayToPropogate() - await alix.contacts.refreshConsentList() - const isAllowed = await alix.contacts.isAllowed(bo.address) - assert(isAllowed, 'bo should be allowed') - return true -}) - -test('can start conversations without consent proofs', async () => { - const bo = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - const alix = await Client.createRandom({ env: 'local' }) - await delayToPropogate() - - const boConvo = await bo.conversations.newConversation(alix.address) - await delayToPropogate() - assert(!boConvo.consentProof, 'consentProof should not exist') - const alixConvo = (await alix.conversations.list())[0] - await delayToPropogate() - assert(!alixConvo.consentProof, 'consentProof should not exist') - await delayToPropogate() - return true -}) - -test('can export logs', async () => { - await createClients(2) - - const logs = await Client.exportNativeLogs() - console.log(logs) - assert(logs.includes('libxmtp'), 'should be able to read libxmtp logs') - - return true -}) diff --git a/example/src/tests/v3OnlyTests.ts b/example/src/tests/v3OnlyTests.ts deleted file mode 100644 index 9e0004780..000000000 --- a/example/src/tests/v3OnlyTests.ts +++ /dev/null @@ -1,305 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-extra-non-null-assertion */ -import { Client } from 'xmtp-react-native-sdk' - -import { - Test, - assert, - createV3TestingClients, - delayToPropogate, -} from './test-utils' - -export const v3OnlyTests: Test[] = [] -let counter = 1 -function test(name: string, perform: () => Promise) { - v3OnlyTests.push({ - name: String(counter++) + '. ' + name, - run: perform, - }) -} - -test('can make a V3 only client', async () => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const keyBytes = new Uint8Array([ - 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, - 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, - ]) - const client = await Client.createRandomV3({ - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - - const inboxId = await Client.getOrCreateInboxId(client.address, { - env: 'local', - }) - - assert( - client.inboxId === inboxId, - `inboxIds should match but were ${client.inboxId} and ${inboxId}` - ) - - const client2 = await Client.buildV3(client.address, { - env: 'local', - appVersion: 'Testing/0.0.0', - enableV3: true, - dbEncryptionKey: keyBytes, - }) - - assert( - client.inboxId === client2.inboxId, - `inboxIds should match but were ${client.inboxId} and ${client2.inboxId}` - ) - - const canMessageV3 = await client.canGroupMessage([client.address]) - assert( - canMessageV3[client.address.toLowerCase()] === true, - `canMessageV3 should be true` - ) - try { - await client.canMessage(client.address) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - return true - } - throw new Error('should throw error when hitting V2 api') -}) - -test('can create group', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const group = await boV3.conversations.newGroup([caroV2V3.address]) - assert(group?.members?.length === 2, `group should have 2 members`) - - try { - await boV3.conversations.newGroup([alixV2.address]) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - return true - } - throw new Error( - 'should throw error when trying to add a V2 only client to a group' - ) -}) - -test('can create dm', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const dm = await boV3.conversations.findOrCreateDm(caroV2V3.address) - assert(dm?.members?.length === 2, `dm should have 2 members`) - - try { - await boV3.conversations.findOrCreateDm(alixV2.address) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { - return true - } - throw new Error( - 'should throw error when trying to add a V2 only client to a dm' - ) -}) - -test('can send message', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const group = await boV3.conversations.newGroup([caroV2V3.address]) - await group.send('gm') - await group.sync() - const groupMessages = await group.messages() - assert( - groupMessages[0].content() === 'gm', - `first should be gm but was ${groupMessages[0].content()}` - ) - - await caroV2V3.conversations.syncGroups() - const sameGroups = await caroV2V3.conversations.listGroups() - await sameGroups[0].sync() - - const sameGroupMessages = await sameGroups[0].messages() - assert( - sameGroupMessages[0].content() === 'gm', - `second should be gm but was ${sameGroupMessages[0].content()}` - ) - return true -}) - -test('can send messages to dm', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const dm = await boV3.conversations.findOrCreateDm(caroV2V3.address) - await dm.send('gm') - await dm.sync() - const dmMessages = await dm.messages() - assert( - dmMessages[0].content() === 'gm', - `first should be gm but was ${dmMessages[0].content()}` - ) - - await caroV2V3.conversations.syncConversations() - const sameDm = await caroV2V3.conversations.findConversation(dm.id) - await sameDm?.sync() - - const sameDmMessages = await sameDm!!.messages() - assert( - sameDmMessages[0].content() === 'gm', - `second should be gm but was ${sameDmMessages[0].content()}` - ) - return true -}) - -test('can group consent', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const group = await boV3.conversations.newGroup([caroV2V3.address]) - - const isAllowed = await boV3.contacts.isGroupAllowed(group.id) - assert(isAllowed === true, `isAllowed should be true but was ${isAllowed}`) - let groupState = await group.state - assert( - groupState === 'allowed', - `group state should be allowed but was ${groupState}` - ) - - await boV3.contacts.denyGroups([group.id]) - - const isDenied = await boV3.contacts.isGroupDenied(group.id) - assert(isDenied === true, `isDenied should be true but was ${isDenied}`) - groupState = await group.consentState() - assert( - groupState === 'denied', - `group state should be denied but was ${groupState}` - ) - - await group.updateConsent('allowed') - - const isAllowed2 = await boV3.contacts.isGroupAllowed(group.id) - assert(isAllowed2 === true, `isAllowed2 should be true but was ${isAllowed2}`) - groupState = await group.consentState() - assert( - groupState === 'allowed', - `group state should be allowed but was ${groupState}` - ) - - return true -}) - -test('can allow and deny inbox ids', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const boGroup = await boV3.conversations.newGroup([caroV2V3.address]) - - let isInboxAllowed = await boV3.contacts.isInboxAllowed(caroV2V3.inboxId) - let isInboxDenied = await boV3.contacts.isInboxDenied(caroV2V3.inboxId) - assert( - isInboxAllowed === false, - `isInboxAllowed should be false but was ${isInboxAllowed}` - ) - assert( - isInboxDenied === false, - `isInboxDenied should be false but was ${isInboxDenied}` - ) - - await boV3.contacts.allowInboxes([caroV2V3.inboxId]) - - let caroMember = (await boGroup.membersList()).find( - (member) => member.inboxId === caroV2V3.inboxId - ) - assert( - caroMember?.consentState === 'allowed', - `caroMember should be allowed but was ${caroMember?.consentState}` - ) - - isInboxAllowed = await boV3.contacts.isInboxAllowed(caroV2V3.inboxId) - isInboxDenied = await boV3.contacts.isInboxDenied(caroV2V3.inboxId) - assert( - isInboxAllowed === true, - `isInboxAllowed2 should be true but was ${isInboxAllowed}` - ) - assert( - isInboxDenied === false, - `isInboxDenied2 should be false but was ${isInboxDenied}` - ) - - let isAddressAllowed = await boV3.contacts.isAllowed(caroV2V3.address) - let isAddressDenied = await boV3.contacts.isDenied(caroV2V3.address) - assert( - isAddressAllowed === true, - `isAddressAllowed should be true but was ${isAddressAllowed}` - ) - assert( - isAddressDenied === false, - `isAddressDenied should be false but was ${isAddressDenied}` - ) - - await boV3.contacts.denyInboxes([caroV2V3.inboxId]) - - caroMember = (await boGroup.membersList()).find( - (member) => member.inboxId === caroV2V3.inboxId - ) - assert( - caroMember?.consentState === 'denied', - `caroMember should be denied but was ${caroMember?.consentState}` - ) - - isInboxAllowed = await boV3.contacts.isInboxAllowed(caroV2V3.inboxId) - isInboxDenied = await boV3.contacts.isInboxDenied(caroV2V3.inboxId) - assert( - isInboxAllowed === false, - `isInboxAllowed3 should be false but was ${isInboxAllowed}` - ) - assert( - isInboxDenied === true, - `isInboxDenied3 should be true but was ${isInboxDenied}` - ) - - await boV3.contacts.allow([alixV2.address]) - - isAddressAllowed = await boV3.contacts.isAllowed(alixV2.address) - isAddressDenied = await boV3.contacts.isDenied(alixV2.address) - assert( - isAddressAllowed === true, - `isAddressAllowed2 should be true but was ${isAddressAllowed}` - ) - assert( - isAddressDenied === false, - `isAddressDenied2 should be false but was ${isAddressDenied}` - ) - - return true -}) - -test('can stream all messages', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - const conversation = await alixV2.conversations.newConversation( - caroV2V3.address - ) - const group = await boV3.conversations.newGroup([caroV2V3.address]) - await caroV2V3.conversations.syncGroups() - - const allMessages: any[] = [] - - await caroV2V3.conversations.streamAllMessages(async (conversation) => { - allMessages.push(conversation) - }, true) - - await conversation.send('hi') - await group.send('hi') - - assert(allMessages.length === 2, '2 messages should have been streamed') - - return true -}) - -test('can stream groups and conversations', async () => { - const [alixV2, boV3, caroV2V3] = await createV3TestingClients() - - const allConvos: any[] = [] - - await caroV2V3.conversations.streamAll(async (conversation) => { - allConvos.push(conversation) - }) - - await alixV2.conversations.newConversation(caroV2V3.address) - await boV3.conversations.newGroup([caroV2V3.address]) - - await delayToPropogate() - - assert(allConvos.length === 2, '2 convos should have been streamed') - - return true -}) diff --git a/src/index.ts b/src/index.ts index 48df34727..8ea10f046 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,7 @@ import { EventEmitter, NativeModulesProxy } from 'expo-modules-core' import { Client } from '.' import XMTPModule from './XMTPModule' -import { InboxId, XMTPEnvironment } from './lib/Client' +import { Address, InboxId, XMTPEnvironment } from './lib/Client' import { ConsentListEntry, ConsentListEntryType, @@ -29,7 +29,7 @@ import { import { DefaultContentTypes } from './lib/types/DefaultContentType' import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' -import { Address, getAddress } from './utils/address' +import { getAddress } from './utils/address' export * from './context' export * from './hooks' @@ -955,7 +955,7 @@ export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus, ConsentState } export { Group } from './lib/Group' export { Dm } from './lib/Dm' export { Member } from './lib/Member' -export { InboxId, XMTPEnvironment } from './lib/Client' +export { Address, InboxId, XMTPEnvironment } from './lib/Client' export { ConversationOptions, ConversationOrder, @@ -963,5 +963,4 @@ export { ConversationTopic, ConversationType, } from './lib/types/ConversationOptions' -export { Address } from './utils/address' export { MessageId, MessageOrder } from './lib/types/MessagesOptions' diff --git a/src/lib/Client.ts b/src/lib/Client.ts index accbe1844..d141cbf57 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -14,7 +14,6 @@ import { Signer, getSigner } from './Signer' import { DefaultContentTypes } from './types/DefaultContentType' import { hexToBytes } from './util' import * as XMTPModule from '../index' -import { Address } from '../index' declare const Buffer @@ -25,6 +24,7 @@ export type ExtractDecodedType = C extends XMTPModule.ContentCodec ? T : never export type InboxId = string & { readonly brand: unique symbol } +export type Address = string export class Client< ContentTypes extends DefaultContentTypes = DefaultContentTypes, @@ -34,7 +34,7 @@ export class Client< installationId: string dbPath: string conversations: Conversations - preferences: PrivatePreferences + zpreferences: PrivatePreferences codecRegistry: { [key: string]: XMTPModule.ContentCodec } private static signSubscription: Subscription | null = null private static authSubscription: Subscription | null = null diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index 1e9c5626a..0b8db1a18 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,5 +1,5 @@ import { ConsentState } from './ConsentListEntry' -import { ConversationSendPayload, MessagesOptions } from './types' +import { ConversationSendPayload, MessageId, MessagesOptions } from './types' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' import { DecodedMessage, Member, Dm, Group } from '../index' @@ -20,7 +20,7 @@ export interface ConversationBase { send( content: ConversationSendPayload - ): Promise + ): Promise sync() messages(opts?: MessagesOptions): Promise[]> streamMessages( @@ -36,4 +36,4 @@ export interface ConversationBase { export type Conversation< ContentTypes extends DefaultContentTypes = DefaultContentTypes, -> = Group | Dm \ No newline at end of file +> = Group | Dm diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index de0f8c218..352438712 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -6,7 +6,7 @@ import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' -import { MessagesOptions } from './types/MessagesOptions' +import { MessageId, MessagesOptions } from './types/MessagesOptions' import * as XMTP from '../index' import { ConversationId, ConversationTopic } from '../index' @@ -25,7 +25,7 @@ export class Dm id: ConversationId createdAt: number version = ConversationVersion.DM as const - topic: string + topic: ConversationTopic state: ConsentState lastMessage?: DecodedMessage @@ -60,7 +60,7 @@ export class Dm */ async send( content: ConversationSendPayload - ): Promise { + ): Promise { // TODO: Enable other content types // if (opts && opts.contentType) { // return await this._sendWithJSCodec(content, opts.contentType) diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 419a44beb..06612335c 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -6,7 +6,7 @@ import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' -import { MessagesOptions } from './types/MessagesOptions' +import { MessageId, MessagesOptions } from './types/MessagesOptions' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTP from '../index' import { Address, ConversationId, ConversationTopic } from '../index' @@ -87,7 +87,7 @@ export class Group< */ async send( content: ConversationSendPayload - ): Promise { + ): Promise { // TODO: Enable other content types // if (opts && opts.contentType) { // return await this._sendWithJSCodec(content, opts.contentType) diff --git a/src/lib/InboxState.ts b/src/lib/InboxState.ts index bff41bbb1..421230f7f 100644 --- a/src/lib/InboxState.ts +++ b/src/lib/InboxState.ts @@ -1,5 +1,4 @@ -import { InboxId } from './Client' -import { Address } from '../utils/address' +import { Address, InboxId } from './Client' export class InboxState { inboxId: InboxId diff --git a/src/lib/Member.ts b/src/lib/Member.ts index 7570eb4de..416431be6 100644 --- a/src/lib/Member.ts +++ b/src/lib/Member.ts @@ -1,6 +1,5 @@ -import { InboxId } from './Client' +import { Address, InboxId } from './Client' import { ConsentState } from './ConsentListEntry' -import { Address } from '../utils/address' export type PermissionLevel = 'member' | 'admin' | 'super_admin' diff --git a/src/lib/PrivatePreferences.ts b/src/lib/PrivatePreferences.ts index e0b4f2aa9..7bd355c1e 100644 --- a/src/lib/PrivatePreferences.ts +++ b/src/lib/PrivatePreferences.ts @@ -1,8 +1,8 @@ -import { Client, InboxId } from './Client' +import { Address, Client, InboxId } from './Client' import { ConsentListEntry, ConsentState } from './ConsentListEntry' import * as XMTPModule from '../index' import { ConversationId } from '../index' -import { Address, getAddress } from '../utils/address' +import { getAddress } from '../utils/address' export default class PrivatePreferences { client: Client @@ -11,7 +11,7 @@ export default class PrivatePreferences { this.client = client } - async consentConversationIdState( + async conversationIdConsentState( conversationId: ConversationId ): Promise { return await XMTPModule.consentConversationIdState( @@ -20,11 +20,11 @@ export default class PrivatePreferences { ) } - async consentInboxIdState(inboxId: InboxId): Promise { + async inboxIdConsentState(inboxId: InboxId): Promise { return await XMTPModule.consentInboxIdState(this.client.inboxId, inboxId) } - async consentAddressState(address: Address): Promise { + async addressConsentState(address: Address): Promise { return await XMTPModule.consentAddressState( this.client.inboxId, getAddress(address) diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index ac9b03879..d43c74350 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -3,3 +3,5 @@ export * from './SendOptions' export * from './ExtractDecodedType' export * from './ConversationCodecs' export * from './MessagesOptions' +export * from './ConversationOptions' +export * from './CreateGroupOptions' diff --git a/src/utils/address.ts b/src/utils/address.ts index ca15683e9..cbca82c70 100644 --- a/src/utils/address.ts +++ b/src/utils/address.ts @@ -4,8 +4,6 @@ import { TextEncoder } from 'text-encoding' const addressRegex = /^0x[a-fA-F0-9]{40}$/ const encoder = new TextEncoder() -export type Address = string - export function stringToBytes(value: string): Uint8Array { const bytes = encoder.encode(value) return bytes From 6371ad44b89a962f79a810eeb964d2be3e10f98a Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 10 Nov 2024 19:15:31 -0800 Subject: [PATCH 09/21] do the ios side --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 28 +- .../wrappers/DecodedMessageWrapper.kt | 2 +- example/ios/Podfile.lock | 14 +- ios/Wrappers/AuthParamsWrapper.swift | 8 +- .../ConversationContainerWrapper.swift | 32 - ios/Wrappers/ConversationWrapper.swift | 40 +- ios/Wrappers/DecodedMessageWrapper.swift | 6 +- ios/Wrappers/GroupWrapper.swift | 2 +- ios/XMTPModule.swift | 2470 +++++++---------- ios/XMTPReactNative.podspec | 2 +- 11 files changed, 985 insertions(+), 1621 deletions(-) delete mode 100644 ios/Wrappers/ConversationContainerWrapper.swift diff --git a/android/build.gradle b/android/build.gradle index bd15b1aa7..abd8db8f2 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:3.0.1" + implementation "org.xmtp:android:3.0.3" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 1a61f4790..b681025b8 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -433,18 +433,8 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val sortedGroupList = if (order == ConversationOrder.LAST_MESSAGE) { - client.conversations.listGroups() - .sortedByDescending { group -> - group.messages(limit = 1).firstOrNull()?.sent - } - .let { groups -> - if (limit != null && limit > 0) groups.take(limit) else groups - } - } else { - client.conversations.listGroups(limit = limit) - } - sortedGroupList.map { group -> + val groups = client.conversations.listGroups(order = order, limit = limit) + groups.map { group -> GroupWrapper.encode(client, group, params) } } @@ -471,18 +461,8 @@ class XMTPModule : Module() { val client = clients[inboxId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val sortedDmList = if (order == ConversationOrder.LAST_MESSAGE) { - client.conversations.listDms() - .sortedByDescending { dm -> - dm.messages(limit = 1).firstOrNull()?.sent - } - .let { dms -> - if (limit != null && limit > 0) dms.take(limit) else dms - } - } else { - client.conversations.listDms(limit = limit) - } - sortedDmList.map { dm -> + val dms = client.conversations.listDms(order = order, limit = limit) + dms.map { dm -> DmWrapper.encode(client, dm, params) } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt index d0f1662f7..6d51552cd 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/DecodedMessageWrapper.kt @@ -23,7 +23,7 @@ class DecodedMessageWrapper { "contentTypeId" to model.encodedContent.type.description, "content" to ContentJson(model.encodedContent).toJsonMap(), "senderAddress" to model.senderAddress, - "sentNs" to model.sent.time, + "sentNs" to model.sentNs, "fallback" to fallback, "deliveryStatus" to model.deliveryStatus.toString() ) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f9492c5c9..c48f3a48d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -56,7 +56,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (0.6.0) + - LibXMTP (3.0.0) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (2.0.0): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (3.0.0): + - XMTP (3.0.1): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.6.0) + - LibXMTP (= 3.0.0) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 3.0.0) + - XMTP (= 3.0.1) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 059c6d51b2c59419941ecff600aa586bbe083673 + LibXMTP: 4ef99026c3b353bd27195b48580e1bd34d083c3a Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 15d027733802cb4a0fed06529701ce920c0d2f17 - XMTPReactNative: 02ae4f694b984cd320d444281f9940e35b02de6b + XMTP: 00937113c4e6055980dcff9a51f8b70de83605aa + XMTPReactNative: 21011caca6d56cf3128b672fcba791b2009bb704 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/ios/Wrappers/AuthParamsWrapper.swift b/ios/Wrappers/AuthParamsWrapper.swift index e5fed3096..4a9ccfd1e 100644 --- a/ios/Wrappers/AuthParamsWrapper.swift +++ b/ios/Wrappers/AuthParamsWrapper.swift @@ -11,17 +11,15 @@ import XMTP struct AuthParamsWrapper { let environment: String let appVersion: String? - let enableV3: Bool let dbDirectory: String? let historySyncUrl: String? let walletType: WalletType let chainId: Int64? let blockNumber: Int64? - init(environment: String, appVersion: String?, enableV3: Bool, dbDirectory: String?, historySyncUrl: String?, walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { + init(environment: String, appVersion: String?, dbDirectory: String?, historySyncUrl: String?, walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { self.environment = environment self.appVersion = appVersion - self.enableV3 = enableV3 self.dbDirectory = dbDirectory self.historySyncUrl = historySyncUrl self.walletType = walletType @@ -32,12 +30,11 @@ struct AuthParamsWrapper { static func authParamsFromJson(_ authParams: String) -> AuthParamsWrapper { guard let data = authParams.data(using: .utf8), let jsonOptions = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - return AuthParamsWrapper(environment: "dev", appVersion: nil, enableV3: false, dbDirectory: nil, historySyncUrl: nil, walletType: WalletType.EOA, chainId: nil, blockNumber: nil) + return AuthParamsWrapper(environment: "dev", appVersion: nil, dbDirectory: nil, historySyncUrl: nil, walletType: WalletType.EOA, chainId: nil, blockNumber: nil) } let environment = jsonOptions["environment"] as? String ?? "dev" let appVersion = jsonOptions["appVersion"] as? String - let enableV3 = jsonOptions["enableV3"] as? Bool ?? false let dbDirectory = jsonOptions["dbDirectory"] as? String let historySyncUrl = jsonOptions["historySyncUrl"] as? String let walletTypeString = jsonOptions["walletType"] as? String ?? "EOA" @@ -56,7 +53,6 @@ struct AuthParamsWrapper { return AuthParamsWrapper( environment: environment, appVersion: appVersion, - enableV3: enableV3, dbDirectory: dbDirectory, historySyncUrl: historySyncUrl, walletType: walletType, diff --git a/ios/Wrappers/ConversationContainerWrapper.swift b/ios/Wrappers/ConversationContainerWrapper.swift deleted file mode 100644 index c670ae7d0..000000000 --- a/ios/Wrappers/ConversationContainerWrapper.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// ConversationContainerWrapper.swift -// XMTPReactNative -// -// Created by Naomi Plasterer on 2/14/24. -// - -import Foundation -import XMTP - -// Wrapper around XMTP.ConversationContainer to allow passing these objects back into react native. -struct ConversationContainerWrapper { - static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> [String: Any] { - switch conversation { - case .group(let group): - return try await GroupWrapper.encodeToObj(group, client: client) - case .dm(let dm): - return try await DmWrapper.encodeToObj(dm, client: client) - default: - return try ConversationWrapper.encodeToObj(conversation, client: client) - } - } - - static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> String { - let obj = try await encodeToObj(conversation, client: client) - let data = try JSONSerialization.data(withJSONObject: obj) - guard let result = String(data: data, encoding: .utf8) else { - throw WrapperError.encodeError("could not encode conversation") - } - return result - } -} diff --git a/ios/Wrappers/ConversationWrapper.swift b/ios/Wrappers/ConversationWrapper.swift index acddcc7b6..c4e3af3e5 100644 --- a/ios/Wrappers/ConversationWrapper.swift +++ b/ios/Wrappers/ConversationWrapper.swift @@ -1,41 +1,19 @@ -// -// ConversationWrapper.swift -// -// Created by Pat Nakajima on 4/21/23. -// - import Foundation import XMTP // Wrapper around XMTP.Conversation to allow passing these objects back into react native. struct ConversationWrapper { - static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client) throws -> [String: Any] { - var context = [:] as [String: Any] - if case let .v2(cv2) = conversation { - context = [ - "conversationID": cv2.context.conversationID, - "metadata": cv2.context.metadata, - ] + static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> [String: Any] { + switch conversation { + case .group(let group): + return try await GroupWrapper.encodeToObj(group, client: client) + case .dm(let dm): + return try await DmWrapper.encodeToObj(dm, client: client) } - var consentProof: String? = nil - if (conversation.consentProof != nil) { - consentProof = try conversation.consentProof?.serializedData().base64EncodedString() - } - return [ - "clientAddress": client.address, - "topic": conversation.topic, - "createdAt": UInt64(conversation.createdAt.timeIntervalSince1970 * 1000), - "context": context, - "peerAddress": try conversation.peerAddress, - "version": "DIRECT", - "conversationID": conversation.conversationID ?? "", - "keyMaterial": conversation.keyMaterial?.base64EncodedString() ?? "", - "consentProof": consentProof ?? "" - ] } - - static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) throws -> String { - let obj = try encodeToObj(conversation, client: client) + + static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> String { + let obj = try await encodeToObj(conversation, client: client) let data = try JSONSerialization.data(withJSONObject: obj) guard let result = String(data: data, encoding: .utf8) else { throw WrapperError.encodeError("could not encode conversation") diff --git a/ios/Wrappers/DecodedMessageWrapper.swift b/ios/Wrappers/DecodedMessageWrapper.swift index aa1b985b2..89b611009 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/DecodedMessageWrapper.swift @@ -4,7 +4,7 @@ import XMTP // Wrapper around XMTP.DecodedMessage to allow passing these objects back // into react native. struct DecodedMessageWrapper { - static func encodeToObj(_ model: XMTP.DecryptedMessage, client: Client) throws -> [String: Any] { + static func encodeToObj(_ model: XMTP.DecodedMessage, client: Client) throws -> [String: Any] { // Swift Protos don't support null values and will always put the default "" // Check if there is a fallback, if there is then make it the set fallback, if not null let fallback = model.encodedContent.hasFallback ? model.encodedContent.fallback : nil @@ -14,13 +14,13 @@ struct DecodedMessageWrapper { "contentTypeId": model.encodedContent.type.description, "content": try ContentJson.fromEncoded(model.encodedContent, client: client).toJsonMap() as Any, "senderAddress": model.senderAddress, - "sent": UInt64(model.sentAt.timeIntervalSince1970 * 1000), + "sentNs": model.sentNs, "fallback": fallback, "deliveryStatus": model.deliveryStatus.rawValue.uppercased(), ] } - static func encode(_ model: XMTP.DecryptedMessage, client: Client) throws -> String { + static func encode(_ model: XMTP.DecodedMessage, client: Client) throws -> String { let obj = try encodeToObj(model, client: client) return try obj.toJson() } diff --git a/ios/Wrappers/GroupWrapper.swift b/ios/Wrappers/GroupWrapper.swift index c02267232..bea29d63f 100644 --- a/ios/Wrappers/GroupWrapper.swift +++ b/ios/Wrappers/GroupWrapper.swift @@ -38,7 +38,7 @@ struct GroupWrapper { result["consentState"] = ConsentWrapper.consentStateToString(state: try group.consentState()) } if conversationParams.lastMessage { - if let lastMessage = try await group.decryptedMessages(limit: 1).first { + if let lastMessage = try await group.messages(limit: 1).first { result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) } } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 03ee59620..59126e8a7 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -1,7 +1,7 @@ import ExpoModulesCore -import XMTP import LibXMTP import OSLog +import XMTP extension Conversation { static func cacheKeyForTopic(inboxId: String, topic: String) -> String { @@ -11,24 +11,6 @@ extension Conversation { func cacheKey(_ inboxId: String) -> String { return Conversation.cacheKeyForTopic(inboxId: inboxId, topic: topic) } - - static func cacheKeyForV3(inboxId: String, topic: String, id: String) -> String { - return "\(inboxId):\(topic):\(id)" - } - - func cacheKeyV3(_ inboxId: String) throws -> String { - return try Conversation.cacheKeyForV3(inboxId: inboxId, topic: topic, id: id) - } -} - -extension XMTP.Group { - static func cacheKeyForId(inboxId: String, id: String) -> String { - return "\(inboxId):\(id)" - } - - func cacheKey(_ inboxId: String) -> String { - return XMTP.Group.cacheKeyForId(inboxId: inboxId, id: id) - } } actor IsolatedManager { @@ -46,11 +28,7 @@ actor IsolatedManager { public class XMTPModule: Module { var signer: ReactNativeSigner? let clientsManager = ClientsManager() - let conversationsManager = IsolatedManager() - let groupsManager = IsolatedManager() let subscriptionsManager = IsolatedManager>() - private var preEnableIdentityCallbackDeferred: DispatchSemaphore? - private var preCreateIdentityCallbackDeferred: DispatchSemaphore? private var preAuthenticateToInboxCallbackDeferred: DispatchSemaphore? actor ClientsManager { @@ -61,22 +39,22 @@ public class XMTPModule: Module { ContentJson.initCodecs(client: client) clients[key] = client } - - // A method to drop client for a given key from memory - func dropClient(key: String) { - clients[key] = nil - } + + // A method to drop client for a given key from memory + func dropClient(key: String) { + clients[key] = nil + } // A method to retrieve a client func getClient(key: String) -> XMTP.Client? { return clients[key] } - + // A method to disconnect all dbs func dropAllLocalDatabaseConnections() throws { for (_, client) in clients { // Call the drop method on each v3 client - if (!client.installationID.isEmpty) { + if !client.installationID.isEmpty { try client.dropLocalDatabaseConnection() } } @@ -86,7 +64,7 @@ public class XMTPModule: Module { func reconnectAllLocalDatabaseConnections() async throws { for (_, client) in clients { // Call the reconnect method on each v3 client - if (!client.installationID.isEmpty) { + if !client.installationID.isEmpty { try await client.reconnectLocalDatabase() } } @@ -94,35 +72,25 @@ public class XMTPModule: Module { } enum Error: Swift.Error { - case noClient, conversationNotFound(String), noMessage, invalidKeyBundle, invalidDigest, badPreparation(String), mlsNotEnabled(String), invalidString, invalidPermissionOption + case noClient + case conversationNotFound(String) + case noMessage, invalidKeyBundle, invalidDigest + case badPreparation(String) + case mlsNotEnabled(String) + case invalidString, invalidPermissionOption } public func definition() -> ModuleDefinition { Name("XMTP") Events( - // Auth - "sign", - "authed", - "authedV3", - "bundleAuthed", - "preCreateIdentityCallback", - "preEnableIdentityCallback", + "sign", + "authed", "preAuthenticateToInboxCallback", - // ConversationV2 - "conversation", - "conversationContainer", - "message", - "conversationMessage", - // ConversationV3 - "conversationV3", - "allConversationMessages", - "conversationV3Message", - // Group - "group", - "groupMessage", - "allGroupMessage" - ) + "conversation", + "message", + "conversationMessage" + ) AsyncFunction("address") { (inboxId: String) -> String in if let client = await clientsManager.getClient(key: inboxId) { @@ -131,7 +99,7 @@ public class XMTPModule: Module { return "No Client." } } - + AsyncFunction("inboxId") { (inboxId: String) -> String in if let client = await clientsManager.getClient(key: inboxId) { return client.inboxID @@ -139,390 +107,205 @@ public class XMTPModule: Module { return "No Client." } } - - AsyncFunction("findInboxIdFromAddress") { (inboxId: String, address: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("findInboxIdFromAddress") { + (inboxId: String, address: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } return try await client.inboxIdFromAddress(address: address) } AsyncFunction("deleteLocalDatabase") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } try client.deleteLocalDatabase() } - + AsyncFunction("dropLocalDatabaseConnection") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } try client.dropLocalDatabaseConnection() } - + AsyncFunction("reconnectLocalDatabase") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } try await client.reconnectLocalDatabase() } - + AsyncFunction("requestMessageHistorySync") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } try await client.requestMessageHistorySync() } - + AsyncFunction("revokeAllOtherInstallations") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let signer = ReactNativeSigner(module: self, address: client.address) + let signer = ReactNativeSigner( + module: self, address: client.address) self.signer = signer try await client.revokeAllOtherInstallations(signingKey: signer) self.signer = nil } - - AsyncFunction("getInboxState") { (inboxId: String, refreshFromNetwork: Bool) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("getInboxState") { + (inboxId: String, refreshFromNetwork: Bool) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let inboxState = try await client.inboxState(refreshFromNetwork: refreshFromNetwork) + let inboxState = try await client.inboxState( + refreshFromNetwork: refreshFromNetwork) return try InboxStateWrapper.encode(inboxState) } // // Auth functions // - AsyncFunction("auth") { (address: String, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) in - let signer = ReactNativeSigner(module: self, address: address) - self.signer = signer - if(hasCreateIdentityCallback ?? false) { - self.preCreateIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasEnableIdentityCallback ?? false) { - self.preEnableIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasAuthenticateToInboxCallback ?? false) { - self.preAuthenticateToInboxCallbackDeferred = DispatchSemaphore(value: 0) - } - let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil - let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil - let preAuthenticateToInboxCallback: PreEventCallback? = hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - - let options = self.createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preEnableIdentityCallback: preEnableIdentityCallback, - preCreateIdentityCallback: preCreateIdentityCallback, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: authOptions.enableV3, - dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl - ) - let client = try await XMTP.Client.create(account: signer, options: options) - await self.clientsManager.updateClient(key: client.inboxID, client: client) - self.signer = nil - self.sendEvent("authed", try ClientWrapper.encodeToObj(client)) - } - Function("receiveSignature") { (requestID: String, signature: String) in try signer?.handle(id: requestID, signature: signature) } - - Function("receiveSCWSignature") { (requestID: String, signature: String) in + + Function("receiveSCWSignature") { + (requestID: String, signature: String) in try signer?.handleSCW(id: requestID, signature: signature) } - // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { (hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in + AsyncFunction("createRandom") { + ( + hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8], + authParams: String + ) -> [String: String] in let privateKey = try PrivateKey.generate() - if(hasCreateIdentityCallback ?? false) { - preCreateIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasEnableIdentityCallback ?? false) { - preEnableIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasAuthenticateToInboxCallback ?? false) { - preAuthenticateToInboxCallbackDeferred = DispatchSemaphore(value: 0) - } - let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil - let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil - let preAuthenticateToInboxCallback: PreEventCallback? = hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) + if hasAuthenticateToInboxCallback ?? false { + preAuthenticateToInboxCallbackDeferred = DispatchSemaphore( + value: 0) + } + let preAuthenticateToInboxCallback: PreEventCallback? = + hasAuthenticateToInboxCallback ?? false + ? self.preAuthenticateToInboxCallback : nil + let encryptionKeyData = Data(dbEncryptionKey) let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) let options = createClientConfig( env: authOptions.environment, appVersion: authOptions.appVersion, - preEnableIdentityCallback: preEnableIdentityCallback, - preCreateIdentityCallback: preCreateIdentityCallback, preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: authOptions.enableV3, dbEncryptionKey: encryptionKeyData, dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl ) - let client = try await Client.create(account: privateKey, options: options) + let client = try await Client.create( + account: privateKey, options: options) - await clientsManager.updateClient(key: client.inboxID, client: client) + await clientsManager.updateClient( + key: client.inboxID, client: client) return try ClientWrapper.encodeToObj(client) } - // Create a client using its serialized key bundle. - AsyncFunction("createFromKeyBundle") { (keyBundle: String, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in - // V2 ONLY - do { - guard let keyBundleData = Data(base64Encoded: keyBundle), - let bundle = try? PrivateKeyBundle(serializedData: keyBundleData) - else { - throw Error.invalidKeyBundle - } - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - - let options = createClientConfig(env: authOptions.environment, appVersion: authOptions.appVersion, enableV3: authOptions.enableV3, dbEncryptionKey: encryptionKeyData, dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl) - let client = try await Client.from(bundle: bundle, options: options) - await clientsManager.updateClient(key: client.inboxID, client: client) - return try ClientWrapper.encodeToObj(client) - } catch { - print("ERROR! Failed to create client: \(error)") - throw error - } - } - - AsyncFunction("createFromKeyBundleWithSigner") { (address: String, keyBundle: String, dbEncryptionKey: [UInt8]?, authParams: String) in - // V2 ONLY - do { - guard let keyBundleData = Data(base64Encoded: keyBundle), - let bundle = try? PrivateKeyBundle(serializedData: keyBundleData) - else { - throw Error.invalidKeyBundle - } - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - - let signer = ReactNativeSigner(module: self, address: address) - self.signer = signer - - let options = createClientConfig(env: authOptions.environment, appVersion: authOptions.appVersion, enableV3: authOptions.enableV3, dbEncryptionKey: encryptionKeyData, dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl) - let client = try await Client.from(v1Bundle: bundle.v1, options: options, signingKey: signer) - await clientsManager.updateClient(key: client.inboxID, client: client) - self.signer = nil - self.sendEvent("bundleAuthed", try ClientWrapper.encodeToObj(client)) - } catch { - print("ERROR! Failed to create client: \(error)") - throw error - } - } - - AsyncFunction("createRandomV3") { (hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in - - let privateKey = try PrivateKey.generate() - if(hasCreateIdentityCallback ?? false) { - preCreateIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasEnableIdentityCallback ?? false) { - preEnableIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasAuthenticateToInboxCallback ?? false) { - preAuthenticateToInboxCallbackDeferred = DispatchSemaphore(value: 0) - } - let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil - let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil - let preAuthenticateToInboxCallback: PreEventCallback? = hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - - let options = createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preEnableIdentityCallback: preEnableIdentityCallback, - preCreateIdentityCallback: preCreateIdentityCallback, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: authOptions.enableV3, - dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl - ) - let client = try await Client.createV3(account: privateKey, options: options) - - await clientsManager.updateClient(key: client.inboxID, client: client) - return try ClientWrapper.encodeToObj(client) - } - - AsyncFunction("createV3") { (address: String, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?, hasAuthenticateToInboxCallback: Bool?, dbEncryptionKey: [UInt8]?, authParams: String) in + AsyncFunction("create") { + ( + address: String, hasAuthenticateToInboxCallback: Bool?, + dbEncryptionKey: [UInt8], authParams: String + ) in let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - let signer = ReactNativeSigner(module: self, address: address, walletType: authOptions.walletType, chainId: authOptions.chainId, blockNumber: authOptions.blockNumber) + let signer = ReactNativeSigner( + module: self, address: address, + walletType: authOptions.walletType, + chainId: authOptions.chainId, + blockNumber: authOptions.blockNumber) self.signer = signer - if(hasCreateIdentityCallback ?? false) { - self.preCreateIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasEnableIdentityCallback ?? false) { - self.preEnableIdentityCallbackDeferred = DispatchSemaphore(value: 0) - } - if(hasAuthenticateToInboxCallback ?? false) { - self.preAuthenticateToInboxCallbackDeferred = DispatchSemaphore(value: 0) + if hasAuthenticateToInboxCallback ?? false { + self.preAuthenticateToInboxCallbackDeferred = DispatchSemaphore( + value: 0) } - let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil - let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil - let preAuthenticateToInboxCallback: PreEventCallback? = hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - + let preAuthenticateToInboxCallback: PreEventCallback? = + hasAuthenticateToInboxCallback ?? false + ? self.preAuthenticateToInboxCallback : nil + let encryptionKeyData = Data(dbEncryptionKey) + let options = self.createClientConfig( env: authOptions.environment, appVersion: authOptions.appVersion, - preEnableIdentityCallback: preEnableIdentityCallback, - preCreateIdentityCallback: preCreateIdentityCallback, preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: authOptions.enableV3, dbEncryptionKey: encryptionKeyData, dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl ) - let client = try await XMTP.Client.createV3(account: signer, options: options) - await self.clientsManager.updateClient(key: client.inboxID, client: client) + let client = try await XMTP.Client.create( + account: signer, options: options) + await self.clientsManager.updateClient( + key: client.inboxID, client: client) self.signer = nil - self.sendEvent("authedV3", try ClientWrapper.encodeToObj(client)) + self.sendEvent("authed", try ClientWrapper.encodeToObj(client)) } - - AsyncFunction("buildV3") { (address: String, dbEncryptionKey: [UInt8]?, authParams: String) -> [String: String] in + + AsyncFunction("buildV3") { + (address: String, dbEncryptionKey: [UInt8], authParams: String) + -> [String: String] in let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - let encryptionKeyData = dbEncryptionKey == nil ? nil : Data(dbEncryptionKey!) - + let encryptionKeyData = Data(dbEncryptionKey) + let options = self.createClientConfig( env: authOptions.environment, appVersion: authOptions.appVersion, - preEnableIdentityCallback: preEnableIdentityCallback, - preCreateIdentityCallback: preCreateIdentityCallback, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: authOptions.enableV3, + preAuthenticateToInboxCallback: nil, dbEncryptionKey: encryptionKeyData, dbDirectory: authOptions.dbDirectory, historySyncUrl: authOptions.historySyncUrl ) - let client = try await XMTP.Client.buildV3(address: address, options: options) - await clientsManager.updateClient(key: client.inboxID, client: client) + let client = try await XMTP.Client.build( + address: address, options: options) + await clientsManager.updateClient( + key: client.inboxID, client: client) return try ClientWrapper.encodeToObj(client) } - - // Remove a client from memory for a given inboxId - AsyncFunction("dropClient") { (inboxId: String) in - await clientsManager.dropClient(key: inboxId) - } - - AsyncFunction("sign") { (inboxId: String, digest: [UInt8], keyType: String, preKeyIndex: Int) -> [UInt8] in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let privateKeyBundle = try client.keys - let key = keyType == "prekey" ? privateKeyBundle.preKeys[preKeyIndex] : privateKeyBundle.identityKey - - let privateKey = try PrivateKey(key) - let signature = try await privateKey.sign(Data(digest)) - let uint = try [UInt8](signature.serializedData()) - return uint - } - - AsyncFunction("exportPublicKeyBundle") { (inboxId: String) -> [UInt8] in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let bundle = try client.publicKeyBundle.serializedData() - return Array(bundle) - } - - // Export the client's serialized key bundle. - AsyncFunction("exportKeyBundle") { (inboxId: String) -> String in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let bundle = try client.privateKeyBundle.serializedData().base64EncodedString() - return bundle - } - - // Export the conversation's serialized topic data. - AsyncFunction("exportConversationTopicData") { (inboxId: String, topic: String) -> String in - // V2 ONLY - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - throw Error.conversationNotFound(topic) - } - return try conversation.toTopicData().serializedData().base64EncodedString() - } - - AsyncFunction("getHmacKeys") { (inboxId: String) -> [UInt8] in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let hmacKeys = await client.conversations.getHmacKeys() - - return try [UInt8](hmacKeys.serializedData()) - } - // Import a conversation from its serialized topic data. - AsyncFunction("importConversationTopicData") { (inboxId: String, topicData: String) -> String in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let data = try Xmtp_KeystoreApi_V1_TopicMap.TopicData( - serializedData: Data(base64Encoded: Data(topicData.utf8))! - ) - let conversation = try await client.conversations.importTopicData(data: data) - await conversationsManager.set(conversation.cacheKey(inboxId), conversation) - return try ConversationWrapper.encode(conversation, client: client) + // Remove a client from memory for a given inboxId + AsyncFunction("dropClient") { (inboxId: String) in + await clientsManager.dropClient(key: inboxId) } - // - // Client API - AsyncFunction("canMessage") { (inboxId: String, peerAddress: String) -> Bool in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - return try await client.canMessage(peerAddress) - } - - AsyncFunction("canGroupMessage") { (inboxId: String, peerAddresses: [String]) -> [String: Bool] in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("canMessage") { + (inboxId: String, peerAddresses: [String]) -> [String: Bool] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - return try await client.canMessageV3(addresses: peerAddresses) + return try await client.canMessage(addresses: peerAddresses) } - AsyncFunction("staticCanMessage") { (peerAddress: String, environment: String, appVersion: String?) -> Bool in - // V2 ONLY - do { - let options = createClientConfig(env: environment, appVersion: appVersion) - return try await XMTP.Client.canMessage(peerAddress, options: options) - } catch { - throw Error.noClient - } - } - - AsyncFunction("getOrCreateInboxId") { (address: String, environment: String) -> String in + AsyncFunction("getOrCreateInboxId") { + (address: String, environment: String) -> String in do { - let options = createClientConfig(env: environment, appVersion: nil) - return try await XMTP.Client.getOrCreateInboxId(options: options, address: address) + let api = createApiClient(env: environment) + return try await XMTP.Client.getOrCreateInboxId( + api: api, address: address) } catch { throw Error.noClient } } - AsyncFunction("encryptAttachment") { (inboxId: String, fileJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("encryptAttachment") { + (inboxId: String, fileJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } let file = try DecryptedLocalAttachment.fromJson(fileJson) @@ -538,7 +321,8 @@ public class XMTPModule: Module { codec: AttachmentCodec(), with: client ) - let encryptedFile = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let encryptedFile = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) try encrypted.payload.write(to: encryptedFile) return try EncryptedLocalAttachment.from( @@ -548,12 +332,16 @@ public class XMTPModule: Module { ).toJson() } - AsyncFunction("decryptAttachment") { (inboxId: String, encryptedFileJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("decryptAttachment") { + (inboxId: String, encryptedFileJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let encryptedFile = try EncryptedLocalAttachment.fromJson(encryptedFileJson) - let encryptedData = try Data(contentsOf: URL(string: encryptedFile.encryptedLocalFileUri)!) + let encryptedFile = try EncryptedLocalAttachment.fromJson( + encryptedFileJson) + let encryptedData = try Data( + contentsOf: URL(string: encryptedFile.encryptedLocalFileUri)!) let encrypted = EncryptedEncodedContent( secret: encryptedFile.metadata.secret, @@ -562,9 +350,11 @@ public class XMTPModule: Module { nonce: encryptedFile.metadata.nonce, payload: encryptedData ) - let encoded = try RemoteAttachment.decryptEncoded(encrypted: encrypted) + let encoded = try RemoteAttachment.decryptEncoded( + encrypted: encrypted) let attachment: Attachment = try encoded.decoded(with: client) - let file = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let file = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString) try attachment.data.write(to: file) return try DecryptedLocalAttachment( fileUri: file.absoluteString, @@ -573,185 +363,139 @@ public class XMTPModule: Module { ).toJson() } - AsyncFunction("sendEncodedContent") { (inboxId: String, topic: String, encodedContentData: [UInt8]) -> String in - // V2 ONLY - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - throw Error.conversationNotFound("no conversation found for \(topic)") + AsyncFunction("listGroups") { + ( + inboxId: String, groupParams: String?, sortOrder: String?, + limit: Int? + ) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient } - let encodedContent = try EncodedContent(serializedData: Data(encodedContentData)) - - return try await conversation.send(encodedContent: encodedContent) - } + let params = ConversationParamsWrapper.conversationParamsFromJson( + groupParams ?? "") + let order = getConversationSortOrder(order: sortOrder ?? "") - AsyncFunction("listConversations") { (inboxId: String) -> [String] in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } + var groupList: [Group] = try await client.conversations.listGroups( + limit: limit, order: order) - let conversations = try await client.conversations.list() - var results: [String] = [] - for conversation in conversations { - await self.conversationsManager.set(conversation.cacheKey(inboxId), conversation) - let encodedConversation = try ConversationWrapper.encode(conversation, client: client) - results.append(encodedConversation) + for group in groupList { + let encodedGroup = try await GroupWrapper.encode( + group, client: client, conversationParams: params) + results.append(encodedGroup) } - return results } - - AsyncFunction("listGroups") { (inboxId: String, groupParams: String?, sortOrder: String?, limit: Int?) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("listDms") { + ( + inboxId: String, groupParams: String?, sortOrder: String?, + limit: Int? + ) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?? "") + let params = ConversationParamsWrapper.conversationParamsFromJson( + groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - var groupList: [Group] = [] - - if order == .lastMessage { - let groups = try await client.conversations.groups() - var groupsWithMessages: [(Group, Date)] = [] - for group in groups { - do { - let firstMessage = try await group.decryptedMessages(limit: 1).first - let sentAt = firstMessage?.sentAt ?? Date.distantPast - groupsWithMessages.append((group, sentAt)) - } catch { - print("Failed to fetch messages for group: \(group.id)") - } - } - let sortedGroups = groupsWithMessages.sorted { $0.1 > $1.1 }.map { $0.0 } - - if let limit = limit, limit > 0 { - groupList = Array(sortedGroups.prefix(limit)) - } else { - groupList = sortedGroups - } - } else { - groupList = try await client.conversations.groups(limit: limit) - } + var dmList: [Dm] = try await client.conversations.listDms( + limit: limit, order: order) var results: [String] = [] - for group in groupList { - await self.groupsManager.set(group.cacheKey(inboxId), group) - let encodedGroup = try await GroupWrapper.encode(group, client: client, conversationParams: params) - results.append(encodedGroup) + for dm in dmList { + let encodedDm = try await DmWrapper.encode( + dm, client: client, conversationParams: params) + results.append(encodedDm) } return results } - - AsyncFunction("listV3Conversations") { (inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int?) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("listConversations") { + ( + inboxId: String, conversationParams: String?, + sortOrder: String?, limit: Int? + ) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?? "") + let params = ConversationParamsWrapper.conversationParamsFromJson( + conversationParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - let conversations = try await client.conversations.listConversations(limit: limit, order: order) - + let conversations = try await client.conversations.list( + limit: limit, order: order) + var results: [String] = [] for conversation in conversations { - let encodedConversationContainer = try await ConversationContainerWrapper.encode(conversation, client: client) - results.append(encodedConversationContainer) - } - return results - } - - AsyncFunction("listAll") { (inboxId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let conversationContainerList = try await client.conversations.list(includeGroups: true) - - var results: [String] = [] - for conversation in conversationContainerList { - await self.conversationsManager.set(conversation.cacheKey(inboxId), conversation) - let encodedConversationContainer = try await ConversationContainerWrapper.encode(conversation, client: client) + let encodedConversationContainer = + try await ConversationContainerWrapper.encode( + conversation, client: client) results.append(encodedConversationContainer) } - return results } - AsyncFunction("loadMessages") { (inboxId: String, topic: String, limit: Int?, before: Double?, after: Double?, direction: String?) -> [String] in - // V2 ONLY - let beforeDate = before != nil ? Date(timeIntervalSince1970: TimeInterval(before!) / 1000) : nil - let afterDate = after != nil ? Date(timeIntervalSince1970: TimeInterval(after!) / 1000) : nil - - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("conversationMessages") { + ( + inboxId: String, conversationId: String, limit: Int?, + beforeNs: Double?, afterNs: Double?, direction: String? + ) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - throw Error.conversationNotFound("no conversation found for \(topic)") + guard + let conversation = try client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") } - - let sortDirection: Int = (direction != nil && direction == "SORT_DIRECTION_ASCENDING") ? 1 : 2 - - let decryptedMessages = try await conversation.decryptedMessages( + let messages = try await conversation.messages( limit: limit, - before: beforeDate, - after: afterDate, - direction: PagingInfoSortDirection(rawValue: sortDirection) + beforeNs: (beforeNs != nil) ? Int64(beforeNs!) : nil, + afterNs: (afterNs != nil) ? Int64(afterNs!) : nil, + direction: getSortDirection( + direction: direction ?? "DESCENDING") ) - return decryptedMessages.compactMap { msg in + return messages.compactMap { msg in do { return try DecodedMessageWrapper.encode(msg, client: client) } catch { - print("discarding message, unable to encode wrapper \(msg.id)") + print( + "discarding message, unable to encode wrapper \(msg.id)" + ) return nil } } } - - AsyncFunction("conversationMessages") { (inboxId: String, conversationId: String, limit: Int?, before: Double?, after: Double?, direction: String?) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - let beforeDate = before != nil ? Date(timeIntervalSince1970: TimeInterval(before!) / 1000) : nil - let afterDate = after != nil ? Date(timeIntervalSince1970: TimeInterval(after!) / 1000) : nil - let sortDirection: Int = (direction != nil && direction == "SORT_DIRECTION_ASCENDING") ? 1 : 2 - - guard let conversation = try client.findConversation(conversationId: conversationId) else { - throw Error.conversationNotFound("no conversation found for \(conversationId)") - } - let decryptedMessages = try await conversation.decryptedMessages( - limit: limit, - before: beforeDate, - after: afterDate, - direction: PagingInfoSortDirection(rawValue: sortDirection) - ) - - return decryptedMessages.compactMap { msg in - do { - return try DecodedMessageWrapper.encode(msg, client: client) - } catch { - print("discarding message, unable to encode wrapper \(msg.id)") - return nil - } - } - } - - AsyncFunction("findV3Message") { (inboxId: String, messageId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("findMessage") { + (inboxId: String, messageId: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } if let message = try client.findMessage(messageId: messageId) { - return try DecodedMessageWrapper.encode(message.decrypt(), client: client) + return try DecodedMessageWrapper.encode( + message.decrypt(), client: client) } else { return nil } } - AsyncFunction("findGroup") { (inboxId: String, groupId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("findGroup") { + (inboxId: String, groupId: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } if let group = try client.findGroup(groupId: groupId) { @@ -760,32 +504,44 @@ public class XMTPModule: Module { return nil } } - - AsyncFunction("findConversation") { (inboxId: String, conversationId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("findConversation") { + (inboxId: String, conversationId: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - if let conversation = try client.findConversation(conversationId: conversationId) { - return try await ConversationContainerWrapper.encode(conversation, client: client) + if let conversation = try client.findConversation( + conversationId: conversationId) + { + return try await ConversationContainerWrapper.encode( + conversation, client: client) } else { return nil } } - - AsyncFunction("findConversationByTopic") { (inboxId: String, topic: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("findConversationByTopic") { + (inboxId: String, topic: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - if let conversation = try client.findConversationByTopic(topic: topic) { - return try await ConversationContainerWrapper.encode(conversation, client: client) + if let conversation = try client.findConversationByTopic( + topic: topic) + { + return try await ConversationContainerWrapper.encode( + conversation, client: client) } else { return nil } } - - AsyncFunction("findDm") { (inboxId: String, peerAddress: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("findDm") { + (inboxId: String, peerAddress: String) -> String? in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } if let dm = try await client.findDm(address: peerAddress) { @@ -795,69 +551,18 @@ public class XMTPModule: Module { } } - AsyncFunction("loadBatchMessages") { (inboxId: String, topics: [String]) -> [String] in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("sendMessage") { + (inboxId: String, id: String, contentJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - var topicsList: [String: Pagination?] = [:] - topics.forEach { topicJSON in - let jsonData = topicJSON.data(using: .utf8)! - guard let jsonObj = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any], - let topic = jsonObj["topic"] as? String - else { - return // Skip this topic if it doesn't have valid JSON data or missing "topic" field - } - - var limit: Int? - var before: Double? - var after: Double? - var direction: PagingInfoSortDirection = .descending - - if let limitInt = jsonObj["limit"] as? Int { - limit = limitInt - } - - if let beforeInt = jsonObj["before"] as? Double { - before = TimeInterval(beforeInt / 1000) - } - - if let afterInt = jsonObj["after"] as? Double { - after = TimeInterval(afterInt / 1000) - } - - if let directionStr = jsonObj["direction"] as? String { - let sortDirection: Int = (directionStr == "SORT_DIRECTION_ASCENDING") ? 1 : 2 - direction = PagingInfoSortDirection(rawValue: sortDirection) ?? .descending - } - - let page = Pagination( - limit: limit ?? nil, - before: before != nil && before! > 0 ? Date(timeIntervalSince1970: before!) : nil, - after: after != nil && after! > 0 ? Date(timeIntervalSince1970: after!) : nil, - direction: direction - ) - - topicsList[topic] = page - } - - let decodedMessages = try await client.conversations.listBatchDecryptedMessages(topics: topicsList) - - return decodedMessages.compactMap { msg in - do { - return try DecodedMessageWrapper.encode(msg, client: client) - } catch { - print("discarding message, unable to encode wrapper \(msg.id)") - return nil - } - } - } - - AsyncFunction("sendMessage") { (inboxId: String, conversationTopic: String, contentJson: String) -> String in - // V2 ONLY - guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { - throw Error.conversationNotFound("no conversation found for \(conversationTopic)") + guard + let conversation = try client.findConversation( + conversationId: id) + else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } let sending = try ContentJson.fromJson(contentJson) @@ -866,168 +571,69 @@ public class XMTPModule: Module { options: SendOptions(contentType: sending.type) ) } - - AsyncFunction("sendMessageToConversation") { (inboxId: String, id: String, contentJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("publishPreparedMessages") { + (inboxId: String, id: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try client.findConversation(conversationId: id) else { - throw Error.conversationNotFound("no conversation found for \(id)") - } - - let sending = try ContentJson.fromJson(contentJson) - return try await conversation.send( - content: sending.content, - options: SendOptions(contentType: sending.type) - ) - } - - AsyncFunction("publishPreparedGroupMessages") { (inboxId: String, id: String) in - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard + let conversation = try client.findConversation( + conversationId: id) + else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - try await group.publishMessages() + try await conversation.publishMessages() } - AsyncFunction("prepareConversationMessage") { (inboxId: String, id: String, contentJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("prepareMessage") { + (inboxId: String, id: String, contentJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try client.findConversation(conversationId: id) else { - throw Error.conversationNotFound("no conversation found for \(id)") + guard + let conversation = try client.findConversation( + conversationId: id) + else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } let sending = try ContentJson.fromJson(contentJson) - return try await conversation.prepareMessageV3( + return try await conversation.prepareMessage( content: sending.content, options: SendOptions(contentType: sending.type) ) } - AsyncFunction("prepareMessage") { ( - inboxId: String, - conversationTopic: String, - contentJson: String - ) -> String in - // V2 ONLY - guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { - throw Error.conversationNotFound("no conversation found for \(conversationTopic)") - } - let sending = try ContentJson.fromJson(contentJson) - let prepared = try await conversation.prepareMessage( - content: sending.content, - options: SendOptions(contentType: sending.type) - ) - let preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 - let preparedData = try prepared.serializedData() - let preparedFile = FileManager.default.temporaryDirectory.appendingPathComponent(prepared.messageID) - try preparedData.write(to: preparedFile) - return try PreparedLocalMessage( - messageId: prepared.messageID, - preparedFileUri: preparedFile.absoluteString, - preparedAt: preparedAtMillis - ).toJson() - } - - AsyncFunction("prepareEncodedMessage") { ( - inboxId: String, - conversationTopic: String, - encodedContentData: [UInt8] - ) -> String in - // V2 ONLY - guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { - throw Error.conversationNotFound("no conversation found for \(conversationTopic)") - } - let encodedContent = try EncodedContent(serializedData: Data(encodedContentData)) - - let prepared = try await conversation.prepareMessage( - encodedContent: encodedContent - ) - let preparedAtMillis = prepared.envelopes[0].timestampNs / 1_000_000 - let preparedData = try prepared.serializedData() - let preparedFile = FileManager.default.temporaryDirectory.appendingPathComponent(prepared.messageID) - try preparedData.write(to: preparedFile) - return try PreparedLocalMessage( - messageId: prepared.messageID, - preparedFileUri: preparedFile.absoluteString, - preparedAt: preparedAtMillis - ).toJson() - } - - AsyncFunction("sendPreparedMessage") { (inboxId: String, preparedLocalMessageJson: String) -> String in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - guard let local = try? PreparedLocalMessage.fromJson(preparedLocalMessageJson) else { - throw Error.badPreparation("bad prepared local message") - } - guard let preparedFileUrl = URL(string: local.preparedFileUri) else { - throw Error.badPreparation("bad prepared local message URI \(local.preparedFileUri)") - } - guard let preparedData = try? Data(contentsOf: preparedFileUrl) else { - throw Error.badPreparation("unable to load local message file") - } - guard let prepared = try? PreparedMessage.fromSerializedData(preparedData) else { - throw Error.badPreparation("unable to deserialized \(local.preparedFileUri)") - } - try await client.publish(envelopes: prepared.envelopes) - do { - try FileManager.default.removeItem(at: URL(string: local.preparedFileUri)!) - } catch { /* ignore: the sending succeeds even if we fail to rm the tmp file afterward */ } - return prepared.messageID - } - - AsyncFunction("createConversation") { (inboxId: String, peerAddress: String, contextJson: String, consentProofBytes: [UInt8]) -> String in - // V2 ONLY - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - do { - let contextData = contextJson.data(using: .utf8)! - let contextObj = (try? JSONSerialization.jsonObject(with: contextData) as? [String: Any]) ?? [:] - var consentProofData:ConsentProofPayload? - if consentProofBytes.count != 0 { - do { - consentProofData = try ConsentProofPayload(serializedData: Data(consentProofBytes)) - } catch { - print("Error: \(error)") - } - } - let conversation = try await client.conversations.newConversation( - with: peerAddress, - context: .init( - conversationID: contextObj["conversationID"] as? String ?? "", - metadata: contextObj["metadata"] as? [String: String] ?? [:] as [String: String] - ), - consentProofPayload:consentProofData - ) - - return try ConversationWrapper.encode(conversation, client: client) - } catch { - print("ERRRO!: \(error.localizedDescription)") - throw error - } - } - - AsyncFunction("findOrCreateDm") { (inboxId: String, peerAddress: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("findOrCreateDm") { + (inboxId: String, peerAddress: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } do { - let dm = try await client.conversations.findOrCreateDm(with: peerAddress) + let dm = try await client.conversations.findOrCreateDm( + with: peerAddress) return try await DmWrapper.encode(dm, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") throw error } } - - AsyncFunction("createGroup") { (inboxId: String, peerAddresses: [String], permission: String, groupOptionsJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("createGroup") { + ( + inboxId: String, peerAddresses: [String], permission: String, + groupOptionsJson: String + ) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } let permissionLevel: GroupPermissionPreconfiguration = { @@ -1039,13 +645,15 @@ public class XMTPModule: Module { } }() do { - let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + let createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson( + groupOptionsJson) let group = try await client.conversations.newGroup( - with: peerAddresses, - permissions: permissionLevel, - name: createGroupParams.groupName, - imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription, + with: peerAddresses, + permissions: permissionLevel, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl ) return try await GroupWrapper.encode(group, client: client) @@ -1055,546 +663,666 @@ public class XMTPModule: Module { } } - AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionPolicySetJson: String, groupOptionsJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("createGroupCustomPermissions") { + ( + inboxId: String, peerAddresses: [String], + permissionPolicySetJson: String, groupOptionsJson: String + ) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } do { - let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) - let permissionPolicySet = try PermissionPolicySetWrapper.createPermissionPolicySet(from: permissionPolicySetJson) - let group = try await client.conversations.newGroupCustomPermissions( - with: peerAddresses, - permissionPolicySet: permissionPolicySet, - name: createGroupParams.groupName, - imageUrlSquare: createGroupParams.groupImageUrlSquare, - description: createGroupParams.groupDescription, - pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl - ) + let createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson( + groupOptionsJson) + let permissionPolicySet = + try PermissionPolicySetWrapper.createPermissionPolicySet( + from: permissionPolicySetJson) + let group = try await client.conversations + .newGroupCustomPermissions( + with: peerAddresses, + permissionPolicySet: permissionPolicySet, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, + pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl + ) return try await GroupWrapper.encode(group, client: client) } catch { print("ERRRO!: \(error.localizedDescription)") throw error } } - - AsyncFunction("listMemberInboxIds") { (inboxId: String, groupId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("listMemberInboxIds") { + (inboxId: String, groupId: String) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: groupId) else { - throw Error.conversationNotFound("no group found for \(groupId)") + guard let group = try client.findGroup(groupId: groupId) else { + throw Error.conversationNotFound( + "no conversation found for \(groupId)") } return try await group.members.map(\.inboxId) } - - AsyncFunction("dmPeerInboxId") { (inboxId: String, dmId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("dmPeerInboxId") { + (inboxId: String, dmId: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try client.findConversation(conversationId: dmId) else { - throw Error.conversationNotFound("no conversation found for \(dmId)") + guard + let conversation = try client.findConversation( + conversationId: dmId) + else { + throw Error.conversationNotFound( + "no conversation found for \(dmId)") } if case let .dm(dm) = conversation { return try await dm.peerInboxId } else { - throw Error.conversationNotFound("no conversation found for \(dmId)") + throw Error.conversationNotFound( + "no conversation found for \(dmId)") } } - - AsyncFunction("listConversationMembers") { (inboxId: String, conversationId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("listConversationMembers") { + (inboxId: String, conversationId: String) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try client.findConversation(conversationId: conversationId) else { - throw Error.conversationNotFound("no conversation found for \(conversationId)") + guard + let conversation = try client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") } return try await conversation.members().compactMap { member in return try MemberWrapper.encode(member) } } - - + AsyncFunction("syncConversations") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } try await client.conversations.sync() } - + AsyncFunction("syncAllConversations") { (inboxId: String) -> UInt32 in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } return try await client.conversations.syncAllConversations() } AsyncFunction("syncConversation") { (inboxId: String, id: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let conversation = try client.findConversation(conversationId: id) else { - throw Error.conversationNotFound("no conversation found for \(id)") + guard + let conversation = try client.findConversation( + conversationId: id) + else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await conversation.sync() } - AsyncFunction("addGroupMembers") { (inboxId: String, id: String, peerAddresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("addGroupMembers") { + (inboxId: String, id: String, peerAddresses: [String]) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.addMembers(addresses: peerAddresses) } - AsyncFunction("removeGroupMembers") { (inboxId: String, id: String, peerAddresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("removeGroupMembers") { + (inboxId: String, id: String, peerAddresses: [String]) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - try await group.removeMembers(addresses: peerAddresses) } - - AsyncFunction("addGroupMembersByInboxId") { (inboxId: String, id: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("addGroupMembersByInboxId") { + (inboxId: String, id: String, inboxIds: [String]) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.addMembersByInboxId(inboxIds: inboxIds) } - AsyncFunction("removeGroupMembersByInboxId") { (inboxId: String, id: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("removeGroupMembersByInboxId") { + (inboxId: String, id: String, inboxIds: [String]) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.removeMembersByInboxId(inboxIds: inboxIds) } AsyncFunction("groupName") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.groupName() } - AsyncFunction("updateGroupName") { (inboxId: String, id: String, groupName: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateGroupName") { + (inboxId: String, id: String, groupName: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.updateGroupName(groupName: groupName) } - - AsyncFunction("groupImageUrlSquare") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("groupImageUrlSquare") { + (inboxId: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.groupImageUrlSquare() } - AsyncFunction("updateGroupImageUrlSquare") { (inboxId: String, id: String, groupImageUrl: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateGroupImageUrlSquare") { + (inboxId: String, id: String, groupImageUrl: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - try await group.updateGroupImageUrlSquare(imageUrlSquare: groupImageUrl) + try await group.updateGroupImageUrlSquare( + imageUrlSquare: groupImageUrl) } - - AsyncFunction("groupDescription") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("groupDescription") { + (inboxId: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.groupDescription() } - AsyncFunction("updateGroupDescription") { (inboxId: String, id: String, description: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateGroupDescription") { + (inboxId: String, id: String, description: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - try await group.updateGroupDescription(groupDescription: description) + try await group.updateGroupDescription( + groupDescription: description) } - AsyncFunction("groupPinnedFrameUrl") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("groupPinnedFrameUrl") { + (inboxId: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.groupPinnedFrameUrl() } - AsyncFunction("updateGroupPinnedFrameUrl") { (inboxId: String, id: String, pinnedFrameUrl: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateGroupPinnedFrameUrl") { + (inboxId: String, id: String, pinnedFrameUrl: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - try await group.updateGroupPinnedFrameUrl(groupPinnedFrameUrl: pinnedFrameUrl) + try await group.updateGroupPinnedFrameUrl( + groupPinnedFrameUrl: pinnedFrameUrl) } - - AsyncFunction("isGroupActive") { (inboxId: String, id: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("isGroupActive") { + (inboxId: String, id: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - + return try group.isActive() } - AsyncFunction("addedByInboxId") { (inboxId: String, id: String) -> String in - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + AsyncFunction("addedByInboxId") { + (inboxId: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient } - + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + return try group.addedByInboxId() } - AsyncFunction("creatorInboxId") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("creatorInboxId") { + (inboxId: String, id: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - return try group.creatorInboxId() } - AsyncFunction("isAdmin") { (clientInboxId: String, id: String, inboxId: String) -> Bool in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("isAdmin") { + (clientInboxId: String, id: String, inboxId: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.isAdmin(inboxId: inboxId) } - AsyncFunction("isSuperAdmin") { (clientInboxId: String, id: String, inboxId: String) -> Bool in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("isSuperAdmin") { + (clientInboxId: String, id: String, inboxId: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.isSuperAdmin(inboxId: inboxId) } - AsyncFunction("listAdmins") { (inboxId: String, id: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("listAdmins") { + (inboxId: String, id: String) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.listAdmins() } - AsyncFunction("listSuperAdmins") { (inboxId: String, id: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("listSuperAdmins") { + (inboxId: String, id: String) -> [String] in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } return try group.listSuperAdmins() } - AsyncFunction("addAdmin") { (clientInboxId: String, id: String, inboxId: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("addAdmin") { + (clientInboxId: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.addAdmin(inboxId: inboxId) } - AsyncFunction("addSuperAdmin") { (clientInboxId: String, id: String, inboxId: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("addSuperAdmin") { + (clientInboxId: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.addSuperAdmin(inboxId: inboxId) } - AsyncFunction("removeAdmin") { (clientInboxId: String, id: String, inboxId: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("removeAdmin") { + (clientInboxId: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.removeAdmin(inboxId: inboxId) } - AsyncFunction("removeSuperAdmin") { (clientInboxId: String, id: String, inboxId: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("removeSuperAdmin") { + (clientInboxId: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } try await group.removeSuperAdmin(inboxId: inboxId) } - - AsyncFunction("updateAddMemberPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateAddMemberPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateRemoveMemberPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateRemoveMemberPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateAddAdminPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateAddAdminPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateRemoveAdminPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateRemoveAdminPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateGroupNamePermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateGroupNamePermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateGroupImageUrlSquarePermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateGroupImageUrlSquarePermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateGroupDescriptionPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateGroupDescriptionPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("updateGroupPinnedFrameUrlPermission") { (clientInboxId: String, id: String, newPermission: String) in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { - throw Error.conversationNotFound("no group found for \(id)") - } - try await group.updateGroupPinnedFrameUrlPermission(newPermissionOption: getPermissionOption(permission: newPermission)) - } - - AsyncFunction("permissionPolicySet") { (inboxId: String, id: String) async throws -> String in - - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - throw Error.conversationNotFound("Permission policy set not found for group: \(id)") - } - - let permissionPolicySet = try group.permissionPolicySet() - - return try PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) - } - - - - AsyncFunction("processConversationMessage") { (inboxId: String, id: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let conversation = try client.findConversation(conversationId: id) else { - throw Error.conversationNotFound("no conversation found for \(id)") - } - - guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { - throw Error.noMessage + + AsyncFunction("updateAddMemberPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient } - let decodedMessage = try await conversation.processMessage(envelopeBytes: encryptedMessageData) - return try DecodedMessageWrapper.encode(decodedMessage.decrypt(), client: client) + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateAddMemberPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) } - AsyncFunction("processWelcomeMessage") { (inboxId: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateRemoveMemberPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { throw Error.noClient } - guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { - throw Error.noMessage + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - guard let group = try await client.conversations.fromWelcome(envelopeBytes: encryptedMessageData) else { - throw Error.conversationNotFound("no group found") + try await group.updateRemoveMemberPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) + } + + AsyncFunction("updateAddAdminPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateAddAdminPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) + } - return try await GroupWrapper.encode(group, client: client) + AsyncFunction("updateRemoveAdminPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient + } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateRemoveAdminPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) } - - AsyncFunction("processConversationWelcomeMessage") { (inboxId: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { + + AsyncFunction("updateGroupNamePermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { throw Error.noClient } - guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { - throw Error.noMessage + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") } - guard let conversation = try await client.conversations.conversationFromWelcome(envelopeBytes: encryptedMessageData) else { - throw Error.conversationNotFound("no group found") + try await group.updateGroupNamePermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) + } + + AsyncFunction("updateGroupImageUrlSquarePermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateGroupImageUrlSquarePermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) + } - return try await ConversationContainerWrapper.encode(conversation, client: client) + AsyncFunction("updateGroupDescriptionPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient + } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateGroupDescriptionPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) } - AsyncFunction("subscribeToConversations") { (inboxId: String) in - // V2 ONLY - try await subscribeToConversations(inboxId: inboxId) + AsyncFunction("updateGroupPinnedFrameUrlPermission") { + (clientInboxId: String, id: String, newPermission: String) in + guard + let client = await clientsManager.getClient(key: clientInboxId) + else { + throw Error.noClient + } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + try await group.updateGroupPinnedFrameUrlPermission( + newPermissionOption: getPermissionOption( + permission: newPermission)) } - AsyncFunction("subscribeToAllMessages") { (inboxId: String, includeGroups: Bool) in - try await subscribeToAllMessages(inboxId: inboxId, includeGroups: includeGroups) + AsyncFunction("permissionPolicySet") { + (inboxId: String, id: String) async throws -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + guard let group = try client.findGroup(groupId: id) else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + + let permissionPolicySet = try group.permissionPolicySet() + + return try PermissionPolicySetWrapper.encodeToJsonString( + permissionPolicySet) } - - AsyncFunction("subscribeToAllGroupMessages") { (inboxId: String) in - try await subscribeToAllGroupMessages(inboxId: inboxId) + + AsyncFunction("processMessage") { + (inboxId: String, id: String, encryptedMessage: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + + guard + let conversation = try client.findConversation( + conversationId: id) + else { + throw Error.conversationNotFound( + "no conversation found for \(id)") + } + + guard + let encryptedMessageData = Data( + base64Encoded: Data(encryptedMessage.utf8)) + else { + throw Error.noMessage + } + let decodedMessage = try await conversation.processMessage( + envelopeBytes: encryptedMessageData) + return try DecodedMessageWrapper.encode( + decodedMessage.decrypt(), client: client) } - AsyncFunction("subscribeToMessages") { (inboxId: String, topic: String) in - // V2 ONLY - try await subscribeToMessages(inboxId: inboxId, topic: topic) + AsyncFunction("processWelcomeMessage") { + (inboxId: String, encryptedMessage: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { + throw Error.noClient + } + guard + let encryptedMessageData = Data( + base64Encoded: Data(encryptedMessage.utf8)) + else { + throw Error.noMessage + } + guard + let conversation = try await client.conversations.fromWelcome( + envelopeBytes: encryptedMessageData) + else { + throw Error.conversationNotFound("no group found") + } + + return try await ConversationContainerWrapper.encode( + conversation, client: client) } - - AsyncFunction("subscribeToGroups") { (inboxId: String) in - try await subscribeToGroups(inboxId: inboxId) + + AsyncFunction("subscribeToConversations") { (inboxId: String) in + try await subscribeToConversations(inboxId: inboxId) } - - AsyncFunction("subscribeToAll") { (inboxId: String) in - try await subscribeToAll(inboxId: inboxId) + + AsyncFunction("subscribeToAllMessages") { + (inboxId: String, includeGroups: Bool) in + try await subscribeToAllMessages( + inboxId: inboxId, includeGroups: includeGroups) } - AsyncFunction("subscribeToGroupMessages") { (inboxId: String, id: String) in - try await subscribeToGroupMessages(inboxId: inboxId, id: id) + AsyncFunction("subscribeToMessages") { + (inboxId: String, topic: String) in + try await subscribeToMessages(inboxId: inboxId, topic: topic) } AsyncFunction("unsubscribeFromConversations") { (inboxId: String) in - // V2 ONLY - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() + await subscriptionsManager.get( + getConversationsKey(inboxId: inboxId))?.cancel() } AsyncFunction("unsubscribeFromAllMessages") { (inboxId: String) in - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))?.cancel() - } - - AsyncFunction("unsubscribeFromAllGroupMessages") { (inboxId: String) in - await subscriptionsManager.get(getGroupMessagesKey(inboxId: inboxId))?.cancel() + await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))? + .cancel() } - - AsyncFunction("unsubscribeFromMessages") { (inboxId: String, topic: String) in - // V2 ONLY + AsyncFunction("unsubscribeFromMessages") { + (inboxId: String, topic: String) in try await unsubscribeFromMessages(inboxId: inboxId, topic: topic) } - - AsyncFunction("unsubscribeFromGroupMessages") { (inboxId: String, id: String) in - try await unsubscribeFromGroupMessages(inboxId: inboxId, id: id) - } - - AsyncFunction("unsubscribeFromGroups") { (inboxId: String) in - await subscriptionsManager.get(getGroupsKey(inboxId: inboxId))?.cancel() - } - AsyncFunction("registerPushToken") { (pushServer: String, token: String) in + AsyncFunction("registerPushToken") { + (pushServer: String, token: String) in XMTPPush.shared.setPushServer(pushServer) do { try await XMTPPush.shared.register(token: token) @@ -1603,164 +1331,90 @@ public class XMTPModule: Module { } } - AsyncFunction("subscribePushTopics") { (inboxId: String, topics: [String]) in + AsyncFunction("subscribePushTopics") { (topics: [String]) in do { - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let hmacKeysResult = await client.conversations.getHmacKeys() - let subscriptions = topics.map { topic -> NotificationSubscription in - let hmacKeys = hmacKeysResult.hmacKeys - - let result = hmacKeys[topic]?.values.map { hmacKey -> NotificationSubscriptionHmacKey in - NotificationSubscriptionHmacKey.with { sub_key in - sub_key.key = hmacKey.hmacKey - sub_key.thirtyDayPeriodsSinceEpoch = UInt32(hmacKey.thirtyDayPeriodsSinceEpoch) - } - } - + let subscriptions = topics.map { + topic -> NotificationSubscription in return NotificationSubscription.with { sub in - sub.hmacKeys = result ?? [] sub.topic = topic } } - try await XMTPPush.shared.subscribeWithMetadata(subscriptions: subscriptions) + try await XMTPPush.shared.subscribeWithMetadata( + subscriptions: subscriptions) } catch { print("Error subscribing: \(error)") } } - AsyncFunction("decodeMessage") { (inboxId: String, topic: String, encryptedMessage: String) -> String in - // V2 ONLY - guard let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) else { - throw Error.noMessage - } - - let envelope = XMTP.Envelope.with { envelope in - envelope.message = encryptedMessageData - envelope.contentTopic = topic - } - - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - throw Error.conversationNotFound("no conversation found for \(topic)") - } - let decodedMessage = try conversation.decrypt(envelope) - return try DecodedMessageWrapper.encode(decodedMessage, client: client) - } - - AsyncFunction("isAllowed") { (inboxId: String, address: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - return try await client.contacts.isAllowed(address) - } - - AsyncFunction("isDenied") { (inboxId: String, address: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - return try await client.contacts.isDenied(address) - } - - AsyncFunction("denyContacts") { (inboxId: String, addresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("consentAddressState") { + (inboxId: String, address: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - try await client.contacts.deny(addresses: addresses) + return try ConsentWrapper.consentStateToString( + state: client.preferences.consentList.addressState(address)) } - AsyncFunction("allowContacts") { (inboxId: String, addresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("consentInboxIdState") { + (inboxId: String, inboxId: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - try await client.contacts.allow(addresses: addresses) - } - - AsyncFunction("isInboxAllowed") { (clientInboxId: String, inboxId: String) -> Bool in - guard let client = await clientsManager.getClient(key: clientInboxId) else { - throw Error.noClient - } - return try await client.contacts.isInboxAllowed(inboxId: inboxId) + return try ConsentWrapper.consentStateToString( + state: client.preferences.consentList.inboxIdState(inboxId)) } - AsyncFunction("isInboxDenied") { (clientInboxId: String,inboxId: String) -> Bool in - guard let client = await clientsManager.getClient(key: clientInboxId) else { + AsyncFunction("consentConversationIdState") { + (inboxId: String, conversationId: String) -> Bool in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - return try await client.contacts.isInboxDenied(inboxId: inboxId) + return try ConsentWrapper.consentStateToString( + state: client.preferences.consentList.conversationState( + conversationId)) } - AsyncFunction("denyInboxes") { (inboxId: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("setConsentState") { + ( + inboxId: String, value: String, entryType: String, + consentType: String + ) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - try await client.contacts.denyInboxes(inboxIds: inboxIds) - } + client.preferences.consentList.setConsentState( + [ + ConsentListEntry( + value, + getEntryType(entryType), + getConsentState(consentType) + ) - AsyncFunction("allowInboxes") { (inboxId: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - try await client.contacts.allowInboxes(inboxIds: inboxIds) - } - - AsyncFunction("refreshConsentList") { (inboxId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - let consentList = try await client.contacts.refreshConsentList() - - return try await consentList.entriesManager.map.compactMap { entry in - try ConsentWrapper.encode(entry.value) - } - } - - AsyncFunction("conversationConsentState") { (inboxId: String, conversationTopic: String) -> String in - guard let conversation = try await findConversation(inboxId: inboxId, topic: conversationTopic) else { - throw Error.conversationNotFound(conversationTopic) - } - return try ConsentWrapper.consentStateToString(state: await conversation.consentState()) - } - - AsyncFunction("conversationV3ConsentState") { (inboxId: String, conversationId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let conversation = try client.findConversation(conversationId: conversationId) else { - throw Error.conversationNotFound("no conversation found for \(conversationId)") - } - return try ConsentWrapper.consentStateToString(state: await conversation.consentState()) + ] + ) } - AsyncFunction("consentList") { (inboxId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("conversationConsentState") { + (inboxId: String, conversationId: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - let entries = await client.contacts.consentList.entriesManager.map - return try entries.compactMap { entry in - try ConsentWrapper.encode(entry.value) - } - } - - Function("preEnableIdentityCallbackCompleted") { - DispatchQueue.global().async { - self.preEnableIdentityCallbackDeferred?.signal() - self.preEnableIdentityCallbackDeferred = nil - } - } - - Function("preCreateIdentityCallbackCompleted") { - DispatchQueue.global().async { - self.preCreateIdentityCallbackDeferred?.signal() - self.preCreateIdentityCallbackDeferred = nil + guard + let conversation = try client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") } + return try ConsentWrapper.consentStateToString( + state: await conversation.consentState()) } Function("preAuthenticateToInboxCallbackCompleted") { @@ -1769,94 +1423,57 @@ public class XMTPModule: Module { self.preAuthenticateToInboxCallbackDeferred = nil } } - - AsyncFunction("allowGroups") { (inboxId: String, groupIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - try await client.contacts.allowGroups(groupIds: groupIds) - } - - AsyncFunction("denyGroups") { (inboxId: String, groupIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - try await client.contacts.denyGroups(groupIds: groupIds) - } - AsyncFunction("isGroupAllowed") { (inboxId: String, groupId: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - return try await client.contacts.isGroupAllowed(groupId: groupId) - } - - AsyncFunction("isGroupDenied") { (inboxId: String, groupId: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - return try await client.contacts.isGroupDenied(groupId: groupId) - } - - AsyncFunction("updateConversationConsent") { (inboxId: String, conversationId: String, state: String) in - guard let client = await clientsManager.getClient(key: inboxId) else { + AsyncFunction("updateConversationConsent") { + (inboxId: String, conversationId: String, state: String) in + guard let client = await clientsManager.getClient(key: inboxId) + else { throw Error.noClient } - - guard let conversation = try client.findConversation(conversationId: conversationId) else { - throw Error.conversationNotFound("no conversation found for \(conversationId)") + + guard + let conversation = try client.findConversation( + conversationId: conversationId) + else { + throw Error.conversationNotFound( + "no conversation found for \(conversationId)") } - - try await conversation.updateConsentState(state: getConsentState(state: state)) + + try await conversation.updateConsentState( + state: getConsentState(state: state)) } - + AsyncFunction("exportNativeLogs") { () -> String in var logOutput = "" if #available(iOS 15.0, *) { do { - let logStore = try OSLogStore(scope: .currentProcessIdentifier) - let position = logStore.position(timeIntervalSinceLatestBoot: -300) // Last 5 min of logs + let logStore = try OSLogStore( + scope: .currentProcessIdentifier) + let position = logStore.position( + timeIntervalSinceLatestBoot: -300) // Last 5 min of logs let entries = try logStore.getEntries(at: position) for entry in entries { - if let logEntry = entry as? OSLogEntryLog, logEntry.composedMessage.contains("libxmtp") { - logOutput.append("\(logEntry.date): \(logEntry.composedMessage)\n") + if let logEntry = entry as? OSLogEntryLog, + logEntry.composedMessage.contains("libxmtp") + { + logOutput.append( + "\(logEntry.date): \(logEntry.composedMessage)\n" + ) } } } catch { - logOutput = "Failed to fetch logs: \(error.localizedDescription)" + logOutput = + "Failed to fetch logs: \(error.localizedDescription)" } } else { // Fallback for iOS 14 - logOutput = "OSLogStore is only available on iOS 15 and above. Logging is not supported on this iOS version." + logOutput = + "OSLogStore is only available on iOS 15 and above. Logging is not supported on this iOS version." } - + return logOutput } - - AsyncFunction("subscribeToV3Conversations") { (inboxId: String) in - try await subscribeToV3Conversations(inboxId: inboxId) - } - - AsyncFunction("subscribeToAllConversationMessages") { (inboxId: String) in - try await subscribeToAllConversationMessages(inboxId: inboxId) - } - - AsyncFunction("subscribeToConversationMessages") { (inboxId: String, id: String) in - try await subscribeToConversationMessages(inboxId: inboxId, id: id) - } - - AsyncFunction("unsubscribeFromAllConversationMessages") { (inboxId: String) in - await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() - } - - AsyncFunction("unsubscribeFromV3Conversations") { (inboxId: String) in - await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() - } - - AsyncFunction("unsubscribeFromConversationMessages") { (inboxId: String, id: String) in - try await unsubscribeFromConversationMessages(inboxId: inboxId, id: id) - } OnAppBecomesActive { Task { @@ -1864,7 +1481,6 @@ public class XMTPModule: Module { } } - OnAppEntersBackground { Task { try await clientsManager.dropAllLocalDatabaseConnections() @@ -1875,22 +1491,24 @@ public class XMTPModule: Module { // // Helpers // - - private func getPermissionOption(permission: String) throws -> PermissionOption { - switch permission { - case "allow": - return .allow - case "deny": - return .deny - case "admin": - return .admin - case "super_admin": - return .superAdmin - default: - throw Error.invalidPermissionOption - } - } - + + private func getPermissionOption(permission: String) throws + -> PermissionOption + { + switch permission { + case "allow": + return .allow + case "deny": + return .deny + case "admin": + return .admin + case "super_admin": + return .superAdmin + default: + throw Error.invalidPermissionOption + } + } + private func getConsentState(state: String) throws -> ConsentState { switch state { case "allowed": @@ -1901,7 +1519,20 @@ public class XMTPModule: Module { return .unknown } } - + + private func getEntryType(type: String) throws -> EntryType { + switch type { + case "inbox_id": + return .inbox_id + case "conversation_id": + return .conversation_id + case "address": + return .address + default: + throw Error.invalidPermissionOption + } + } + private func getConversationSortOrder(order: String) -> ConversationOrder { switch order { case "lastMessage": @@ -1911,369 +1542,180 @@ public class XMTPModule: Module { } } - func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, preAuthenticateToInboxCallback: PreEventCallback? = nil, enableV3: Bool = false, dbEncryptionKey: Data? = nil, dbDirectory: String? = nil, historySyncUrl: String? = nil) -> XMTP.ClientOptions { - // Ensure that all codecs have been registered. + private func getSortDirection(direction: String) throws -> SortDirection { + switch direction { + case "ASCENDING": + return .ascending + default: + return .descending + } + } + + func createApiClient(env: String, appVersion: String? = nil) + -> XMTP.ClientOptions.Api + { switch env { case "local": - return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( + return XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.local, isSecure: false, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, enableV3: enableV3, encryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, historySyncUrl: historySyncUrl) + ) case "production": - return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( + return XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.production, isSecure: true, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, enableV3: enableV3, encryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, historySyncUrl: historySyncUrl) + ) default: - return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( + return XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.dev, isSecure: true, appVersion: appVersion - ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback, preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, enableV3: enableV3, encryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, historySyncUrl: historySyncUrl) - } - } - - func findConversation(inboxId: String, topic: String) async throws -> Conversation? { - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - let cacheKey = Conversation.cacheKeyForTopic(inboxId: inboxId, topic: topic) - if let conversation = await conversationsManager.get(cacheKey) { - return conversation - } else if let conversation = try await client.conversations.list().first(where: { $0.topic == topic }) { - await conversationsManager.set(cacheKey, conversation) - return conversation + ) } - - return nil } - - func findGroup(inboxId: String, id: String) async throws -> XMTP.Group? { - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - let cacheKey = XMTP.Group.cacheKeyForId(inboxId: client.inboxID, id: id) - if let group = await groupsManager.get(cacheKey) { - return group - } else if let group = try client.findGroup(groupId: id) { - await groupsManager.set(cacheKey, group) - return group - } - return nil + func createClientConfig( + env: String, appVersion: String?, + preAuthenticateToInboxCallback: PreEventCallback? = nil, + dbEncryptionKey: Data, dbDirectory: String? = nil, + historySyncUrl: String? = nil + ) -> XMTP.ClientOptions { + + return XMTP.ClientOptions( + api: createApiClient(env: env, appVersion: appVersion), + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + dbEncryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, + historySyncUrl: historySyncUrl) } - func subscribeToConversations(inboxId: String) async throws { guard let client = await clientsManager.getClient(key: inboxId) else { return } - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() - await subscriptionsManager.set(getConversationsKey(inboxId: inboxId), Task { - do { - for try await conversation in try await client.conversations.stream() { - try sendEvent("conversation", [ - "inboxId": inboxId, - "conversation": ConversationWrapper.encodeToObj(conversation, client: client), - ]) - } - } catch { - print("Error in conversations subscription: \(error)") - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() - } - }) - } - - func subscribeToAllMessages(inboxId: String, includeGroups: Bool = false) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - return - } - - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))?.cancel() - await subscriptionsManager.set(getMessagesKey(inboxId: inboxId), Task { - do { - for try await message in await client.conversations.streamAllDecryptedMessages(includeGroups: includeGroups) { - do { - try sendEvent("message", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - ]) - } catch { - print("discarding message, unable to encode wrapper \(message.id)") - } - } - } catch { - print("Error in all messages subscription: \(error)") - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))?.cancel() - } - }) - } - - func subscribeToAllGroupMessages(inboxId: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - return - } - - await subscriptionsManager.get(getGroupMessagesKey(inboxId: client.inboxID))?.cancel() - await subscriptionsManager.set(getGroupMessagesKey(inboxId: client.inboxID), Task { - do { - for try await message in await client.conversations.streamAllGroupDecryptedMessages() { - do { - try sendEvent("allGroupMessage", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - ]) - } catch { - print("discarding message, unable to encode wrapper \(message.id)") - } - } - } catch { - print("Error in all messages subscription: \(error)") - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))?.cancel() - } - }) - } - - func subscribeToMessages(inboxId: String, topic: String) async throws { - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - return - } - - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - await subscriptionsManager.get(conversation.cacheKey(inboxId))?.cancel() - await subscriptionsManager.set(conversation.cacheKey(inboxId), Task { - do { - for try await message in conversation.streamDecryptedMessages() { - do { - try sendEvent("conversationMessage", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - "topic": topic - ]) - } catch { - print("discarding message, unable to encode wrapper \(message.id)") + await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))? + .cancel() + await subscriptionsManager.set( + getConversationsKey(inboxId: inboxId), + Task { + do { + for try await conversation in await client.conversations + .stream() + { + try sendEvent( + "conversation", + [ + "inboxId": inboxId, + "conversation": ConversationWrapper.encodeToObj( + conversation, client: client), + ]) } + } catch { + print("Error in all conversations subscription: \(error)") + await subscriptionsManager.get( + getConversationsKey(inboxId: inboxId))?.cancel() } - } catch { - print("Error in messages subscription: \(error)") - await subscriptionsManager.get(conversation.cacheKey(inboxId))?.cancel() - } - }) - } - - func subscribeToV3Conversations(inboxId: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - return - } - - await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() - await subscriptionsManager.set(getV3ConversationsKey(inboxId: inboxId), Task { - do { - for try await conversation in await client.conversations.streamConversations() { - try await sendEvent("conversationV3", [ - "inboxId": inboxId, - "conversation": ConversationContainerWrapper.encodeToObj(conversation, client: client), - ]) - } - } catch { - print("Error in all conversations subscription: \(error)") - await subscriptionsManager.get(getV3ConversationsKey(inboxId: inboxId))?.cancel() - } - }) - } - - func subscribeToGroups(inboxId: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - return - } - await subscriptionsManager.get(getGroupsKey(inboxId: client.inboxID))?.cancel() - await subscriptionsManager.set(getGroupsKey(inboxId: client.inboxID), Task { - do { - for try await group in try await client.conversations.streamGroups() { - try await sendEvent("group", [ - "inboxId": inboxId, - "group": GroupWrapper.encodeToObj(group, client: client), - ]) - } - } catch { - print("Error in groups subscription: \(error)") - await subscriptionsManager.get(getGroupsKey(inboxId: client.inboxID))?.cancel() - } - }) + }) } - - func subscribeToAll(inboxId: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - return - } - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() - await subscriptionsManager.set(getConversationsKey(inboxId: inboxId), Task { - do { - for try await conversation in await client.conversations.streamAll() { - try await sendEvent("conversationContainer", [ - "inboxId": inboxId, - "conversationContainer": ConversationContainerWrapper.encodeToObj(conversation, client: client), - ]) - } - } catch { - print("Error in all conversations subscription: \(error)") - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))?.cancel() - } - }) - } - - func subscribeToAllConversationMessages(inboxId: String) async throws { + func subscribeToAllMessages(inboxId: String) async throws { guard let client = await clientsManager.getClient(key: inboxId) else { return } - await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() - await subscriptionsManager.set(getConversationMessagesKey(inboxId: inboxId), Task { - do { - for try await message in await client.conversations.streamAllDecryptedConversationMessages() { - try sendEvent("allConversationMessages", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - ]) + await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))? + .cancel() + await subscriptionsManager.set( + getMessagesKey(inboxId: inboxId), + Task { + do { + for try await message in await client.conversations + .streamAllMessages() + { + try sendEvent( + "messages", + [ + "inboxId": inboxId, + "message": DecodedMessageWrapper.encodeToObj( + message, client: client), + ]) + } + } catch { + print("Error in all messages subscription: \(error)") + await subscriptionsManager.get( + getMessagesKey(inboxId: inboxId))?.cancel() } - } catch { - print("Error in all conversations subscription: \(error)") - await subscriptionsManager.get(getConversationMessagesKey(inboxId: inboxId))?.cancel() - } - }) + }) } - - func subscribeToGroupMessages(inboxId: String, id: String) async throws { - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - return - } + func subscribeToMessages(inboxId: String, id: String) async throws { guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - await subscriptionsManager.get(group.cacheKey(client.inboxID))?.cancel() - await subscriptionsManager.set(group.cacheKey(client.inboxID), Task { - do { - for try await message in group.streamDecryptedMessages() { - do { - try sendEvent("groupMessage", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - "groupId": id, - ]) - } catch { - print("discarding message, unable to encode wrapper \(message.id)") - } - } - } catch { - print("Error in group messages subscription: \(error)") - await subscriptionsManager.get(group.cacheKey(inboxId))?.cancel() - } - }) - } - - func subscribeToConversationMessages(inboxId: String, id: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let converation = try client.findConversation(conversationId: id) else { + guard let converation = try client.findConversation(conversationId: id) + else { return } - await subscriptionsManager.get(try converation.cacheKeyV3(client.inboxID))?.cancel() - await subscriptionsManager.set(try converation.cacheKeyV3(client.inboxID), Task { - do { - for try await message in converation.streamDecryptedMessages() { - do { - try sendEvent("conversationV3Message", [ - "inboxId": inboxId, - "message": DecodedMessageWrapper.encodeToObj(message, client: client), - "conversationId": id, - ]) - } catch { - print("discarding message, unable to encode wrapper \(message.id)") + await subscriptionsManager.get(converation.cacheKey(client.inboxID))? + .cancel() + await subscriptionsManager.set( + try converation.cacheKey(client.inboxID), + Task { + do { + for try await message in converation.streamMessages() { + do { + try sendEvent( + "conversationMessage", + [ + "inboxId": inboxId, + "message": + DecodedMessageWrapper.encodeToObj( + message, client: client), + "conversationId": id, + ]) + } catch { + print( + "discarding message, unable to encode wrapper \(message.id)" + ) + } } + } catch { + print("Error in group messages subscription: \(error)") + await subscriptionsManager.get( + converation.cacheKey(inboxId))?.cancel() } - } catch { - print("Error in group messages subscription: \(error)") - await subscriptionsManager.get(converation.cacheKey(inboxId))?.cancel() - } - }) + }) } - - func unsubscribeFromMessages(inboxId: String, topic: String) async throws { - guard let conversation = try await findConversation(inboxId: inboxId, topic: topic) else { - return - } - - await subscriptionsManager.get(conversation.cacheKey(inboxId))?.cancel() - } - - func unsubscribeFromGroupMessages(inboxId: String, id: String) async throws { - guard let group = try await findGroup(inboxId: inboxId, id: id) else { - return - } - - await subscriptionsManager.get(group.cacheKey(inboxId))?.cancel() - } - - func unsubscribeFromConversationMessages(inboxId: String, id: String) async throws { + func unsubscribeFromMessages(inboxId: String, id: String) async throws { guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - - guard let converation = try client.findConversation(conversationId: id) else { + + guard let converation = try client.findConversation(conversationId: id) + else { return } - await subscriptionsManager.get(try converation.cacheKeyV3(inboxId))?.cancel() + await subscriptionsManager.get(try converation.cacheKey(inboxId))? + .cancel() } func getMessagesKey(inboxId: String) -> String { return "messages:\(inboxId)" } - - func getGroupMessagesKey(inboxId: String) -> String { - return "groupMessages:\(inboxId)" - } func getConversationsKey(inboxId: String) -> String { return "conversations:\(inboxId)" } - + func getConversationMessagesKey(inboxId: String) -> String { return "conversationMessages:\(inboxId)" } - - func getV3ConversationsKey(inboxId: String) -> String { - return "conversationsV3:\(inboxId)" - } - - func getGroupsKey(inboxId: String) -> String { - return "groups:\(inboxId)" - } - - func preEnableIdentityCallback() { - sendEvent("preEnableIdentityCallback") - self.preEnableIdentityCallbackDeferred?.wait() - } - - func preCreateIdentityCallback() { - sendEvent("preCreateIdentityCallback") - self.preCreateIdentityCallbackDeferred?.wait() - } func preAuthenticateToInboxCallback() { sendEvent("preAuthenticateToInboxCallback") diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index c0a001898..23527dd93 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.0" + s.dependency "XMTP", "= 3.0.1" end From ef0a151614ad28c35ccbae73501a42abf24d4cad Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Sun, 10 Nov 2024 19:47:48 -0800 Subject: [PATCH 10/21] get iOS compiling --- ios/Wrappers/DmWrapper.swift | 4 +- ios/XMTPModule.swift | 102 +++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/ios/Wrappers/DmWrapper.swift b/ios/Wrappers/DmWrapper.swift index 92fc7ff43..e7ff41e54 100644 --- a/ios/Wrappers/DmWrapper.swift +++ b/ios/Wrappers/DmWrapper.swift @@ -17,14 +17,14 @@ struct DmWrapper { "createdAt": UInt64(dm.createdAt.timeIntervalSince1970 * 1000), "version": "DM", "topic": dm.topic, - "peerInboxId": try await dm.peerInboxId + "peerInboxId": try dm.peerInboxId ] if conversationParams.consentState { result["consentState"] = ConsentWrapper.consentStateToString(state: try dm.consentState()) } if conversationParams.lastMessage { - if let lastMessage = try await dm.decryptedMessages(limit: 1).first { + if let lastMessage = try await dm.messages(limit: 1).first { result["lastMessage"] = try DecodedMessageWrapper.encode(lastMessage, client: client) } } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 59126e8a7..637d8cb53 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -434,7 +434,7 @@ public class XMTPModule: Module { var results: [String] = [] for conversation in conversations { let encodedConversationContainer = - try await ConversationContainerWrapper.encode( + try await ConversationWrapper.encode( conversation, client: client) results.append(encodedConversationContainer) } @@ -486,7 +486,7 @@ public class XMTPModule: Module { } if let message = try client.findMessage(messageId: messageId) { return try DecodedMessageWrapper.encode( - message.decrypt(), client: client) + message.decode(), client: client) } else { return nil } @@ -515,7 +515,7 @@ public class XMTPModule: Module { if let conversation = try client.findConversation( conversationId: conversationId) { - return try await ConversationContainerWrapper.encode( + return try await ConversationWrapper.encode( conversation, client: client) } else { return nil @@ -531,7 +531,7 @@ public class XMTPModule: Module { if let conversation = try client.findConversationByTopic( topic: topic) { - return try await ConversationContainerWrapper.encode( + return try await ConversationWrapper.encode( conversation, client: client) } else { return nil @@ -722,7 +722,7 @@ public class XMTPModule: Module { "no conversation found for \(dmId)") } if case let .dm(dm) = conversation { - return try await dm.peerInboxId + return try dm.peerInboxId } else { throw Error.conversationNotFound( "no conversation found for \(dmId)") @@ -1263,9 +1263,9 @@ public class XMTPModule: Module { throw Error.noMessage } let decodedMessage = try await conversation.processMessage( - envelopeBytes: encryptedMessageData) + messageBytes: encryptedMessageData) return try DecodedMessageWrapper.encode( - decodedMessage.decrypt(), client: client) + decodedMessage.decode(), client: client) } AsyncFunction("processWelcomeMessage") { @@ -1287,23 +1287,26 @@ public class XMTPModule: Module { throw Error.conversationNotFound("no group found") } - return try await ConversationContainerWrapper.encode( + return try await ConversationWrapper.encode( conversation, client: client) } - AsyncFunction("subscribeToConversations") { (inboxId: String) in - try await subscribeToConversations(inboxId: inboxId) + AsyncFunction("subscribeToConversations") { + (inboxId: String, type: String) in + + try await subscribeToConversations( + inboxId: inboxId, type: getConversationType(type: type)) } AsyncFunction("subscribeToAllMessages") { - (inboxId: String, includeGroups: Bool) in + (inboxId: String, type: String) in try await subscribeToAllMessages( - inboxId: inboxId, includeGroups: includeGroups) + inboxId: inboxId, type: getConversationType(type: type)) } AsyncFunction("subscribeToMessages") { - (inboxId: String, topic: String) in - try await subscribeToMessages(inboxId: inboxId, topic: topic) + (inboxId: String, id: String) in + try await subscribeToMessages(inboxId: inboxId, id: id) } AsyncFunction("unsubscribeFromConversations") { (inboxId: String) in @@ -1317,8 +1320,8 @@ public class XMTPModule: Module { } AsyncFunction("unsubscribeFromMessages") { - (inboxId: String, topic: String) in - try await unsubscribeFromMessages(inboxId: inboxId, topic: topic) + (inboxId: String, id: String) in + try await unsubscribeFromMessages(inboxId: inboxId, id: id) } AsyncFunction("registerPushToken") { @@ -1348,34 +1351,37 @@ public class XMTPModule: Module { } AsyncFunction("consentAddressState") { - (inboxId: String, address: String) -> Bool in + (inboxId: String, address: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - return try ConsentWrapper.consentStateToString( - state: client.preferences.consentList.addressState(address)) + return try await ConsentWrapper.consentStateToString( + state: client.preferences.consentList.addressState( + address: address)) } AsyncFunction("consentInboxIdState") { - (inboxId: String, inboxId: String) -> Bool in + (inboxId: String, peerInboxId: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - return try ConsentWrapper.consentStateToString( - state: client.preferences.consentList.inboxIdState(inboxId)) + return try await ConsentWrapper.consentStateToString( + state: client.preferences.consentList.inboxIdState( + inboxId: peerInboxId)) } AsyncFunction("consentConversationIdState") { - (inboxId: String, conversationId: String) -> Bool in + (inboxId: String, conversationId: String) -> String in guard let client = await clientsManager.getClient(key: inboxId) else { throw Error.noClient } - return try ConsentWrapper.consentStateToString( + return try await ConsentWrapper.consentStateToString( state: client.preferences.consentList.conversationState( - conversationId)) + conversationId: + conversationId)) } AsyncFunction("setConsentState") { @@ -1387,14 +1393,17 @@ public class XMTPModule: Module { else { throw Error.noClient } - client.preferences.consentList.setConsentState( - [ + + let resolvedEntryType = try getEntryType(type: entryType) + let resolvedConsentState = try getConsentState(state: consentType) + + try await client.preferences.consentList.setConsentState( + entries: [ ConsentListEntry( - value, - getEntryType(entryType), - getConsentState(consentType) + value: value, + entryType: resolvedEntryType, + consentType: resolvedConsentState ) - ] ) } @@ -1414,7 +1423,7 @@ public class XMTPModule: Module { "no conversation found for \(conversationId)") } return try ConsentWrapper.consentStateToString( - state: await conversation.consentState()) + state: conversation.consentState()) } Function("preAuthenticateToInboxCallbackCompleted") { @@ -1551,6 +1560,17 @@ public class XMTPModule: Module { } } + private func getConversationType(type: String) throws -> ConversationType { + switch type { + case "groups": + return .groups + case "dms": + return .dms + default: + return .all + } + } + func createApiClient(env: String, appVersion: String? = nil) -> XMTP.ClientOptions.Api { @@ -1590,7 +1610,9 @@ public class XMTPModule: Module { historySyncUrl: historySyncUrl) } - func subscribeToConversations(inboxId: String) async throws { + func subscribeToConversations(inboxId: String, type: ConversationType) + async throws + { guard let client = await clientsManager.getClient(key: inboxId) else { return } @@ -1602,9 +1624,9 @@ public class XMTPModule: Module { Task { do { for try await conversation in await client.conversations - .stream() + .stream(type: type) { - try sendEvent( + try await sendEvent( "conversation", [ "inboxId": inboxId, @@ -1620,7 +1642,9 @@ public class XMTPModule: Module { }) } - func subscribeToAllMessages(inboxId: String) async throws { + func subscribeToAllMessages(inboxId: String, type: ConversationType) + async throws + { guard let client = await clientsManager.getClient(key: inboxId) else { return } @@ -1632,7 +1656,7 @@ public class XMTPModule: Module { Task { do { for try await message in await client.conversations - .streamAllMessages() + .streamAllMessages(type: type) { try sendEvent( "messages", @@ -1663,7 +1687,7 @@ public class XMTPModule: Module { await subscriptionsManager.get(converation.cacheKey(client.inboxID))? .cancel() await subscriptionsManager.set( - try converation.cacheKey(client.inboxID), + converation.cacheKey(client.inboxID), Task { do { for try await message in converation.streamMessages() { @@ -1701,7 +1725,7 @@ public class XMTPModule: Module { return } - await subscriptionsManager.get(try converation.cacheKey(inboxId))? + await subscriptionsManager.get(converation.cacheKey(inboxId))? .cancel() } From d48013a875ca48c7e47255e2a6708ea3b0b6e91e Mon Sep 17 00:00:00 2001 From: Naomi Plasterer Date: Mon, 11 Nov 2024 10:26:59 -0800 Subject: [PATCH 11/21] update the example app --- example/src/ConversationCreateScreen.tsx | 4 +- example/src/ConversationScreen.tsx | 2 +- example/src/GroupScreen.tsx | 2 +- example/src/HomeScreen.tsx | 8 +- example/src/LaunchScreen.tsx | 52 ++----- example/src/StreamScreen.tsx | 2 +- example/src/TestScreen.tsx | 20 +-- example/src/hooks.tsx | 45 ++---- example/src/tests/conversationTests.ts | 27 ++-- example/src/tests/dmTests.ts | 167 +++++++++++++++++++++++ example/src/types/typeTests.ts | 4 +- src/lib/Conversations.ts | 2 +- 12 files changed, 216 insertions(+), 119 deletions(-) create mode 100644 example/src/tests/dmTests.ts diff --git a/example/src/ConversationCreateScreen.tsx b/example/src/ConversationCreateScreen.tsx index 6973bb6ac..9d8e2440b 100644 --- a/example/src/ConversationCreateScreen.tsx +++ b/example/src/ConversationCreateScreen.tsx @@ -22,7 +22,7 @@ export default function ConversationCreateScreen({ } if (groupsEnabled) { const toAddresses = toAddress.split(',') - const canMessage = await client.canGroupMessage(toAddresses) + const canMessage = await client.canMessage(toAddresses) if (!canMessage) { setAlert(`${toAddress} cannot be added to a group conversation yet`) return @@ -30,7 +30,7 @@ export default function ConversationCreateScreen({ const group = await client.conversations.newGroup(toAddresses) navigation.navigate('group', { id: group.id }) } else { - const canMessage = await client.canMessage(toAddress) + const canMessage = await client.canMessage([toAddress]) if (!canMessage) { setAlert(`${toAddress} is not on the XMTP network yet`) return diff --git a/example/src/ConversationScreen.tsx b/example/src/ConversationScreen.tsx index 6cbe84d64..45304e7b9 100644 --- a/example/src/ConversationScreen.tsx +++ b/example/src/ConversationScreen.tsx @@ -959,7 +959,7 @@ function MessageItem({ {message.senderAddress.slice(-4)} - {moment(message.sent).fromNow()} + {moment(message.sentNs / 1000000).fromNow()} )} diff --git a/example/src/GroupScreen.tsx b/example/src/GroupScreen.tsx index f3ca36fb4..43d856377 100644 --- a/example/src/GroupScreen.tsx +++ b/example/src/GroupScreen.tsx @@ -959,7 +959,7 @@ function MessageItem({ {message.senderAddress.slice(-4)} - {moment(message.sent).fromNow()} + {moment(message.sentNs / 1000000).fromNow()} )} diff --git a/example/src/HomeScreen.tsx b/example/src/HomeScreen.tsx index d67cf3288..882ab48ca 100644 --- a/example/src/HomeScreen.tsx +++ b/example/src/HomeScreen.tsx @@ -103,7 +103,7 @@ function GroupListItem({ const [consentState, setConsentState] = useState() const denyGroup = async () => { - await client?.contacts.denyGroups([group.id]) + await group.updateConsent('denied') const consent = await group.consentState() setConsentState(consent) } @@ -154,7 +154,7 @@ function GroupListItem({ {lastMessage?.fallback} {lastMessage?.senderAddress}: - {moment(lastMessage?.sent).fromNow()} + {moment(lastMessage?.sentNs / 1000000).fromNow()} @@ -185,7 +185,7 @@ function ConversationItem({ }, [conversation]) const denyContact = async () => { - await client?.contacts.deny([conversation.peerAddress]) + await conversation.updateConsent('denied') conversation .consentState() .then(setConsentState) @@ -223,7 +223,7 @@ function ConversationItem({ {lastMessage?.fallback} {lastMessage?.senderAddress}: - {moment(lastMessage?.sent).fromNow()} + {moment(lastMessage?.sentNs / 1000000).fromNow()} {getConsentState} diff --git a/example/src/LaunchScreen.tsx b/example/src/LaunchScreen.tsx index 91332479d..8e9ac0f1a 100644 --- a/example/src/LaunchScreen.tsx +++ b/example/src/LaunchScreen.tsx @@ -10,7 +10,7 @@ import { useXmtp } from 'xmtp-react-native-sdk' import { NavigationParamList } from './Navigation' import { TestCategory } from './TestScreen' import { supportedCodecs } from './contentTypes/contentTypes' -import { useSavedKeys } from './hooks' +import { useSavedAddress } from './hooks' const appVersion = 'XMTP_RN_EX/0.0.1' @@ -65,11 +65,10 @@ export default function LaunchScreen( const [selectedNetwork, setSelectedNetwork] = useState< 'dev' | 'local' | 'production' >('dev') - const [enableGroups, setEnableGroups] = useState('true') const signer = useSigner() const [signerAddressDisplay, setSignerAddressDisplay] = useState() const { setClient } = useXmtp() - const savedKeys = useSavedKeys() + const savedKeys = useSavedAddress() const configureWallet = useCallback( (label: string, configuring: Promise>) => { console.log('Connecting XMTP client', label) @@ -81,8 +80,7 @@ export default function LaunchScreen( setClient(client) navigation.navigate('home') // Save the configured client keys for use in later sessions. - const keyBundle = await client.exportKeyBundle() - await savedKeys.save(keyBundle) + await savedKeys.save(client.address) }) .catch((err) => console.log('Unable to connect XMTP client', label, err) @@ -91,14 +89,6 @@ export default function LaunchScreen( [] ) - const preCreateIdentityCallback = () => { - console.log('Pre Create Identity Callback') - } - - const preEnableIdentityCallback = () => { - console.log('Pre Enable Identity Callback') - } - const preAuthenticateToInboxCallback = async () => { console.log('Pre Authenticate To Inbox Callback') } @@ -191,10 +181,6 @@ export default function LaunchScreen( selectTextStyle={styles.modalSelectText} backdropPressToClose data={groupOptions} - initValue={enableGroups} - onChange={(option) => { - setEnableGroups(option.label) - }} /> @@ -209,12 +195,7 @@ export default function LaunchScreen( color="orange" onPress={() => { ;(async () => { - console.log( - 'Using network ' + - selectedNetwork + - ' and enableV3 ' + - enableGroups - ) + console.log('Using network ' + selectedNetwork) const dbEncryptionKey = await getDbEncryptionKey( selectedNetwork, @@ -227,10 +208,7 @@ export default function LaunchScreen( env: selectedNetwork, appVersion, codecs: supportedCodecs, - preCreateIdentityCallback, - preEnableIdentityCallback, preAuthenticateToInboxCallback, - enableV3: enableGroups === 'true', dbEncryptionKey, }) ) @@ -246,12 +224,7 @@ export default function LaunchScreen( color="green" onPress={() => { ;(async () => { - console.log( - 'Using network ' + - selectedNetwork + - ' and enableV3 ' + - enableGroups - ) + console.log('Using network ' + selectedNetwork) const dbEncryptionKey = await getDbEncryptionKey( selectedNetwork, true @@ -262,10 +235,7 @@ export default function LaunchScreen( env: selectedNetwork, appVersion, codecs: supportedCodecs, - preCreateIdentityCallback, - preEnableIdentityCallback, preAuthenticateToInboxCallback, - enableV3: enableGroups === 'true', dbEncryptionKey, }) ) @@ -273,7 +243,7 @@ export default function LaunchScreen( }} /> - {!!savedKeys.keyBundle && ( + {!!savedKeys.address && ( <>