diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..e5b6d8d6a --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..c8fca743d --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.4/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.github/workflows/betaRelease.yml b/.github/workflows/betaRelease.yml new file mode 100644 index 000000000..b39ce4db5 --- /dev/null +++ b/.github/workflows/betaRelease.yml @@ -0,0 +1,29 @@ +name: Beta Release +on: + push: + branches: + - beta + workflow_dispatch: + inputs: + branch: + description: 'Branch name' + required: true + default: 'beta' +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v3 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx semantic-release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfc3cb8de..5095a605a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,42 @@ name: Release + on: push: branches: - main - - beta - workflow_dispatch: - inputs: - branch: - description: 'Branch name' - required: true - default: 'main' + +concurrency: ${{ github.workflow }}-${{ github.ref }} + jobs: release: name: Release runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: "yarn" + - name: Update npm to latest + run: npm install -g npm@latest - name: Install dependencies run: yarn install --frozen-lockfile - - name: Release + - name: Publish + uses: changesets/action@v1 + with: + title: "release: version packages" + commit: "release: version packages" + publish: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: npx semantic-release + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 7fcbe5e5b..0dee991a1 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.4" + implementation "org.xmtp:android:3.0.13" 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 35ed22f0f..8974d6bdc 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -13,6 +13,7 @@ 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.ContentJson import expo.modules.xmtpreactnativesdk.wrappers.ConversationWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConversationParamsWrapper @@ -25,6 +26,7 @@ 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.WalletParamsWrapper import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -34,7 +36,7 @@ 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.ConsentRecord import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation import org.xmtp.android.library.Conversations.* @@ -127,8 +129,8 @@ data class SignatureRequest( var message: String, ) -fun Conversation.cacheKey(inboxId: String): String { - return "${inboxId}:${topic}" +fun Conversation.cacheKey(installationId: String): String { + return "${installationId}:${topic}" } class XMTPModule : Module() { @@ -175,7 +177,7 @@ class XMTPModule : Module() { val historySyncUrl = authOptions.historySyncUrl ?: when (authOptions.environment) { "production" -> "https://message-history.production.ephemera.network/" - "local" -> "http://0.0.0.0:5558" + "local" -> "http://10.0.2.2:5558" else -> "https://message-history.dev.ephemera.network/" } return ClientOptions( @@ -205,78 +207,66 @@ class XMTPModule : Module() { "conversation", "message", "conversationMessage", + "consent", ) - Function("address") { inboxId: String -> + Function("address") { installationId: String -> logV("address") - val client = clients[inboxId] + val client = clients[installationId] client?.address ?: "No Client." } - Function("inboxId") { inboxId: String -> + Function("inboxId") { installationId: String -> logV("inboxId") - val client = clients[inboxId] + val client = clients[installationId] client?.inboxId ?: "No Client." } - AsyncFunction("findInboxIdFromAddress") Coroutine { inboxId: String, address: String -> + AsyncFunction("findInboxIdFromAddress") Coroutine { installationId: String, address: String -> withContext(Dispatchers.IO) { logV("findInboxIdFromAddress") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.inboxIdFromAddress(address) } } - AsyncFunction("deleteLocalDatabase") { inboxId: String -> - logV(inboxId) + AsyncFunction("deleteLocalDatabase") { installationId: String -> + logV(installationId) logV(clients.toString()) - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.deleteLocalDatabase() } - Function("dropLocalDatabaseConnection") { inboxId: String -> - val client = clients[inboxId] ?: throw XMTPException("No client") + Function("dropLocalDatabaseConnection") { installationId: String -> + val client = clients[installationId] ?: throw XMTPException("No client") client.dropLocalDatabaseConnection() } - AsyncFunction("reconnectLocalDatabase") Coroutine { inboxId: String -> + AsyncFunction("reconnectLocalDatabase") Coroutine { installationId: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.reconnectLocalDatabase() } } - AsyncFunction("requestMessageHistorySync") Coroutine { inboxId: String -> + AsyncFunction("requestMessageHistorySync") Coroutine { installationId: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.requestMessageHistorySync() } } - AsyncFunction("revokeAllOtherInstallations") Coroutine { inboxId: String -> + AsyncFunction("getInboxState") Coroutine { installationId: String, refreshFromNetwork: Boolean -> withContext(Dispatchers.IO) { - logV("revokeAllOtherInstallations") - val client = clients[inboxId] ?: throw XMTPException("No client") - val reactSigner = - ReactNativeSigner(module = this@XMTPModule, address = client.address) - signer = reactSigner - - client.revokeAllOtherInstallations(reactSigner) - signer = null - } - } - - AsyncFunction("getInboxState") Coroutine { inboxId: String, refreshFromNetwork: Boolean -> - withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val inboxState = client.inboxState(refreshFromNetwork) InboxStateWrapper.encode(inboxState) } } - AsyncFunction("getInboxStates") Coroutine { inboxId: String, refreshFromNetwork: Boolean, inboxIds: List -> + AsyncFunction("getInboxStates") Coroutine { installationId: String, refreshFromNetwork: Boolean, inboxIds: List -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val inboxStates = client.inboxStatesForInboxIds(refreshFromNetwork, inboxIds) inboxStates.map { InboxStateWrapper.encode(it) } } @@ -312,21 +302,21 @@ class XMTPModule : Module() { val randomClient = Client().create(account = privateKey, options = options) ContentJson.Companion - clients[randomClient.inboxId] = randomClient + clients[randomClient.installationId] = randomClient ClientWrapper.encodeToObj(randomClient) } } - AsyncFunction("create") Coroutine { address: String, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String -> + AsyncFunction("create") Coroutine { address: String, hasAuthInboxCallback: Boolean?, dbEncryptionKey: List, authParams: String, walletParams: String -> withContext(Dispatchers.IO) { logV("create") - val authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) val reactSigner = ReactNativeSigner( module = this@XMTPModule, address = address, - type = authOptions.walletType, - chainId = authOptions.chainId, - blockNumber = authOptions.blockNumber + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber ) signer = reactSigner val options = clientOptions( @@ -335,43 +325,136 @@ class XMTPModule : Module() { hasAuthInboxCallback, ) val client = Client().create(account = reactSigner, options = options) - clients[client.inboxId] = client + clients[client.installationId] = client ContentJson.Companion signer = null sendEvent("authed", ClientWrapper.encodeToObj(client)) } } - AsyncFunction("build") Coroutine { address: String, dbEncryptionKey: List, authParams: String -> + AsyncFunction("build") Coroutine { address: String, inboxId: String?, dbEncryptionKey: List, authParams: String -> withContext(Dispatchers.IO) { logV("build") val options = clientOptions( dbEncryptionKey, authParams, ) - val client = Client().build(address = address, options = options) + val client = Client().build(address = address, options = options, inboxId = inboxId) ContentJson.Companion - clients[client.inboxId] = client + clients[client.installationId] = client ClientWrapper.encodeToObj(client) } } - AsyncFunction("dropClient") Coroutine { inboxId: String -> + AsyncFunction("revokeAllOtherInstallations") Coroutine { installationId: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("revokeAllOtherInstallations") + val client = clients[installationId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = client.address, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.revokeAllOtherInstallations(reactSigner) + signer = null + } + } + + AsyncFunction("addAccount") Coroutine { installationId: String, newAddress: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("addAccount") + val client = clients[installationId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = newAddress, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.addAccount(reactSigner) + signer = null + } + } + + AsyncFunction("removeAccount") Coroutine { installationId: String, addressToRemove: String, walletParams: String -> + withContext(Dispatchers.IO) { + logV("removeAccount") + val client = clients[installationId] ?: throw XMTPException("No client") + val walletOptions = WalletParamsWrapper.walletParamsFromJson(walletParams) + val reactSigner = + ReactNativeSigner( + module = this@XMTPModule, + address = client.address, + type = walletOptions.walletType, + chainId = walletOptions.chainId, + blockNumber = walletOptions.blockNumber + ) + signer = reactSigner + + client.removeAccount(reactSigner, addressToRemove) + signer = null + } + } + + AsyncFunction("dropClient") Coroutine { installationId: String -> withContext(Dispatchers.IO) { logV("dropClient") - clients.remove(inboxId) + clients.remove(installationId) Unit } } - AsyncFunction("canMessage") Coroutine { inboxId: String, peerAddresses: List -> + AsyncFunction("signWithInstallationKey") Coroutine { installationId: String, message: String -> + withContext(Dispatchers.IO) { + logV("signWithInstallationKey") + val client = clients[installationId] ?: throw XMTPException("No client") + + val signature = client.signWithInstallationKey(message) + signature.map { it.toInt() and 0xFF } + } + } + + AsyncFunction("verifySignature") Coroutine { installationId: String, message: String, signature: List -> + withContext(Dispatchers.IO) { + logV("verifySignature") + val client = clients[installationId] ?: throw XMTPException("No client") + val signatureBytes = + signature.foldIndexed(ByteArray(signature.size)) { i, a, v -> + a.apply { set(i, v.toByte()) } + } + client.verifySignature(message, signatureBytes) + } + } + + AsyncFunction("canMessage") Coroutine { installationId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("canMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.canMessage(peerAddresses) } } + AsyncFunction("staticCanMessage") Coroutine { environment: String, peerAddresses: List -> + withContext(Dispatchers.IO) { + logV("staticCanMessage") + Client.canMessage( + peerAddresses, + context, + apiEnvironments(environment, null) + ) + } + } + AsyncFunction("getOrCreateInboxId") Coroutine { address: String, environment: String -> withContext(Dispatchers.IO) { try { @@ -386,7 +469,7 @@ class XMTPModule : Module() { } } - AsyncFunction("encryptAttachment") { inboxId: String, fileJson: String -> + AsyncFunction("encryptAttachment") { installationId: String, fileJson: String -> logV("encryptAttachment") val file = DecryptedLocalAttachment.fromJson(fileJson) val uri = Uri.parse(file.fileUri) @@ -412,7 +495,7 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("decryptAttachment") { inboxId: String, encryptedFileJson: String -> + AsyncFunction("decryptAttachment") { installationId: String, encryptedFileJson: String -> logV("decryptAttachment") val encryptedFile = EncryptedLocalAttachment.fromJson(encryptedFileJson) val encryptedData = appContext.reactContext?.contentResolver @@ -438,51 +521,62 @@ class XMTPModule : Module() { ).toJson() } - AsyncFunction("listGroups") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listGroups") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listGroups") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val groups = client.conversations.listGroups(order = order, limit = limit) + val consent = consentState?.let { getConsentState(it) } + val groups = client.conversations.listGroups( + order = order, + limit = limit, + consentState = consent + ) groups.map { group -> GroupWrapper.encode(client, group, params) } } } - AsyncFunction("listDms") Coroutine { inboxId: String, groupParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listDms") Coroutine { installationId: String, groupParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listDms") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(groupParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") - val dms = client.conversations.listDms(order = order, limit = limit) + val consent = consentState?.let { getConsentState(it) } + val dms = client.conversations.listDms( + order = order, + limit = limit, + consentState = consent + ) dms.map { dm -> DmWrapper.encode(client, dm, params) } } } - AsyncFunction("listConversations") Coroutine { inboxId: String, conversationParams: String?, sortOrder: String?, limit: Int? -> + AsyncFunction("listConversations") Coroutine { installationId: String, conversationParams: String?, sortOrder: String?, limit: Int?, consentState: String? -> withContext(Dispatchers.IO) { logV("listConversations") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val params = ConversationParamsWrapper.conversationParamsFromJson(conversationParams ?: "") val order = getConversationSortOrder(sortOrder ?: "") + val consent = consentState?.let { getConsentState(it) } val conversations = - client.conversations.list(order = order, limit = limit) + client.conversations.list(order = order, limit = limit, consentState = consent) conversations.map { conversation -> ConversationWrapper.encode(client, conversation, params) } } } - AsyncFunction("conversationMessages") Coroutine { inboxId: String, conversationId: String, limit: Int?, beforeNs: Long?, afterNs: Long?, direction: String? -> + AsyncFunction("conversationMessages") Coroutine { installationId: 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 client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) conversation?.messages( limit = limit, @@ -495,10 +589,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findMessage") Coroutine { inboxId: String, messageId: String -> + AsyncFunction("findMessage") Coroutine { installationId: String, messageId: String -> withContext(Dispatchers.IO) { logV("findMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val message = client.findMessage(messageId) message?.let { DecodedMessageWrapper.encode(it.decode()) @@ -506,10 +600,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findGroup") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("findGroup") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("findGroup") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) group?.let { GroupWrapper.encode(client, it) @@ -517,10 +611,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findConversation") Coroutine { inboxId: String, conversationId: String -> + AsyncFunction("findConversation") Coroutine { installationId: String, conversationId: String -> withContext(Dispatchers.IO) { logV("findConversation") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) conversation?.let { ConversationWrapper.encode(client, conversation) @@ -528,10 +622,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findConversationByTopic") Coroutine { inboxId: String, topic: String -> + AsyncFunction("findConversationByTopic") Coroutine { installationId: String, topic: String -> withContext(Dispatchers.IO) { logV("findConversationByTopic") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversationByTopic(topic) conversation?.let { ConversationWrapper.encode(client, conversation) @@ -539,10 +633,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findDmByInboxId") Coroutine { inboxId: String, peerInboxId: String -> + AsyncFunction("findDmByInboxId") Coroutine { installationId: String, peerInboxId: String -> withContext(Dispatchers.IO) { logV("findDmByInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val dm = client.findDmByInboxId(peerInboxId) dm?.let { DmWrapper.encode(client, dm) @@ -550,10 +644,10 @@ class XMTPModule : Module() { } } - AsyncFunction("findDmByAddress") Coroutine { inboxId: String, peerAddress: String -> + AsyncFunction("findDmByAddress") Coroutine { installationId: String, peerAddress: String -> withContext(Dispatchers.IO) { logV("findDmByAddress") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val dm = client.findDmByAddress(peerAddress) dm?.let { DmWrapper.encode(client, dm) @@ -561,10 +655,10 @@ class XMTPModule : Module() { } } - AsyncFunction("sendMessage") Coroutine { inboxId: String, id: String, contentJson: String -> + AsyncFunction("sendMessage") Coroutine { installationId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { logV("sendMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) @@ -575,20 +669,20 @@ class XMTPModule : Module() { } } - AsyncFunction("publishPreparedMessages") Coroutine { inboxId: String, id: String -> + AsyncFunction("publishPreparedMessages") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { logV("publishPreparedMessages") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") conversation.publishMessages() } } - AsyncFunction("prepareMessage") Coroutine { inboxId: String, id: String, contentJson: String -> + AsyncFunction("prepareMessage") Coroutine { installationId: String, id: String, contentJson: String -> withContext(Dispatchers.IO) { logV("prepareMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") val sending = ContentJson.fromJson(contentJson) @@ -599,19 +693,19 @@ class XMTPModule : Module() { } } - AsyncFunction("findOrCreateDm") Coroutine { inboxId: String, peerAddress: String -> + AsyncFunction("findOrCreateDm") Coroutine { installationId: String, peerAddress: String -> withContext(Dispatchers.IO) { logV("findOrCreateDm") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val dm = client.conversations.findOrCreateDm(peerAddress) DmWrapper.encode(client, dm) } } - AsyncFunction("createGroup") Coroutine { inboxId: String, peerAddresses: List, permission: String, groupOptionsJson: String -> + AsyncFunction("createGroup") Coroutine { installationId: String, peerAddresses: List, permission: String, groupOptionsJson: String -> withContext(Dispatchers.IO) { logV("createGroup") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val permissionLevel = when (permission) { "admin_only" -> GroupPermissionPreconfiguration.ADMIN_ONLY else -> GroupPermissionPreconfiguration.ALL_MEMBERS @@ -630,10 +724,10 @@ class XMTPModule : Module() { } } - AsyncFunction("createGroupCustomPermissions") Coroutine { inboxId: String, peerAddresses: List, permissionPolicySetJson: String, groupOptionsJson: String -> + AsyncFunction("createGroupCustomPermissions") Coroutine { installationId: String, peerAddresses: List, permissionPolicySetJson: String, groupOptionsJson: String -> withContext(Dispatchers.IO) { logV("createGroup") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) val permissionPolicySet = @@ -653,20 +747,20 @@ class XMTPModule : Module() { } - AsyncFunction("listMemberInboxIds") Coroutine { inboxId: String, id: String -> + AsyncFunction("listMemberInboxIds") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { logV("listMembers") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") conversation.members().map { it.inboxId } } } - AsyncFunction("dmPeerInboxId") Coroutine { inboxId: String, dmId: String -> + AsyncFunction("dmPeerInboxId") Coroutine { installationId: String, dmId: String -> withContext(Dispatchers.IO) { logV("dmPeerInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(dmId) ?: throw XMTPException("no conversation found for $dmId") val dm = (conversation as Conversation.Dm).dm @@ -674,358 +768,358 @@ class XMTPModule : Module() { } } - AsyncFunction("listConversationMembers") Coroutine { inboxId: String, conversationId: String -> + AsyncFunction("listConversationMembers") Coroutine { installationId: String, conversationId: String -> withContext(Dispatchers.IO) { logV("listConversationMembers") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) ?: throw XMTPException("no conversation found for $conversationId") conversation.members().map { MemberWrapper.encode(it) } } } - AsyncFunction("syncConversations") Coroutine { inboxId: String -> + AsyncFunction("syncConversations") Coroutine { installationId: String -> withContext(Dispatchers.IO) { logV("syncConversations") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") client.conversations.sync() } } - AsyncFunction("syncAllConversations") Coroutine { inboxId: String -> + AsyncFunction("syncAllConversations") Coroutine { installationId: String -> withContext(Dispatchers.IO) { logV("syncAllConversations") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val numGroupsSyncedInt: Int = client.conversations.syncAllConversations().toInt() numGroupsSyncedInt } } - AsyncFunction("syncConversation") Coroutine { inboxId: String, id: String -> + AsyncFunction("syncConversation") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { logV("syncConversation") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") conversation.sync() } } - AsyncFunction("addGroupMembers") Coroutine { inboxId: String, groupId: String, peerAddresses: List -> + AsyncFunction("addGroupMembers") Coroutine { installationId: String, groupId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("addGroupMembers") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.addMembers(peerAddresses) } } - AsyncFunction("removeGroupMembers") Coroutine { inboxId: String, groupId: String, peerAddresses: List -> + AsyncFunction("removeGroupMembers") Coroutine { installationId: String, groupId: String, peerAddresses: List -> withContext(Dispatchers.IO) { logV("removeGroupMembers") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.removeMembers(peerAddresses) } } - AsyncFunction("addGroupMembersByInboxId") Coroutine { inboxId: String, groupId: String, peerInboxIds: List -> + AsyncFunction("addGroupMembersByInboxId") Coroutine { installationId: String, groupId: String, peerInboxIds: List -> withContext(Dispatchers.IO) { logV("addGroupMembersByInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.addMembersByInboxId(peerInboxIds) } } - AsyncFunction("removeGroupMembersByInboxId") Coroutine { inboxId: String, groupId: String, peerInboxIds: List -> + AsyncFunction("removeGroupMembersByInboxId") Coroutine { installationId: String, groupId: String, peerInboxIds: List -> withContext(Dispatchers.IO) { logV("removeGroupMembersByInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.removeMembersByInboxId(peerInboxIds) } } - AsyncFunction("groupName") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("groupName") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupName") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.name } } - AsyncFunction("updateGroupName") Coroutine { inboxId: String, groupId: String, groupName: String -> + AsyncFunction("updateGroupName") Coroutine { installationId: String, groupId: String, groupName: String -> withContext(Dispatchers.IO) { logV("updateGroupName") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupName(groupName) } } - AsyncFunction("groupImageUrlSquare") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("groupImageUrlSquare") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupImageUrlSquare") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.imageUrlSquare } } - AsyncFunction("updateGroupImageUrlSquare") Coroutine { inboxId: String, groupId: String, groupImageUrl: String -> + AsyncFunction("updateGroupImageUrlSquare") Coroutine { installationId: String, groupId: String, groupImageUrl: String -> withContext(Dispatchers.IO) { logV("updateGroupImageUrlSquare") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupImageUrlSquare(groupImageUrl) } } - AsyncFunction("groupDescription") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("groupDescription") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupDescription") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.description } } - AsyncFunction("updateGroupDescription") Coroutine { inboxId: String, groupId: String, groupDescription: String -> + AsyncFunction("updateGroupDescription") Coroutine { installationId: String, groupId: String, groupDescription: String -> withContext(Dispatchers.IO) { logV("updateGroupDescription") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupDescription(groupDescription) } } - AsyncFunction("groupPinnedFrameUrl") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("groupPinnedFrameUrl") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupPinnedFrameUrl") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.pinnedFrameUrl } } - AsyncFunction("updateGroupPinnedFrameUrl") Coroutine { inboxId: String, groupId: String, pinnedFrameUrl: String -> + AsyncFunction("updateGroupPinnedFrameUrl") Coroutine { installationId: String, groupId: String, pinnedFrameUrl: String -> withContext(Dispatchers.IO) { logV("updateGroupPinnedFrameUrl") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupPinnedFrameUrl(pinnedFrameUrl) } } - AsyncFunction("isGroupActive") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("isGroupActive") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("isGroupActive") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.isActive() } } - AsyncFunction("addedByInboxId") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("addedByInboxId") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("addedByInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.addedByInboxId() } } - AsyncFunction("creatorInboxId") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("creatorInboxId") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("creatorInboxId") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.creatorInboxId() } } - AsyncFunction("isAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("isAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("isGroupAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.isAdmin(inboxId) } } - AsyncFunction("isSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("isSuperAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("isSuperAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.isSuperAdmin(inboxId) } } - AsyncFunction("listAdmins") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("listAdmins") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("listAdmins") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.listAdmins() } } - AsyncFunction("listSuperAdmins") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("listSuperAdmins") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("listSuperAdmins") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.listSuperAdmins() } } - AsyncFunction("addAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("addAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("addAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.addAdmin(inboxId) } } - AsyncFunction("addSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("addSuperAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("addSuperAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.addSuperAdmin(inboxId) } } - AsyncFunction("removeAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("removeAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("removeAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.removeAdmin(inboxId) } } - AsyncFunction("removeSuperAdmin") Coroutine { clientInboxId: String, groupId: String, inboxId: String -> + AsyncFunction("removeSuperAdmin") Coroutine { clientInstallationId: String, groupId: String, inboxId: String -> withContext(Dispatchers.IO) { logV("removeSuperAdmin") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.removeSuperAdmin(inboxId) } } - AsyncFunction("updateAddMemberPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateAddMemberPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateAddMemberPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateAddMemberPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateRemoveMemberPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateRemoveMemberPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateRemoveMemberPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateRemoveMemberPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateAddAdminPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateAddAdminPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateAddAdminPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateAddAdminPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateRemoveAdminPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateRemoveAdminPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateRemoveAdminPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateRemoveAdminPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupNamePermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateGroupNamePermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupNamePermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupNamePermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupImageUrlSquarePermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateGroupImageUrlSquarePermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupImageUrlSquarePermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupImageUrlSquarePermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupDescriptionPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateGroupDescriptionPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupDescriptionPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupDescriptionPermission(getPermissionOption(newPermission)) } } - AsyncFunction("updateGroupPinnedFrameUrlPermission") Coroutine { clientInboxId: String, groupId: String, newPermission: String -> + AsyncFunction("updateGroupPinnedFrameUrlPermission") Coroutine { clientInstallationId: String, groupId: String, newPermission: String -> withContext(Dispatchers.IO) { logV("updateGroupPinnedFrameUrlPermission") - val client = clients[clientInboxId] ?: throw XMTPException("No client") + val client = clients[clientInstallationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") group.updateGroupPinnedFrameUrlPermission(getPermissionOption(newPermission)) } } - AsyncFunction("permissionPolicySet") Coroutine { inboxId: String, groupId: String -> + AsyncFunction("permissionPolicySet") Coroutine { installationId: String, groupId: String -> withContext(Dispatchers.IO) { logV("groupImageUrlSquare") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val group = client.findGroup(groupId) ?: throw XMTPException("no group found for $groupId") val permissionPolicySet = group.permissionPolicySet() @@ -1033,21 +1127,21 @@ class XMTPModule : Module() { } } - AsyncFunction("processMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> + AsyncFunction("processMessage") Coroutine { installationId: String, id: String, encryptedMessage: String -> withContext(Dispatchers.IO) { logV("processMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: 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.decode()) + DecodedMessageWrapper.encode(message.decode()) } } - AsyncFunction("processWelcomeMessage") Coroutine { inboxId: String, encryptedMessage: String -> + AsyncFunction("processWelcomeMessage") Coroutine { installationId: String, encryptedMessage: String -> withContext(Dispatchers.IO) { logV("processWelcomeMessage") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.conversations.fromWelcome( @@ -1060,12 +1154,19 @@ class XMTPModule : Module() { } } - AsyncFunction("setConsentState") Coroutine { inboxId: String, value: String, entryType: String, consentType: String -> + AsyncFunction("syncConsent") Coroutine { installationId: String -> + withContext(Dispatchers.IO) { + val client = clients[installationId] ?: throw XMTPException("No client") + client.preferences.syncConsent() + } + } + + AsyncFunction("setConsentState") Coroutine { installationId: String, value: String, entryType: String, consentType: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") - client.preferences.consentList.setConsentState( + val client = clients[installationId] ?: throw XMTPException("No client") + client.preferences.setConsentState( listOf( - ConsentListEntry( + ConsentRecord( value, getEntryType(entryType), getConsentState(consentType) @@ -1075,40 +1176,40 @@ class XMTPModule : Module() { } } - AsyncFunction("consentAddressState") Coroutine { inboxId: String, peerAddress: String -> + AsyncFunction("consentAddressState") Coroutine { installationId: String, peerAddress: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") - consentStateToString(client.preferences.consentList.addressState(peerAddress)) + val client = clients[installationId] ?: throw XMTPException("No client") + consentStateToString(client.preferences.addressState(peerAddress)) } } - AsyncFunction("consentInboxIdState") Coroutine { inboxId: String, peerInboxId: String -> + AsyncFunction("consentInboxIdState") Coroutine { installationId: String, peerInboxId: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") - consentStateToString(client.preferences.consentList.inboxIdState(peerInboxId)) + val client = clients[installationId] ?: throw XMTPException("No client") + consentStateToString(client.preferences.inboxIdState(peerInboxId)) } } - AsyncFunction("consentConversationIdState") Coroutine { inboxId: String, conversationId: String -> + AsyncFunction("consentConversationIdState") Coroutine { installationId: String, conversationId: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") - consentStateToString(client.preferences.consentList.conversationState(conversationId)) + val client = clients[installationId] ?: throw XMTPException("No client") + consentStateToString(client.preferences.conversationState(conversationId)) } } - AsyncFunction("conversationConsentState") Coroutine { inboxId: String, conversationId: String -> + AsyncFunction("conversationConsentState") Coroutine { installationId: String, conversationId: String -> withContext(Dispatchers.IO) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) ?: throw XMTPException("no group found for $conversationId") consentStateToString(conversation.consentState()) } } - AsyncFunction("updateConversationConsent") Coroutine { inboxId: String, conversationId: String, state: String -> + AsyncFunction("updateConversationConsent") Coroutine { installationId: String, conversationId: String, state: String -> withContext(Dispatchers.IO) { logV("updateConversationConsent") - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(conversationId) ?: throw XMTPException("no group found for $conversationId") @@ -1116,42 +1217,53 @@ class XMTPModule : Module() { } } - Function("subscribeToConversations") { inboxId: String, type: String -> + Function("subscribeToConsent") { installationId: String -> + logV("subscribeToConsent") + + subscribeToConsent(installationId = installationId) + } + + Function("subscribeToConversations") { installationId: String, type: String -> logV("subscribeToConversations") - subscribeToConversations(inboxId = inboxId, getStreamType(type)) + subscribeToConversations(installationId = installationId, getStreamType(type)) } - Function("subscribeToAllMessages") { inboxId: String, type: String -> + Function("subscribeToAllMessages") { installationId: String, type: String -> logV("subscribeToAllMessages") - subscribeToAllMessages(inboxId = inboxId, getStreamType(type)) + subscribeToAllMessages(installationId = installationId, getStreamType(type)) } - AsyncFunction("subscribeToMessages") Coroutine { inboxId: String, id: String -> + AsyncFunction("subscribeToMessages") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { logV("subscribeToMessages") subscribeToMessages( - inboxId = inboxId, + installationId = installationId, id = id ) } } - Function("unsubscribeFromConversations") { inboxId: String -> + Function("unsubscribeFromConsent") { installationId: String -> + logV("unsubscribeFromConsent") + subscriptions[getConsentKey(installationId)]?.cancel() + } + + Function("unsubscribeFromConversations") { installationId: String -> logV("unsubscribeFromConversations") - subscriptions[getConversationsKey(inboxId)]?.cancel() + subscriptions[getConversationsKey(installationId)]?.cancel() } - Function("unsubscribeFromAllMessages") { inboxId: String -> + Function("unsubscribeFromAllMessages") { installationId: String -> logV("unsubscribeFromAllMessages") - subscriptions[getMessagesKey(inboxId)]?.cancel() + subscriptions[getMessagesKey(installationId)]?.cancel() } - AsyncFunction("unsubscribeFromMessages") Coroutine { inboxId: String, id: String -> + AsyncFunction("unsubscribeFromMessages") Coroutine { installationId: String, id: String -> withContext(Dispatchers.IO) { logV("unsubscribeFromMessages") unsubscribeFromMessages( - inboxId = inboxId, + installationId = installationId, id = id ) } @@ -1253,18 +1365,41 @@ class XMTPModule : Module() { } } - private fun subscribeToConversations(inboxId: String, type: ConversationType) { - val client = clients[inboxId] ?: throw XMTPException("No client") + private fun subscribeToConsent(installationId: String) { + val client = clients[installationId] ?: throw XMTPException("No client") - subscriptions[getConversationsKey(client.inboxId)]?.cancel() - subscriptions[getConversationsKey(client.inboxId)] = + subscriptions[getConsentKey(installationId)]?.cancel() + subscriptions[getConsentKey(installationId)] = + CoroutineScope(Dispatchers.IO).launch { + try { + client.preferences.streamConsent().collect { consent -> + sendEvent( + "consent", + mapOf( + "installationId" to installationId, + "consent" to ConsentWrapper.encodeMap(consent) + ) + ) + } + } catch (e: Exception) { + Log.e("XMTPModule", "Error in group subscription: $e") + subscriptions[getConsentKey(installationId)]?.cancel() + } + } + } + + private fun subscribeToConversations(installationId: String, type: ConversationType) { + val client = clients[installationId] ?: throw XMTPException("No client") + + subscriptions[getConversationsKey(installationId)]?.cancel() + subscriptions[getConversationsKey(installationId)] = CoroutineScope(Dispatchers.IO).launch { try { client.conversations.stream(type).collect { conversation -> sendEvent( "conversation", mapOf( - "inboxId" to inboxId, + "installationId" to installationId, "conversation" to ConversationWrapper.encodeToObj( client, conversation @@ -1274,46 +1409,46 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in group subscription: $e") - subscriptions[getConversationsKey(client.inboxId)]?.cancel() + subscriptions[getConversationsKey(installationId)]?.cancel() } } } - private fun subscribeToAllMessages(inboxId: String, type: ConversationType) { - val client = clients[inboxId] ?: throw XMTPException("No client") + private fun subscribeToAllMessages(installationId: String, type: ConversationType) { + val client = clients[installationId] ?: throw XMTPException("No client") - subscriptions[getMessagesKey(inboxId)]?.cancel() - subscriptions[getMessagesKey(inboxId)] = CoroutineScope(Dispatchers.IO).launch { + subscriptions[getMessagesKey(installationId)]?.cancel() + subscriptions[getMessagesKey(installationId)] = CoroutineScope(Dispatchers.IO).launch { try { client.conversations.streamAllMessages(type).collect { message -> sendEvent( "message", mapOf( - "inboxId" to inboxId, + "installationId" to installationId, "message" to DecodedMessageWrapper.encodeMap(message), ) ) } } catch (e: Exception) { Log.e("XMTPModule", "Error in all group messages subscription: $e") - subscriptions[getMessagesKey(inboxId)]?.cancel() + subscriptions[getMessagesKey(installationId)]?.cancel() } } } - private suspend fun subscribeToMessages(inboxId: String, id: String) { - val client = clients[inboxId] ?: throw XMTPException("No client") + private suspend fun subscribeToMessages(installationId: String, id: String) { + val client = clients[installationId] ?: throw XMTPException("No client") val conversation = client.findConversation(id) ?: throw XMTPException("no conversation found for $id") - subscriptions[conversation.cacheKey(inboxId)]?.cancel() - subscriptions[conversation.cacheKey(inboxId)] = + subscriptions[conversation.cacheKey(installationId)]?.cancel() + subscriptions[conversation.cacheKey(installationId)] = CoroutineScope(Dispatchers.IO).launch { try { conversation.streamMessages().collect { message -> sendEvent( "conversationMessage", mapOf( - "inboxId" to inboxId, + "installationId" to installationId, "message" to DecodedMessageWrapper.encodeMap(message), "conversationId" to id, ) @@ -1321,26 +1456,30 @@ class XMTPModule : Module() { } } catch (e: Exception) { Log.e("XMTPModule", "Error in messages subscription: $e") - subscriptions[conversation.cacheKey(inboxId)]?.cancel() + subscriptions[conversation.cacheKey(installationId)]?.cancel() } } } - private fun getMessagesKey(inboxId: String): String { - return "messages:$inboxId" + private fun getConsentKey(installationId: String): String { + return "consent:$installationId" + } + + private fun getMessagesKey(installationId: String): String { + return "messages:$installationId" } - private fun getConversationsKey(inboxId: String): String { - return "conversations:$inboxId" + private fun getConversationsKey(installationId: String): String { + return "conversations:$installationId" } private fun unsubscribeFromMessages( - inboxId: String, + installationId: String, id: String, ) { - val client = clients[inboxId] ?: throw XMTPException("No client") + val client = clients[installationId] ?: throw XMTPException("No client") val convo = client.findConversation(id) ?: return - subscriptions[convo.cacheKey(inboxId)]?.cancel() + subscriptions[convo.cacheKey(installationId)]?.cancel() } private fun logV(msg: String) { 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 feeae6b21..1273912f3 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt @@ -8,9 +8,6 @@ class AuthParamsWrapper( val appVersion: String?, val dbDirectory: String?, val historySyncUrl: String?, - val walletType: WalletType = WalletType.EOA, - val chainId: Long?, - val blockNumber: Long?, ) { companion object { fun authParamsFromJson(authParams: String): AuthParamsWrapper { @@ -20,6 +17,21 @@ class AuthParamsWrapper( if (jsonOptions.has("appVersion")) jsonOptions.get("appVersion").asString else null, if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null, if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null, + ) + } + } +} + +class WalletParamsWrapper( + val walletType: WalletType = WalletType.EOA, + val chainId: Long?, + val blockNumber: Long?, + + ) { + companion object { + fun walletParamsFromJson(walletParams: String): WalletParamsWrapper { + val jsonOptions = JsonParser.parseString(walletParams).asJsonObject + return WalletParamsWrapper( if (jsonOptions.has("walletType")) { when (jsonOptions.get("walletType").asString) { "SCW" -> WalletType.SCW @@ -30,7 +42,8 @@ class AuthParamsWrapper( }, if (jsonOptions.has("chainId")) jsonOptions.get("chainId").asLong else null, if (jsonOptions.has("blockNumber")) jsonOptions.get("blockNumber").asLong else null, - ) + ) } } } + diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt new file mode 100644 index 000000000..1b6d984a0 --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ConsentWrapper.kt @@ -0,0 +1,30 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import com.google.gson.GsonBuilder +import org.xmtp.android.library.ConsentRecord +import org.xmtp.android.library.ConsentState + +class ConsentWrapper { + + companion object { + fun encode(model: ConsentRecord): String { + val gson = GsonBuilder().create() + val message = encodeMap(model) + return gson.toJson(message) + } + + fun encodeMap(model: ConsentRecord): 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" + } + } + } +} \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index a24051143..c4ec2128e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,5 +1,4 @@ PODS: - - BigInt (5.0.0) - boost (1.76.0) - CoinbaseWalletSDK/Client (1.0.4) - CoinbaseWalletSDK/CrossPlatform (1.0.4): @@ -7,8 +6,10 @@ PODS: - CoinbaseWalletSDKExpo (1.0.10): - CoinbaseWalletSDK/CrossPlatform (= 1.0.4) - ExpoModulesCore - - Connect-Swift (0.12.0): - - SwiftProtobuf (~> 1.25.2) + - Connect-Swift (1.0.0): + - SwiftProtobuf (~> 1.28.2) + - CryptoSwift (1.8.3) + - CSecp256k1 (0.2.0) - DoubleConversion (1.1.6) - EXApplication (5.4.0): - ExpoModulesCore @@ -49,15 +50,12 @@ PODS: - React-jsi (= 0.71.14) - ReactCommon/turbomodule/core (= 0.71.14) - fmt (6.2.1) - - GenericJSON (2.0.2) - glog (0.3.5) - - GzipSwift (5.1.1) - hermes-engine (0.71.14): - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (3.0.0) - - Logging (1.0.0) + - LibXMTP (3.0.10) - MessagePacker (0.4.7) - MMKV (2.0.0): - MMKVCore (~> 2.0.0) @@ -435,6 +433,8 @@ PODS: - React-perflogger (= 0.71.14) - RNCAsyncStorage (1.21.0): - React-Core + - RNCClipboard (1.5.1): + - React-Core - RNFS (2.20.0): - React-Core - RNScreens (3.20.0): @@ -442,23 +442,24 @@ PODS: - React-RCTImage - RNSVG (13.14.0): - React-Core - - secp256k1.swift (0.1.4) - - SwiftProtobuf (1.25.2) - - web3.swift (1.6.0): - - BigInt (~> 5.0.0) - - GenericJSON (~> 2.0) - - Logging (~> 1.0.0) - - secp256k1.swift (~> 0.1) - - XMTP (3.0.4): - - Connect-Swift (= 0.12.0) - - GzipSwift - - LibXMTP (= 3.0.0) - - web3.swift - - XMTPReactNative (0.1.0): + - SQLCipher (4.5.7): + - SQLCipher/standard (= 4.5.7) + - SQLCipher/common (4.5.7) + - SQLCipher/standard (4.5.7): + - SQLCipher/common + - SwiftProtobuf (1.28.2) + - XMTP (3.0.14): + - Connect-Swift (= 1.0.0) + - CryptoSwift (= 1.8.3) + - CSecp256k1 (~> 0.2) + - LibXMTP (= 3.0.10) + - SQLCipher (= 4.5.7) + - XMTPReactNative (3.1.1): + - CSecp256k1 (~> 0.2) - ExpoModulesCore - MessagePacker - - secp256k1.swift - - XMTP (= 3.0.4) + - SQLCipher (= 4.5.7) + - XMTP (= 3.0.14) - Yoga (1.14.0) DEPENDENCIES: @@ -525,6 +526,7 @@ DEPENDENCIES: - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" + - "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)" - RNFS (from `../node_modules/react-native-fs`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) @@ -533,22 +535,19 @@ DEPENDENCIES: SPEC REPOS: trunk: - - BigInt - CoinbaseWalletSDK - Connect-Swift + - CryptoSwift + - CSecp256k1 - fmt - - GenericJSON - - GzipSwift - libevent - LibXMTP - - Logging - MessagePacker - MMKV - MMKVCore - OpenSSL-Universal - - secp256k1.swift + - SQLCipher - SwiftProtobuf - - web3.swift - XMTP EXTERNAL SOURCES: @@ -672,6 +671,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" RNCAsyncStorage: :path: "../node_modules/@react-native-async-storage/async-storage" + RNCClipboard: + :path: "../node_modules/@react-native-community/clipboard" RNFS: :path: "../node_modules/react-native-fs" RNScreens: @@ -684,11 +685,12 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/yoga" SPEC CHECKSUMS: - BigInt: 74b4d88367b0e819d9f77393549226d36faeb0d8 boost: 57d2868c099736d80fcd648bf211b4431e51a558 CoinbaseWalletSDK: ea1f37512bbc69ebe07416e3b29bf840f5cc3152 CoinbaseWalletSDKExpo: c79420eb009f482f768c23b6768fc5b2d7c98777 - Connect-Swift: 1de2ef4a548c59ecaeb9120812dfe0d6e07a0d47 + Connect-Swift: 84e043b904f63dc93a2c01c6c125da25e765b50d + CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + CSecp256k1: 2a59c03e52637ded98896a33be4b2649392cb843 DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 EXApplication: 2a0d9abd4feace9c6faedfe541c1dec02e3702cd EXConstants: f348da07e21b23d2b085e270d7b74f282df1a7d9 @@ -706,13 +708,10 @@ SPEC CHECKSUMS: FBLazyVector: 12ea01e587c9594e7b144e1bfc86ac4d9ac28fde FBReactNativeSpec: b6ae48e67aaba46442f84d6f9ba598ccfbe2ee66 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - GenericJSON: 79a840eeb77030962e8cf02a62d36bd413b67626 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: 4ef99026c3b353bd27195b48580e1bd34d083c3a - Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 + LibXMTP: e550ccb4565023eb77f5a2eddd9d5a71cfb9d2b3 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: f7d1d5945c8765f97f39c3d121f353d46735d801 MMKVCore: c04b296010fcb1d1638f2c69405096aac12f6390 @@ -757,14 +756,14 @@ SPEC CHECKSUMS: React-runtimeexecutor: ffe826b7b1cfbc32a35ed5b64d5886c0ff75f501 ReactCommon: 7f3dd5e98a9ec627c6b03d26c062bf37ea9fc888 RNCAsyncStorage: 618d03a5f52fbccb3d7010076bc54712844c18ef + RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f RNSVG: d00c8f91c3cbf6d476451313a18f04d220d4f396 - secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 - SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 - web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: dba23b4f3bcee464ca2f7569e1dc05fd9f4c0148 - XMTPReactNative: 117d8a00b063044029c11f50320a6b9c8abcdea7 + SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 + SwiftProtobuf: 4dbaffec76a39a8dc5da23b40af1a5dc01a4c02d + XMTP: 3b586fa3703640bb5fec8a64daba9e157d9e5fdc + XMTPReactNative: f3e1cbf80b7278b817bd42982703a95a9250497d Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 0e6fe50018f34e575d38dc6a1fdf1f99c9596cdd diff --git a/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj b/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj index 0d2d68566..2400bd110 100644 --- a/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj +++ b/example/ios/xmtpreactnativesdkexample.xcodeproj/project.pbxproj @@ -293,13 +293,19 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-xmtpreactnativesdkexample/Pods-xmtpreactnativesdkexample-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/CryptoSwift/CryptoSwift.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SQLCipher/SQLCipher.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/SwiftProtobuf/SwiftProtobuf.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/CryptoSwift.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SQLCipher.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SwiftProtobuf.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/example/package.json b/example/package.json index 5fac9e8f2..2ebe867ba 100644 --- a/example/package.json +++ b/example/package.json @@ -5,10 +5,12 @@ "start": "expo start --dev-client", "android": "expo run:android", "ios": "expo run:ios", + "ios:clean": "expo run:ios --no-build-cache", "web": "expo start --web" }, "dependencies": { "@react-native-async-storage/async-storage": "^1.21.0", + "@react-native-community/clipboard": "^1.5.1", "@react-native-community/netinfo": "^11.2.0", "@react-navigation/native": "^6.1.6", "@react-navigation/native-stack": "^6.9.12", diff --git a/example/src/HomeScreen.tsx b/example/src/HomeScreen.tsx index e229a48ec..d45c57ed4 100644 --- a/example/src/HomeScreen.tsx +++ b/example/src/HomeScreen.tsx @@ -1,3 +1,4 @@ +import Clipboard from '@react-native-community/clipboard' import { NavigationContext } from '@react-navigation/native' import moment from 'moment' import React, { useContext, useEffect, useState } from 'react' @@ -8,6 +9,7 @@ import { StyleSheet, Text, View, + TouchableOpacity, } from 'react-native' import { Conversation, @@ -16,7 +18,6 @@ import { DecodedMessage, } from 'xmtp-react-native-sdk' -import { SupportedContentTypes } from './contentTypes/contentTypes' import { useConversationList } from './hooks' /// Show the user's list of conversations. @@ -56,9 +57,26 @@ export default function HomeScreen() { }} > Connected as - - {client?.address} - + + + {client?.address} + + { + if (client?.address) { + Clipboard.setString(client.address) + } + }} + style={{ + padding: 8, + backgroundColor: '#ddd', + borderRadius: 4, + marginLeft: 8, + }} + > + Copy + + } /> @@ -75,9 +93,7 @@ function ConversationItem({ client: Client | null }) { const navigation = useContext(NavigationContext) - const [messages, setMessages] = useState< - DecodedMessage[] - >([]) + const [messages, setMessages] = useState([]) const lastMessage = messages?.[0] const [consentState, setConsentState] = useState() @@ -103,11 +119,13 @@ function ConversationItem({ return ( + onPress={() => { + console.log('conversation pressed') + console.log(conversation.topic) navigation!.navigate('conversation', { - id: conversation.id, + topic: conversation.topic, }) - } + }} > []> { +}): UseQueryResult { const { client } = useXmtp() const { data: conversation } = useConversation({ topic }) - return useQuery[]>( + return useQuery( ['xmtp', 'messages', client?.address, conversation?.topic], - () => conversation!.messages(), + async () => { + await conversation!.sync() + return conversation!.messages() + }, { enabled: !!client && !!topic && !!conversation, } @@ -110,10 +113,10 @@ export function useGroupMessages({ id, }: { id: string -}): UseQueryResult[]> { +}): UseQueryResult { const { client } = useXmtp() const { data: group } = useGroup({ groupId: id }) - return useQuery[]>( + return useQuery( ['xmtp', 'groupMessages', client?.address, group?.id], async () => { await group!.sync() @@ -139,7 +142,7 @@ export function useMessage({ topic: string messageId: string }): { - message: DecodedMessage | undefined + message: DecodedMessage | undefined isSenderMe: boolean performReaction: | undefined @@ -183,7 +186,7 @@ export function useGroupMessage({ groupId: string messageId: string }): { - message: DecodedMessage | undefined + message: DecodedMessage | undefined isSenderMe: boolean performReaction: | undefined @@ -551,29 +554,26 @@ export function useSavedAddress(): { } } -export function getDbEncryptionKey( +export async function getDbEncryptionKey( network: string, clear: boolean = false -): Uint8Array { +): Promise { const key = `xmtp-${network}` - // eslint-disable-next-line no-unused-expressions - ;async () => { - const result = await EncryptedStorage.getItem(key) - if ((result && clear === true) || !result) { - if (result) { - await EncryptedStorage.removeItem(key) - } - const randomBytes = crypto.getRandomValues(new Uint8Array(32)) - const randomBytesString = uint8ArrayToHexString(randomBytes) - await EncryptedStorage.setItem(key, randomBytesString) - return randomBytes - } else { - return hexStringToUint8Array(result) + const result = await EncryptedStorage.getItem(key) + if ((result && clear === true) || !result) { + if (result) { + console.log('Removing existing dbEncryptionKey', key) + await EncryptedStorage.removeItem(key) } + + const randomBytes = crypto.getRandomValues(new Uint8Array(32)) + const randomBytesString = uint8ArrayToHexString(randomBytes) + await EncryptedStorage.setItem(key, randomBytesString) + return randomBytes + } else { + return hexStringToUint8Array(result) } - const randomBytes = crypto.getRandomValues(new Uint8Array(32)) - return randomBytes } function uint8ArrayToHexString(byteArray: Uint8Array): string { diff --git a/example/src/tests/clientTests.ts b/example/src/tests/clientTests.ts index d916ef779..b05405f94 100644 --- a/example/src/tests/clientTests.ts +++ b/example/src/tests/clientTests.ts @@ -34,6 +34,34 @@ test('can make a client', async () => { return true }) +test('static can message', async () => { + const [alix, bo] = await createClients(2) + + const addressMap = await Client.canMessage('local', [ + alix.address, + '0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67', + bo.address, + ]) + + assert( + addressMap[ + '0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67'.toLocaleLowerCase() + ] === false, + `should not be able to message 0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67` + ) + + assert( + addressMap[alix.address.toLowerCase()] === true, + `should be able to message ${alix.address}` + ) + + assert( + addressMap[bo.address.toLowerCase()] === true, + `should be able to message ${bo.address}` + ) + return true +}) + test('can revoke all other installations', async () => { const keyBytes = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, @@ -244,7 +272,7 @@ test('can drop client from memory', async () => { await anotherClient.dropLocalDatabaseConnection() await client.reconnectLocalDatabase() - await Client.dropClient(anotherClient.inboxId) + await Client.dropClient(anotherClient.installationId) try { await anotherClient.reconnectLocalDatabase() return false @@ -297,3 +325,68 @@ test('can find others inbox states', async () => { return true }) + +test('can verify signatures', async () => { + const [alix, bo] = await createClients(2) + const signature = await alix.signWithInstallationKey('a message') + + assert( + (await alix.verifySignature('a message', signature)) === true, + `message should verify` + ) + + assert( + (await alix.verifySignature('bad string', signature)) === false, + `message should not verify for bad string` + ) + + assert( + (await bo.verifySignature('a message', signature)) === false, + `message should not verify for bo` + ) + + return true +}) + +test('can add and remove accounts', async () => { + 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 alixWallet = Wallet.createRandom() + const alixWallet2 = Wallet.createRandom() + const alixWallet3 = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + }) + + await alix.addAccount(alixWallet2) + await alix.addAccount(alixWallet3) + + const inboxState = await alix.inboxState(true) + assert( + inboxState.addresses.length === 3, + `addresses length should be 3 but was ${inboxState.addresses.length}` + ) + assert( + inboxState.installations.length === 1, + `addresses length should be 1 but was ${inboxState.installations.length}` + ) + assert( + inboxState.recoveryAddress === alix.address.toLowerCase(), + `recovery address should be ${alix.address} but was ${inboxState.recoveryAddress}` + ) + + await alix.removeAccount(alixWallet, await alixWallet3.getAddress()) + const inboxState2 = await alix.inboxState(true) + assert( + inboxState2.addresses.length === 2, + `addresses length should be 2 but was ${inboxState.addresses.length}` + ) + + return true +}) diff --git a/example/src/tests/conversationTests.ts b/example/src/tests/conversationTests.ts index 573a334da..508e62ad4 100644 --- a/example/src/tests/conversationTests.ts +++ b/example/src/tests/conversationTests.ts @@ -1,13 +1,13 @@ -import { content } from '@xmtp/proto' -import ReactNativeBlobUtil from 'react-native-blob-util' +import { Wallet } from 'ethers' +import RNFS from 'react-native-fs' import { Test, assert, createClients, delayToPropogate } from './test-utils' import { Client, + ConsentRecord, Conversation, ConversationId, ConversationVersion, - JSContentCodec, } from '../../../src/index' export const conversationTests: Test[] = [] @@ -19,662 +19,685 @@ function test(name: string, perform: () => Promise) { }) } -type EncodedContent = content.EncodedContent -type ContentTypeId = content.ContentTypeId +test('can find a conversations by id', async () => { + const [alixClient, boClient] = await createClients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) -const { fs } = ReactNativeBlobUtil + await boClient.conversations.sync() + const boGroup = await boClient.conversations.findConversation(alixGroup.id) + const boDm = await boClient.conversations.findConversation(alixDm.id) + const boDm2 = await boClient.conversations.findConversation( + 'GARBAGE' as ConversationId + ) -const ContentTypeNumber: ContentTypeId = { - authorityId: 'org', - typeId: 'number', - versionMajor: 1, - versionMinor: 0, -} + assert(boDm2 === undefined, `bodm2 should be undefined`) -const ContentTypeNumberWithUndefinedFallback: ContentTypeId = { - authorityId: 'org', - typeId: 'number_undefined_fallback', - versionMajor: 1, - versionMinor: 0, -} + assert( + boGroup?.id === alixGroup.id, + `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` + ) -const ContentTypeNumberWithEmptyFallback: ContentTypeId = { - authorityId: 'org', - typeId: 'number_empty_fallback', - versionMajor: 1, - versionMinor: 0, -} + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can find a conversation by topic', async () => { + const [alixClient, boClient] = await createClients(2) + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boGroup = await boClient.conversations.findConversationByTopic( + alixGroup.topic + ) + const boDm = await boClient.conversations.findConversationByTopic( + alixDm.topic + ) + + assert( + boGroup?.id === alixGroup.id, + `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` + ) + + assert( + boDm?.id === alixDm.id, + `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` + ) + + return true +}) + +test('can find a dm by inbox id', async () => { + const [alixClient, boClient] = await createClients(2) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) -export type NumberRef = { - topNumber: { - bottomNumber: number + return true +}) + +test('can find a dm by address', async () => { + const [alixClient, boClient] = await createClients(2) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + + await boClient.conversations.sync() + const boDm = await boClient.conversations.findDmByAddress(alixClient.address) + + assert( + boDm?.id === alixDm.id, + `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` + ) + + return true +}) + +test('can filter conversations by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync() + const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.list() + const boConvosFilteredAllowed = await boClient.conversations.list( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.list( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 4, + `Conversation length should be 4 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boGroup1.id, boDm1.id].toString(), + `Conversation allowed should be ${[ + boGroup1.id, + boDm1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boGroup2?.id, boDm2?.id].toString(), + `Conversation unknown filter should be ${[ + boGroup2?.id, + boDm2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + +test('can list conversations with params', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) + + await boGroup1.send({ text: `first message` }) + await boGroup1.send({ text: `second message` }) + await boGroup1.send({ text: `third message` }) + await boDm2.send({ text: `third message` }) + await boGroup2.send({ text: `first message` }) + await boDm1.send({ text: `dm message` }) + // Order should be [Dm1, Group2, Dm2, Group1] + + await boClient.conversations.syncAllConversations() + const boConvosOrderCreated = await boClient.conversations.list() + const boConvosOrderLastMessage = await boClient.conversations.list( + { lastMessage: true }, + 'lastMessage' + ) + const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) + + assert( + boConvosOrderCreated.map((group: any) => group.id).toString() === + [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), + `Conversation created at order should be ${[ + boGroup1.id, + boGroup2.id, + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderCreated + .map((group: any) => group.id) + .toString()}` + ) + + assert( + boConvosOrderLastMessage.map((group: any) => group.id).toString() === + [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), + `Conversation last message order should be ${[ + boDm1.id, + boGroup2.id, + boDm2.id, + boGroup1.id, + ].toString()} but was ${boConvosOrderLastMessage + .map((group: any) => group.id) + .toString()}` + ) + + const messages = await boConvosOrderLastMessage[0].messages() + assert( + messages[0].content() === 'dm message', + `last message 1 should be dm message ${messages[0].content()}` + ) + // assert( + // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', + // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` + // ) + assert( + boGroupsLimit.length === 1, + `List length should be 1 but was ${boGroupsLimit.length}` + ) + assert( + boGroupsLimit[0].id === boGroup1.id, + `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` + ) + + return true +}) + +test('can list groups', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup = await boClient.conversations.newGroup([alixClient.address]) + await boClient.conversations.newGroup([ + caroClient.address, + alixClient.address, + ]) + const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) + await boClient.conversations.findOrCreateDm(alixClient.address) + + const boConversations = await boClient.conversations.list() + await alixClient.conversations.sync() + const alixConversations = await alixClient.conversations.list() + + assert( + boConversations.length === 4, + `bo conversation lengths should be 4 but was ${boConversations.length}` + ) + + assert( + alixConversations.length === 3, + `alix conversation lengths should be 3 but was ${alixConversations.length}` + ) + + if ( + boConversations[0].topic !== boGroup.topic || + boConversations[0].version !== ConversationVersion.GROUP || + boConversations[2].version !== ConversationVersion.DM || + boConversations[2].createdAt !== boDm.createdAt + ) { + throw Error('Listed containers should match streamed containers') } -} -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)), + return true +}) + +test('can list conversation messages', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup = await boClient.conversations.newGroup([alixClient.address]) + const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) + const boGroupConversation = await boClient.conversations.findConversation( + boGroup.id + ) + const boDmConversation = await boClient.conversations.findConversation( + boDm.id + ) + + await boGroupConversation?.send('hello') + await boGroupConversation?.send('hello') + await boDmConversation?.send('hello') + await boDmConversation?.send('hello') + + const boGroupMessages = await boGroupConversation?.messages() + const boDmMessages = await boDmConversation?.messages() + + assert( + boGroupMessages?.length === 3, + `bo conversation lengths should be 3 but was ${boGroupMessages?.length}` + ) + + assert( + boDmMessages?.length === 2, + `alix conversation lengths should be 2 but was ${boDmMessages?.length}` + ) + + return true +}) + +test('can stream both conversations and messages at same time', async () => { + const [alix, bo] = await createClients(2) + + let conversationCallbacks = 0 + let messageCallbacks = 0 + await bo.conversations.stream(async () => { + conversationCallbacks++ + }) + + await bo.conversations.streamAllMessages(async () => { + messageCallbacks++ + }) + + const group = await alix.conversations.newGroup([bo.address]) + const dm = await alix.conversations.findOrCreateDm(bo.address) + await delayToPropogate() + await group.send('hello') + await dm.send('hello') + await delayToPropogate() + + assert( + conversationCallbacks === 2, + 'conversation stream should have received 2 conversation' + ) + assert( + messageCallbacks === 2, + 'message stream should have received 2 message' + ) + + return true +}) + +test('can stream conversation messages', async () => { + const [alixClient, boClient] = await createClients(2) + + const alixGroup = await alixClient.conversations.newGroup([boClient.address]) + const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) + const alixGroupConversation = await alixClient.conversations.findConversation( + alixGroup.id + ) + const alixDmConversation = await alixClient.conversations.findConversation( + alixDm.id + ) + + let dmMessageCallbacks = 0 + let conversationMessageCallbacks = 0 + await alixGroupConversation?.streamMessages(async () => { + conversationMessageCallbacks++ + }) + + await alixDmConversation?.streamMessages(async () => { + dmMessageCallbacks++ + }) + + await alixGroupConversation?.send({ text: `first message` }) + await alixDmConversation?.send({ text: `first message` }) + + assert( + conversationMessageCallbacks === 1, + 'conversation stream should have received 1 conversation' + ) + assert( + dmMessageCallbacks === 1, + 'message stream should have received 1 message' + ) + + return true +}) + +test('can stream all groups and conversations', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const containers: Conversation[] = [] + await alixClient.conversations.stream( + async (conversation: Conversation) => { + containers.push(conversation) } + ) + + await boClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 1) { + throw Error( + 'Unexpected num conversations (should be 1): ' + containers.length + ) } - 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 + await boClient.conversations.findOrCreateDm(alixClient.address) + await delayToPropogate() + if ((containers.length as number) !== 2) { + throw Error( + 'Unexpected num conversations (should be 2): ' + containers.length + ) } - fallback(content: NumberRef): string | undefined { - return 'a billion' + await alixClient.conversations.findOrCreateDm(caroClient.address) + await delayToPropogate() + if (containers.length !== 3) { + throw Error( + 'Expected conversations length 3 but it is: ' + containers.length + ) } -} -class NumberCodecUndefinedFallback extends NumberCodec { - contentType = ContentTypeNumberWithUndefinedFallback - fallback(content: NumberRef): string | undefined { - return undefined + alixClient.conversations.cancelStream() + await delayToPropogate() + + await caroClient.conversations.newGroup([alixClient.address]) + await delayToPropogate() + if ((containers.length as number) !== 3) { + throw Error( + 'Unexpected num conversations (should be 3): ' + containers.length + ) } -} -class NumberCodecEmptyFallback extends NumberCodec { - contentType = ContentTypeNumberWithEmptyFallback - fallback(content: NumberRef): string | undefined { - return '' + return true +}) + +test('can streamAll from multiple clients', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream alls + const allBoConversations: any[] = [] + const allAliConversations: any[] = [] + + await bo.conversations.stream(async (conversation) => { + allBoConversations.push(conversation) + }) + await alix.conversations.stream(async (conversation) => { + allAliConversations.push(conversation) + }) + + // Start Caro starts a new conversation. + await caro.conversations.newConversation(alix.address) + await delayToPropogate() + if (allBoConversations.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + + allBoConversations.length + + ' and Alix had ' + + allAliConversations.length + ) } -} + if (allAliConversations.length !== 1) { + throw Error( + 'Unexpected all conversations count ' + allAliConversations.length + ) + } + return true +}) + +test('can streamAll from multiple clients - swapped orderring', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream alls + const allBoConversations: any[] = [] + const allAliConversations: any[] = [] + + await alix.conversations.stream(async (conversation) => { + allAliConversations.push(conversation) + }) + + await bo.conversations.stream(async (conversation) => { + allBoConversations.push(conversation) + }) + + // Start Caro starts a new conversation. + await caro.conversations.newConversation(alix.address) + await delayToPropogate() + if (allBoConversations.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + + allBoConversations.length + + ' and Alix had ' + + allAliConversations.length + ) + } + if (allAliConversations.length !== 1) { + throw Error( + 'Unexpected all conversations count ' + allAliConversations.length + ) + } + return true +}) -test('register and use custom content types', async () => { +test('can streamAllMessages from multiple clients', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream + const allBoMessages: any[] = [] + const allAliMessages: any[] = [] + + await bo.conversations.streamAllMessages(async (conversation) => { + allBoMessages.push(conversation) + }) + await alix.conversations.streamAllMessages(async (conversation) => { + allAliMessages.push(conversation) + }) + + // Start Caro starts a new conversation. + const caroConversation = await caro.conversations.newConversation( + alix.address + ) + await caroConversation.send({ text: `Message` }) + await delayToPropogate() + if (allBoMessages.length !== 0) { + throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) + } + + if (allAliMessages.length !== 1) { + throw Error( + 'Unexpected all conversations count for Ali ' + allAliMessages.length + ) + } + + return true +}) + +test('can streamAllMessages from multiple clients - swapped', async () => { + const [alix, bo, caro] = await createClients(3) + + // Setup stream + const allBoMessages: any[] = [] + const allAliMessages: any[] = [] + const caroGroup = await caro.conversations.newGroup([alix.address]) + + await alix.conversations.streamAllMessages(async (conversation) => { + allAliMessages.push(conversation) + }) + await bo.conversations.streamAllMessages(async (conversation) => { + allBoMessages.push(conversation) + }) + + // Start Caro starts a new conversation. + const caroConvo = await caro.conversations.newConversation(alix.address) + await delayToPropogate() + await caroConvo.send({ text: `Message` }) + await caroGroup.send({ text: `Message` }) + await delayToPropogate() + if (allBoMessages.length !== 0) { + throw Error( + 'Unexpected all conversations count for Bo ' + allBoMessages.length + ) + } + + if (allAliMessages.length !== 2) { + throw Error( + 'Unexpected all conversations count for Ali ' + allAliMessages.length + ) + } + + return true +}) + +test('can sync consent', async () => { + const [bo] = await createClients(1) 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 bob = await Client.createRandom({ - env: 'local', - codecs: [new NumberCodec()], - dbEncryptionKey: keyBytes, - }) - const alice = await Client.createRandom({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + const directoryExists = await RNFS.exists(dbDirPath) + if (!directoryExists) { + await RNFS.mkdir(dbDirPath) + } + const directoryExists2 = await RNFS.exists(dbDirPath2) + if (!directoryExists2) { + await RNFS.mkdir(dbDirPath2) + } + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { env: 'local', - codecs: [new NumberCodec()], + appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, }) - bob.register(new NumberCodec()) - alice.register(new NumberCodec()) + // Create DM conversation + const dm = await alix.conversations.findOrCreateDm(bo.address) + await dm.updateConsent('denied') + const consentState = await dm.consentState() + assert(consentState === 'denied', `Expected 'denied', got ${consentState}`) - await delayToPropogate() + await bo.conversations.sync() + const boDm = await bo.conversations.findConversation(dm.id) - const bobConvo = await bob.conversations.newConversation(alice.address) - await delayToPropogate() - const aliceConvo = await alice.conversations.newConversation(bob.address) + const alix2 = await Client.create(alixWallet, { + env: 'local', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, + }) - await bobConvo.send( - { topNumber: { bottomNumber: 12 } }, - { contentType: ContentTypeNumber } + const state = await alix2.inboxState(true) + assert( + state.installations.length === 2, + `Expected 2 installations, got ${state.installations.length}` ) - const messages = await aliceConvo.messages() - assert(messages.length === 1, 'did not get messages') + // Sync conversations + await bo.conversations.sync() + if (boDm) await boDm.sync() + await alix2.preferences.syncConsent() + await alix.conversations.syncAllConversations() + await delayToPropogate(2000) + await alix2.conversations.syncAllConversations() + await delayToPropogate(2000) + + const dm2 = await alix2.conversations.findConversation(dm.id) + const consentState2 = await dm2?.consentState() + assert(consentState2 === 'denied', `Expected 'denied', got ${consentState2}`) + + await alix2.preferences.setConsentState( + new ConsentRecord(dm2!.id, 'conversation_id', 'allowed') + ) - const message = messages[0] - const messageContent = message.content() + const convoState = await alix2.preferences.conversationConsentState(dm2!.id) + assert(convoState === 'allowed', `Expected 'allowed', got ${convoState}`) + const updatedConsentState = await dm2?.consentState() assert( - typeof messageContent === 'object' && - 'topNumber' in messageContent && - messageContent.topNumber.bottomNumber === 12, - 'did not get content properly: ' + JSON.stringify(messageContent) + updatedConsentState === 'allowed', + `Expected 'allowed', got ${updatedConsentState}` ) return true }) -test('handle fallback types appropriately', async () => { +test('can stream consent', async () => { + const [bo] = await createClients(1) 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 bob = await Client.createRandom({ + const dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const dbDirPath2 = `${RNFS.DocumentDirectoryPath}/xmtp_db2` + + // Ensure the directories exist + if (!(await RNFS.exists(dbDirPath))) { + await RNFS.mkdir(dbDirPath) + } + if (!(await RNFS.exists(dbDirPath2))) { + await RNFS.mkdir(dbDirPath2) + } + + const alixWallet = Wallet.createRandom() + + const alix = await Client.create(alixWallet, { env: 'local', - codecs: [ - new NumberCodecEmptyFallback(), - new NumberCodecUndefinedFallback(), - ], + appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, }) - const alice = await Client.createRandom({ + + const alixGroup = await alix.conversations.newGroup([bo.address]) + + const alix2 = await Client.create(alixWallet, { env: 'local', + appVersion: 'Testing/0.0.0', dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath2, }) - 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 alixGroup.send('Hello') + await alix.conversations.syncAllConversations() + await alix2.conversations.syncAllConversations() + + const alix2Group = await alix2.conversations.findConversation(alixGroup.id) + await delayToPropogate() - await bobConvo.send(12, { - contentType: ContentTypeNumberWithUndefinedFallback, + const consent = [] + await alix.preferences.streamConsent(async (entry: ConsentRecord) => { + consent.push(entry) }) - const messages = await aliceConvo.messages() - assert(messages.length === 2, 'did not get messages') + await delayToPropogate() - const messageUndefinedFallback = messages[0] - const messageWithDefinedFallback = messages[1] + await alix2Group!.updateConsent('denied') + const dm = await alix2.conversations.newConversation(bo.address) + await dm!.updateConsent('denied') - let message1Content = undefined - try { - message1Content = messageUndefinedFallback.content() - } catch { - message1Content = messageUndefinedFallback.fallback - } + await delayToPropogate(3000) + await alix.conversations.syncAllConversations() + await alix2.conversations.syncAllConversations() assert( - message1Content === undefined, - 'did not get content properly when empty fallback: ' + - JSON.stringify(message1Content) + consent.length === 4, + `Expected 4 consent records, got ${consent.length}` ) - - let message2Content = undefined - try { - message2Content = messageWithDefinedFallback.content() - } catch { - message2Content = messageWithDefinedFallback.fallback - } - + const updatedConsentState = await alixGroup.consentState() assert( - message2Content === '', - 'did not get content properly: ' + JSON.stringify(message2Content) + updatedConsentState === 'denied', + `Expected 'denied', got ${updatedConsentState}` ) + alix.preferences.cancelStreamConsent() + return true }) - -// test('can find a conversations by id', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boGroup = await boClient.conversations.findConversation(alixGroup.id) -// const boDm = await boClient.conversations.findConversation(alixDm.id) -// const boDm2 = await boClient.conversations.findConversation( -// 'GARBAGE' as ConversationId -// ) - -// assert(boDm2 === undefined, `bodm2 should be undefined`) - -// assert( -// boGroup?.id === alixGroup.id, -// `bo group id ${boGroup?.id} does not match alix group id ${alixGroup.id}` -// ) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a conversation by topic', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boGroup = await boClient.conversations.findConversationByTopic( -// alixGroup.topic -// ) -// const boDm = await boClient.conversations.findConversationByTopic( -// alixDm.topic -// ) - -// assert( -// boGroup?.id === alixGroup.id, -// `bo group topic ${boGroup?.id} does not match alix group topic ${alixGroup.id}` -// ) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm topic ${boDm?.id} does not match alix dm topic ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a dm by inbox id', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boDm = await boClient.conversations.findDmByInboxId(alixClient.inboxId) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can find a dm by address', async () => { -// const [alixClient, boClient] = await createClients(2) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) - -// await boClient.conversations.sync() -// const boDm = await boClient.conversations.findDmByAddress(alixClient.address) - -// assert( -// boDm?.id === alixDm.id, -// `bo dm id ${boDm?.id} does not match alix dm id ${alixDm.id}` -// ) - -// return true -// }) - -// test('can list conversations with params', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) -// const boGroup2 = await boClient.conversations.newGroup([alixClient.address]) -// const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) -// const boDm2 = await boClient.conversations.findOrCreateDm(caroClient.address) - -// await boGroup1.send({ text: `first message` }) -// await boGroup1.send({ text: `second message` }) -// await boGroup1.send({ text: `third message` }) -// await boDm2.send({ text: `third message` }) -// await boGroup2.send({ text: `first message` }) -// await boDm1.send({ text: `dm message` }) -// // Order should be [Dm1, Group2, Dm2, Group1] - -// await boClient.conversations.syncAllConversations() -// const boConvosOrderCreated = await boClient.conversations.list() -// const boConvosOrderLastMessage = await boClient.conversations.list( -// { lastMessage: true }, -// 'lastMessage' -// ) -// const boGroupsLimit = await boClient.conversations.list({}, undefined, 1) - -// assert( -// boConvosOrderCreated.map((group: any) => group.id).toString() === -// [boGroup1.id, boGroup2.id, boDm1.id, boDm2.id].toString(), -// `Conversation created at order should be ${[ -// boGroup1.id, -// boGroup2.id, -// boDm1.id, -// boDm2.id, -// ].toString()} but was ${boConvosOrderCreated -// .map((group: any) => group.id) -// .toString()}` -// ) - -// assert( -// boConvosOrderLastMessage.map((group: any) => group.id).toString() === -// [boDm1.id, boGroup2.id, boDm2.id, boGroup1.id].toString(), -// `Conversation last message order should be ${[ -// boDm1.id, -// boGroup2.id, -// boDm2.id, -// boGroup1.id, -// ].toString()} but was ${boConvosOrderLastMessage -// .map((group: any) => group.id) -// .toString()}` -// ) - -// const messages = await boConvosOrderLastMessage[0].messages() -// assert( -// messages[0].content() === 'dm message', -// `last message 1 should be dm message ${messages[0].content()}` -// ) -// // assert( -// // boConvosOrderLastMessage[0].lastMessage?.content() === 'dm message', -// // `last message 2 should be dm message ${boConvosOrderLastMessage[0].lastMessage?.content()}` -// // ) -// assert( -// boGroupsLimit.length === 1, -// `List length should be 1 but was ${boGroupsLimit.length}` -// ) -// assert( -// boGroupsLimit[0].id === boGroup1.id, -// `Group should be ${boGroup1.id} but was ${boGroupsLimit[0].id}` -// ) - -// return true -// }) - -// test('can list groups', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup = await boClient.conversations.newGroup([alixClient.address]) -// await boClient.conversations.newGroup([ -// caroClient.address, -// alixClient.address, -// ]) -// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) -// await boClient.conversations.findOrCreateDm(alixClient.address) - -// const boConversations = await boClient.conversations.list() -// await alixClient.conversations.sync() -// const alixConversations = await alixClient.conversations.list() - -// assert( -// boConversations.length === 4, -// `bo conversation lengths should be 4 but was ${boConversations.length}` -// ) - -// assert( -// alixConversations.length === 3, -// `alix conversation lengths should be 3 but was ${alixConversations.length}` -// ) - -// if ( -// boConversations[0].topic !== boGroup.topic || -// boConversations[0].version !== ConversationVersion.GROUP || -// boConversations[2].version !== ConversationVersion.DM || -// boConversations[2].createdAt !== boDm.createdAt -// ) { -// throw Error('Listed containers should match streamed containers') -// } - -// return true -// }) - -// test('can list conversation messages', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const boGroup = await boClient.conversations.newGroup([alixClient.address]) -// const boDm = await boClient.conversations.findOrCreateDm(caroClient.address) -// const boGroupConversation = await boClient.conversations.findConversation( -// boGroup.id -// ) -// const boDmConversation = await boClient.conversations.findConversation( -// boDm.id -// ) - -// await boGroupConversation?.send('hello') -// await boGroupConversation?.send('hello') -// await boDmConversation?.send('hello') -// await boDmConversation?.send('hello') - -// const boGroupMessages = await boGroupConversation?.messages() -// const boDmMessages = await boDmConversation?.messages() - -// assert( -// boGroupMessages?.length === 3, -// `bo conversation lengths should be 4 but was ${boGroupMessages?.length}` -// ) - -// assert( -// boDmMessages?.length === 3, -// `alix conversation lengths should be 3 but was ${boDmMessages?.length}` -// ) - -// return true -// }) - -// test('can stream both conversations and messages at same time', async () => { -// const [alix, bo] = await createClients(2) - -// let conversationCallbacks = 0 -// let messageCallbacks = 0 -// await bo.conversations.stream(async () => { -// conversationCallbacks++ -// }) - -// await bo.conversations.streamAllMessages(async () => { -// messageCallbacks++ -// }) - -// const group = await alix.conversations.newGroup([bo.address]) -// const dm = await alix.conversations.findOrCreateDm(bo.address) -// await delayToPropogate() -// await group.send('hello') -// await dm.send('hello') -// await delayToPropogate() - -// assert( -// conversationCallbacks === 2, -// 'conversation stream should have received 2 conversation' -// ) -// assert( -// messageCallbacks === 2, -// 'message stream should have received 2 message' -// ) - -// return true -// }) - -// test('can stream conversation messages', async () => { -// const [alixClient, boClient] = await createClients(2) - -// const alixGroup = await alixClient.conversations.newGroup([boClient.address]) -// const alixDm = await alixClient.conversations.findOrCreateDm(boClient.address) -// const alixGroupConversation = await alixClient.conversations.findConversation( -// alixGroup.id -// ) -// const alixDmConversation = await alixClient.conversations.findConversation( -// alixDm.id -// ) - -// let dmMessageCallbacks = 0 -// let conversationMessageCallbacks = 0 -// await alixGroupConversation?.streamMessages(async () => { -// conversationMessageCallbacks++ -// }) - -// await alixDmConversation?.streamMessages(async () => { -// dmMessageCallbacks++ -// }) - -// await alixGroupConversation?.send({ text: `first message` }) -// await alixDmConversation?.send({ text: `first message` }) - -// assert( -// conversationMessageCallbacks === 1, -// 'conversation stream should have received 1 conversation' -// ) -// assert( -// dmMessageCallbacks === 1, -// 'message stream should have received 1 message' -// ) - -// return true -// }) - -// test('can stream all groups and conversations', async () => { -// const [alixClient, boClient, caroClient] = await createClients(3) - -// const containers: Conversation[] = [] -// await alixClient.conversations.stream( -// async (conversation: Conversation) => { -// containers.push(conversation) -// } -// ) - -// await boClient.conversations.newGroup([alixClient.address]) -// await delayToPropogate() -// if ((containers.length as number) !== 1) { -// throw Error( -// 'Unexpected num conversations (should be 1): ' + containers.length -// ) -// } - -// await boClient.conversations.findOrCreateDm(alixClient.address) -// await delayToPropogate() -// if ((containers.length as number) !== 2) { -// throw Error( -// 'Unexpected num conversations (should be 2): ' + containers.length -// ) -// } - -// await alixClient.conversations.findOrCreateDm(caroClient.address) -// await delayToPropogate() -// if (containers.length !== 3) { -// throw Error( -// 'Expected conversations length 3 but it is: ' + containers.length -// ) -// } - -// alixClient.conversations.cancelStream() -// await delayToPropogate() - -// await caroClient.conversations.newGroup([alixClient.address]) -// await delayToPropogate() -// if ((containers.length as number) !== 3) { -// throw Error( -// 'Unexpected num conversations (should be 3): ' + containers.length -// ) -// } - -// return true -// }) - -// test('can streamAll from multiple clients', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream alls -// const allBoConversations: any[] = [] -// const allAliConversations: any[] = [] - -// await bo.conversations.stream(async (conversation) => { -// allBoConversations.push(conversation) -// }) -// await alix.conversations.stream(async (conversation) => { -// allAliConversations.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// if (allBoConversations.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + -// allBoConversations.length + -// ' and Alix had ' + -// allAliConversations.length -// ) -// } -// if (allAliConversations.length !== 1) { -// throw Error( -// 'Unexpected all conversations count ' + allAliConversations.length -// ) -// } -// return true -// }) - -// test('can streamAll from multiple clients - swapped orderring', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream alls -// const allBoConversations: any[] = [] -// const allAliConversations: any[] = [] - -// await alix.conversations.stream(async (conversation) => { -// allAliConversations.push(conversation) -// }) - -// await bo.conversations.stream(async (conversation) => { -// allBoConversations.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// if (allBoConversations.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + -// allBoConversations.length + -// ' and Alix had ' + -// allAliConversations.length -// ) -// } -// if (allAliConversations.length !== 1) { -// throw Error( -// 'Unexpected all conversations count ' + allAliConversations.length -// ) -// } -// return true -// }) - -// test('can streamAllMessages from multiple clients', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream -// const allBoMessages: any[] = [] -// const allAliMessages: any[] = [] - -// await bo.conversations.streamAllMessages(async (conversation) => { -// allBoMessages.push(conversation) -// }) -// await alix.conversations.streamAllMessages(async (conversation) => { -// allAliMessages.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// const caroConversation = await caro.conversations.newConversation( -// alix.address -// ) -// await caroConversation.send({ text: `Message` }) -// await delayToPropogate() -// if (allBoMessages.length !== 0) { -// throw Error('Unexpected all messages count for Bo ' + allBoMessages.length) -// } - -// if (allAliMessages.length !== 1) { -// throw Error( -// 'Unexpected all conversations count for Ali ' + allAliMessages.length -// ) -// } - -// return true -// }) - -// test('can streamAllMessages from multiple clients - swapped', async () => { -// const [alix, bo, caro] = await createClients(3) - -// // Setup stream -// const allBoMessages: any[] = [] -// const allAliMessages: any[] = [] -// const caroGroup = await caro.conversations.newGroup([alix.address]) - -// await alix.conversations.streamAllMessages(async (conversation) => { -// allAliMessages.push(conversation) -// }) -// await bo.conversations.streamAllMessages(async (conversation) => { -// allBoMessages.push(conversation) -// }) - -// // Start Caro starts a new conversation. -// const caroConvo = await caro.conversations.newConversation(alix.address) -// await delayToPropogate() -// await caroConvo.send({ text: `Message` }) -// await caroGroup.send({ text: `Message` }) -// await delayToPropogate() -// if (allBoMessages.length !== 0) { -// throw Error( -// 'Unexpected all conversations count for Bo ' + allBoMessages.length -// ) -// } - -// if (allAliMessages.length !== 2) { -// throw Error( -// 'Unexpected all conversations count for Ali ' + allAliMessages.length -// ) -// } - -// return true -// }) diff --git a/example/src/tests/dmTests.ts b/example/src/tests/dmTests.ts index 0c10ca24a..51fd0c367 100644 --- a/example/src/tests/dmTests.ts +++ b/example/src/tests/dmTests.ts @@ -10,6 +10,61 @@ function test(name: string, perform: () => Promise) { }) } +test('can filter dms by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + const boDm1 = await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync() + const boDm2 = await boClient.conversations.findDmByInboxId(caroClient.inboxId) + await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.listDms() + const boConvosFilteredAllowed = await boClient.conversations.listDms( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.listDms( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 2, + `Conversation length should be 2 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boDm1.id].toString(), + `Conversation allowed should be ${[ + boDm1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boDm2?.id].toString(), + `Conversation unknown filter should be ${[ + boDm2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + test('can list dms with params', async () => { const [alixClient, boClient, caroClient] = await createClients(3) @@ -37,14 +92,24 @@ test('can list dms with params', async () => { boConvosOrderCreated .map((conversation: any) => conversation.id) .toString() === [boDm1.id, boDm2.id].toString(), - `Conversation created at order should be ${[boDm1.id, boDm2.id].toString()} but was ${boConvosOrderCreated.map((convo: any) => convo.id).toString()}` + `Conversation created at order should be ${[ + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderCreated + .map((convo: any) => convo.id) + .toString()}` ) assert( boConvosOrderLastMessage .map((conversation: any) => conversation.id) .toString() === [boDm1.id, boDm2.id].toString(), - `Conversation last message order should be ${[boDm1.id, boDm2.id].toString()} but was ${boConvosOrderLastMessage.map((convo: any) => convo.id).toString()}` + `Conversation last message order should be ${[ + boDm1.id, + boDm2.id, + ].toString()} but was ${boConvosOrderLastMessage + .map((convo: any) => convo.id) + .toString()}` ) const messages = await boConvosOrderLastMessage[0].messages() diff --git a/example/src/tests/groupPerformanceTests.ts b/example/src/tests/groupPerformanceTests.ts index a18d86df8..abada6c0b 100644 --- a/example/src/tests/groupPerformanceTests.ts +++ b/example/src/tests/groupPerformanceTests.ts @@ -1,7 +1,16 @@ /* eslint-disable @typescript-eslint/no-extra-non-null-assertion */ -import { Client, Dm, Group } from 'xmtp-react-native-sdk' - -import { Test, assert, createClients } from './test-utils' +import { Wallet } from 'ethers' +import RNFS from 'react-native-fs' +import { + Client, + GroupUpdatedCodec, + ReactionCodec, + RemoteAttachmentCodec, + ReplyCodec, + StaticAttachmentCodec, +} from 'xmtp-react-native-sdk' + +import { Test, assert } from './test-utils' export const groupPerformanceTests: Test[] = [] let counter = 1 @@ -12,389 +21,84 @@ function test(name: string, perform: () => Promise) { }) } -async function createGroups( - client: Client, - peers: Client[], - numGroups: number, - numMessages: number -): Promise { - const groups = [] - const addresses: string[] = peers.map((client) => client.address) - for (let i = 0; i < numGroups; i++) { - const group = await client.conversations.newGroup(addresses, { - name: `group ${i}`, - imageUrlSquare: `www.group${i}.com`, - description: `group ${i}`, - }) - groups.push(group) - for (let i = 0; i < numMessages; i++) { - await group.send({ text: `Alix message ${i}` }) - } - } - return groups -} - -async function createDms( - client: Client, - peers: Client[], - numMessages: number -): Promise { - const dms = [] - for (let i = 0; i < peers.length; i++) { - const dm = await peers[i].conversations.findOrCreateDm(client.address) - dms.push(dm) - for (let i = 0; i < numMessages; i++) { - await dm.send({ text: `Alix message ${i}` }) - } - } - return dms -} - -let alixClient: Client -let boClient: Client -let initialPeers: Client[] -let initialGroups: Group[] -// let initialDms: Dm[] - -async function beforeAll( - groupSize: number = 1, - messages: number = 1, - peersSize: number = 1, - includeDms: boolean = false -) { - ;[alixClient] = await createClients(1) - - initialPeers = await createClients(peersSize) - boClient = initialPeers[0] - - initialGroups = await createGroups( - alixClient, - initialPeers, - groupSize, - messages - ) - - if (includeDms) { - await createDms(alixClient, initialPeers, messages) +test('building and creating', async () => { + 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 dbDirPath = `${RNFS.DocumentDirectoryPath}/xmtp_db` + const directoryExists = await RNFS.exists(dbDirPath) + if (!directoryExists) { + await RNFS.mkdir(dbDirPath) } -} - -test('test compare V3 dms', async () => { - await beforeAll(0, 0, 50, true) - let start = Date.now() - await alixClient.conversations.sync() - let end = Date.now() - console.log(`Davon synced ${50} Dms in ${end - start}ms`) - - start = Date.now() - let dms = await alixClient.conversations.list() - end = Date.now() - console.log(`Davon loaded ${dms.length} Dms in ${end - start}ms`) - - await createDms(alixClient, await createClients(5), 1) - - start = Date.now() - await alixClient.conversations.sync() - end = Date.now() - console.log(`Davon synced ${dms.length} Dms in ${end - start}ms`) - - start = Date.now() - dms = await alixClient.conversations.list() - end = Date.now() - console.log(`Davon loaded ${dms.length} Dms in ${end - start}ms`) - - return true -}) - -test('testing large group listings with ordering', async () => { - await beforeAll(1000, 10, 10) - - let start = Date.now() - let groups = await alixClient.conversations.listGroups() - let end = Date.now() - console.log(`Alix loaded ${groups.length} groups in ${end - start}ms`) - - await groups[5].send({ text: `Alix message` }) - await groups[50].send({ text: `Alix message` }) - await groups[150].send({ text: `Alix message` }) - await groups[500].send({ text: `Alix message` }) - await groups[700].send({ text: `Alix message` }) - await groups[900].send({ text: `Alix message` }) - - let start2 = Date.now() - let groups2 = await alixClient.conversations.listGroups( - { - consentState: false, - description: false, - addedByInboxId: false, - isActive: false, - lastMessage: true, - }, - 'lastMessage' - ) - let end2 = Date.now() - console.log(`Alix loaded ${groups2.length} groups in ${end2 - start2}ms`) - assert( - end2 - start2 < end - start, - 'listing 1000 groups without certain fields should take less time' - ) - - start = Date.now() - await alixClient.conversations.sync() - end = Date.now() - console.log(`Alix synced ${groups.length} groups in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 1000 cached groups should take less than a .1 second' - ) - - start = Date.now() - await boClient.conversations.sync() - end = Date.now() - console.log(`Bo synced ${groups.length} groups in ${end - start}ms`) - - start = Date.now() - await boClient.conversations.syncAllConversations() - end = Date.now() - console.log(`Bo synced all ${groups.length} groups in ${end - start}ms`) - assert( - end - start < 30000, - 'Syncing all 1000 groups should take less than a 30 second' - ) - - start = Date.now() - groups = await boClient.conversations.listGroups() - end = Date.now() - console.log(`Bo loaded ${groups.length} groups in ${end - start}ms`) + const alixWallet = Wallet.createRandom() + + const start1 = performance.now() + const alix = await Client.create(alixWallet, { + env: 'dev', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + codecs: [ + new ReactionCodec(), + new ReplyCodec(), + new GroupUpdatedCodec(), + new StaticAttachmentCodec(), + new RemoteAttachmentCodec(), + ], + }) + const end1 = performance.now() + console.log(`Created a new client in ${end1 - start1}ms`) + + const start2 = performance.now() + await Client.build(alixWallet.address, { + env: 'dev', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + codecs: [ + new ReactionCodec(), + new ReplyCodec(), + new GroupUpdatedCodec(), + new StaticAttachmentCodec(), + new RemoteAttachmentCodec(), + ], + }) + const end2 = performance.now() + console.log(`Built a client in ${end2 - start2}ms`) - start2 = Date.now() - groups2 = await boClient.conversations.listGroups( + const start3 = performance.now() + await Client.build( + alixWallet.address, { - consentState: false, - description: false, - addedByInboxId: false, - isActive: false, - lastMessage: true, + env: 'dev', + appVersion: 'Testing/0.0.0', + dbEncryptionKey: keyBytes, + dbDirectory: dbDirPath, + codecs: [ + new ReactionCodec(), + new ReplyCodec(), + new GroupUpdatedCodec(), + new StaticAttachmentCodec(), + new RemoteAttachmentCodec(), + ], }, - 'lastMessage' - ) - end2 = Date.now() - console.log(`Bo loaded ${groups2.length} groups in ${end2 - start2}ms`) - assert( - end2 - start2 < end - start, - 'listing 1000 groups without certain fields should take less time' - ) - - return true -}) - -test('testing large group listings', async () => { - await beforeAll(1000) - - let start = Date.now() - let groups = await alixClient.conversations.listGroups() - let end = Date.now() - console.log(`Alix loaded ${groups.length} groups in ${end - start}ms`) - assert( - end - start < 3000, - 'listing 1000 groups should take less than a 3 second' - ) - - start = Date.now() - await alixClient.conversations.sync() - end = Date.now() - console.log(`Alix synced ${groups.length} groups in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 1000 cached groups should take less than a .1 second' - ) - - start = Date.now() - await boClient.conversations.sync() - end = Date.now() - console.log(`Bo synced ${groups.length} groups in ${end - start}ms`) - assert( - end - start < 6000, - 'syncing 1000 groups should take less than a 6 second' + alix.inboxId ) + const end3 = performance.now() + console.log(`Built a client with inboxId in ${end3 - start3}ms`) - start = Date.now() - groups = await boClient.conversations.listGroups() - end = Date.now() - console.log(`Bo loaded ${groups.length} groups in ${end - start}ms`) assert( - end - start < 3000, - 'loading 1000 groups should take less than a 3 second' + end2 - start2 < end1 - start1, + 'building a client should be faster than creating one' ) - - return true -}) - -test('testing large message listings', async () => { - await beforeAll(1, 2000) - - const alixGroup = initialGroups[0] - let start = Date.now() - let messages = await alixGroup.messages() - let end = Date.now() - console.log(`Alix loaded ${messages.length} messages in ${end - start}ms`) - assert( - end - start < 1000, - 'listing 2000 self messages should take less than a 1 second' - ) - - start = Date.now() - await alixGroup.sync() - end = Date.now() - console.log(`Alix synced ${messages.length} messages in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 2000 self messages should take less than a .1 second' - ) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findGroup(alixGroup.id) - start = Date.now() - await boGroup!.sync() - end = Date.now() - console.log(`Bo synced ${messages.length} messages in ${end - start}ms`) - assert( - end - start < 3000, - 'syncing 2000 messages should take less than a 3 second' - ) - - start = Date.now() - messages = await boGroup!.messages() - end = Date.now() - console.log(`Bo loaded ${messages.length} messages in ${end - start}ms`) assert( - end - start < 1000, - 'loading 2000 messages should take less than a 1 second' + end3 - start3 < end1 - start1, + 'building a client with an inboxId should be faster than creating one' ) - - return true -}) - -test('testing large member listings', async () => { - await beforeAll(1, 1, 50) - - const alixGroup = initialGroups[0] - let start = Date.now() - let members = await alixGroup.members - let end = Date.now() - console.log(`Alix loaded ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'listing 50 members should take less than a .1 second' - ) - - start = Date.now() - await alixGroup.sync() - end = Date.now() - console.log(`Alix synced ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 50 members should take less than a .1 second' - ) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findGroup(alixGroup.id) - start = Date.now() - await boGroup!.sync() - end = Date.now() - console.log(`Bo synced ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 50 members should take less than a .1 second' - ) - - start = Date.now() - members = await boGroup!.members - end = Date.now() - console.log(`Bo loaded ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'loading 50 members should take less than a .1 second' - ) - - const [davonClient] = await createClients(1) - - start = Date.now() - await alixGroup.addMembers([davonClient.address]) - end = Date.now() - console.log(`Alix added 1 member in ${end - start}ms`) - assert(end - start < 100, 'adding 1 member should take less than a .1 second') - - start = Date.now() - members = await alixGroup.members - end = Date.now() - console.log(`Alix loaded ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'loading 50 member should take less than a .1 second' - ) - - start = Date.now() - await boGroup!.sync() - end = Date.now() - console.log(`Bo synced ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'syncing 50 member should take less than a .1 second' - ) - - start = Date.now() - members = await boGroup!.members - end = Date.now() - console.log(`Bo loaded ${members.length} members in ${end - start}ms`) - assert( - end - start < 100, - 'loading 50 member should take less than a .1 second' - ) - - return true -}) - -test('testing sending message in large group', async () => { - await beforeAll(1, 2000, 100) - - const alixGroup = initialGroups[0] - let start = Date.now() - await alixGroup.send({ text: `Alix message` }) - let end = Date.now() - console.log(`Alix sent a message in ${end - start}ms`) - assert( - end - start < 200, - 'sending a message should take less than a .2 second' - ) - - await boClient.conversations.sync() - const boGroup = await boClient.conversations.findGroup(alixGroup.id) - start = Date.now() - await boGroup!.prepareMessage({ text: `Bo message` }) - end = Date.now() - console.log(`Bo sent a message in ${end - start}ms`) - assert( - end - start < 100, - 'preparing a message should take less than a .1 second' - ) - - start = Date.now() - await boGroup!.sync() - end = Date.now() - console.log(`Bo synced messages in ${end - start}ms`) - assert( - end - start < 9000, - 'syncing 2000 messages should take less than a 9 second' - ) - - start = Date.now() - await boGroup!.send({ text: `Bo message 2` }) - end = Date.now() - console.log(`Bo sent a message in ${end - start}ms`) assert( - end - start < 100, - 'sending a message should take less than a .1 second' + end3 - start3 < end2 - start2, + 'building a client with an inboxId should be faster than building without' ) return true diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 267a96f73..fa0b2dcbb 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1,5 +1,4 @@ import { Wallet } from 'ethers' -import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' import { Test, @@ -14,7 +13,8 @@ import { Group, GroupUpdatedContent, GroupUpdatedCodec, - ConsentListEntry, + DecodedMessage, + ConsentRecord, } from '../../../src/index' export const groupTests: Test[] = [] @@ -671,6 +671,61 @@ test('can stream groups', async () => { return true }) +test('can filter groups by consent', async () => { + const [alixClient, boClient, caroClient] = await createClients(3) + + const boGroup1 = await boClient.conversations.newGroup([alixClient.address]) + const otherGroup = await alixClient.conversations.newGroup([boClient.address]) + await boClient.conversations.findOrCreateDm(alixClient.address) + await caroClient.conversations.findOrCreateDm(boClient.address) + await boClient.conversations.sync() + await boClient.conversations.findDmByInboxId(caroClient.inboxId) + const boGroup2 = await boClient.conversations.findGroup(otherGroup.id) + + const boConvos = await boClient.conversations.listGroups() + const boConvosFilteredAllowed = await boClient.conversations.listGroups( + {}, + undefined, + undefined, + 'allowed' + ) + const boConvosFilteredUnknown = await boClient.conversations.listGroups( + {}, + undefined, + undefined, + 'unknown' + ) + + assert( + boConvos.length === 2, + `Conversation length should be 2 but was ${boConvos.length}` + ) + + assert( + boConvosFilteredAllowed + .map((conversation: any) => conversation.id) + .toString() === [boGroup1.id].toString(), + `Conversation allowed should be ${[ + boGroup1.id, + ].toString()} but was ${boConvosFilteredAllowed + .map((convo: any) => convo.id) + .toString()}` + ) + + assert( + boConvosFilteredUnknown + .map((conversation: any) => conversation.id) + .toString() === [boGroup2?.id].toString(), + `Conversation unknown filter should be ${[ + boGroup2?.id, + ].toString()} but was ${boConvosFilteredUnknown + .map((convo: any) => convo.id) + .toString()}` + ) + + return true +}) + test('can list groups with params', async () => { const [alixClient, boClient] = await createClients(2) @@ -696,13 +751,17 @@ test('can list groups with params', async () => { assert( boGroupsOrderCreated.map((group: any) => group.id).toString() === [boGroup1.id, boGroup2.id].toString(), - `Group order should be group1 then group2 but was ${boGroupsOrderCreated.map((group: any) => group.id).toString()}` + `Group order should be group1 then group2 but was ${boGroupsOrderCreated + .map((group: any) => group.id) + .toString()}` ) assert( boGroupsOrderLastMessage.map((group: any) => group.id).toString() === [boGroup2.id, boGroup1.id].toString(), - `Group order should be group2 then group1 but was ${boGroupsOrderLastMessage.map((group: any) => group.id).toString()}` + `Group order should be group2 then group1 but was ${boGroupsOrderLastMessage + .map((group: any) => group.id) + .toString()}` ) const messages = await boGroupsOrderLastMessage[0].messages() @@ -807,12 +866,13 @@ test('canMessage', async () => { const canMessageV3 = await caro.canMessage([ caro.address, alix.address, - '0x0000000000000000000000000000000000000000', + '0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67', ]) assert( - canMessageV3['0x0000000000000000000000000000000000000000'] === false, - `should not be able to message 0x0000000000000000000000000000000000000000` + canMessageV3['0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67'.toLowerCase()] === + false, + `should not be able to message 0x4E9ce36E442e55EcD9025B9a6E0D88485d628A67` ) assert( @@ -1126,7 +1186,7 @@ test('can group consent', async () => { ) await bo.preferences.setConsentState( - new ConsentListEntry(group.id, 'conversation_id', 'denied') + new ConsentRecord(group.id, 'conversation_id', 'denied') ) const isDenied = await bo.preferences.conversationConsentState(group.id) assert(isDenied === 'denied', `bo group should be denied but was ${isDenied}`) @@ -1160,7 +1220,7 @@ test('can allow and deny a inbox id', async () => { ) await bo.preferences.setConsentState( - new ConsentListEntry(alix.inboxId, 'inbox_id', 'allowed') + new ConsentRecord(alix.inboxId, 'inbox_id', 'allowed') ) let alixMember = (await boGroup.members()).find( @@ -1184,7 +1244,7 @@ test('can allow and deny a inbox id', async () => { ) await bo.preferences.setConsentState( - new ConsentListEntry(alix.inboxId, 'inbox_id', 'denied') + new ConsentRecord(alix.inboxId, 'inbox_id', 'denied') ) alixMember = (await boGroup.members()).find( @@ -1202,7 +1262,7 @@ test('can allow and deny a inbox id', async () => { ) await bo.preferences.setConsentState( - new ConsentListEntry(alix.address, 'address', 'allowed') + new ConsentRecord(alix.address, 'address', 'allowed') ) isAddressAllowed = await bo.preferences.addressConsentState(alix.address) @@ -1447,8 +1507,8 @@ test('can sync all groups', async () => { `messages should be 4 after sync but was ${boGroup?.messages?.length}` ) assert( - numGroupsSynced === 50, - `should have synced 50 groups but synced ${numGroupsSynced}` + numGroupsSynced === 51, + `should have synced 51 groups but synced ${numGroupsSynced}` ) for (const group of groups) { @@ -1458,15 +1518,15 @@ test('can sync all groups', async () => { // 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}` + numGroupsSynced2 === 51, + `should have synced 51 groups but synced ${numGroupsSynced2}` ) // Next syncAllConversations will not sync inactive groups const numGroupsSynced3 = await bo.conversations.syncAllConversations() assert( - numGroupsSynced3 === 0, - `should have synced 0 groups but synced ${numGroupsSynced3}` + numGroupsSynced3 === 1, + `should have synced 1 groups but synced ${numGroupsSynced3}` ) return true }) diff --git a/example/src/types/typeTests.ts b/example/src/types/typeTests.ts index 226b7d49d..7accec7aa 100644 --- a/example/src/types/typeTests.ts +++ b/example/src/types/typeTests.ts @@ -2,6 +2,7 @@ import { Client, ContentTypeId, ConversationVersion, + DecodedMessage, Dm, EncodedContent, JSContentCodec, @@ -9,6 +10,7 @@ import { ReplyCodec, sendMessage, TextCodec, + DecodedMessageUnion, } from 'xmtp-react-native-sdk' const ContentTypeNumber: ContentTypeId = { @@ -169,8 +171,8 @@ export const typeTests = async () => { topNumber: { bottomNumber: 12, }, - }, - { contentType: ContentTypeNumber } + } + // { contentType: ContentTypeNumber } ) const customContentGroup = (await customContentClient.conversations.list())[0] @@ -180,8 +182,8 @@ export const typeTests = async () => { topNumber: { bottomNumber: 12, }, - }, - { contentType: ContentTypeNumber } + } + // { contentType: ContentTypeNumber } ) const customContentMessages = await customContentConvo.messages() customContentMessages[0].content() @@ -268,4 +270,56 @@ export const typeTests = async () => { // @ts-expect-error const peerAddress2 = firstConvo.peerInboxId() } + + const multiCodecClient = await Client.createRandom({ + env: 'local', + codecs: [new TextCodec(), new ReactionCodec(), new ReplyCodec()] as const, + dbEncryptionKey: keyBytes, + }) + const multiCodecConvo = (await multiCodecClient.conversations.list())[0] + const decodedMessageClient = await multiCodecConvo.messages() + + for (const message of decodedMessageClient) { + if (isReactionMessage(message)) { + const { content, action, reference, schema } = message.content() + } else if (isReplyMessage(message)) { + const { content } = message.content() + } else if (isTextMessage(message)) { + const text = message.content() + text.toLowerCase() + } + } + const textMessage = decodedMessageClient[0] as DecodedMessage + const text = textMessage.content() + text.toLowerCase() + + // Message Can infer additional codecs + const message = DecodedMessage.fromObject( + decodedMessageClient[0], + textClient + ) + if (isTextMessage(message)) { + const text = message.content() + } else { + // @ts-expect-error + message.content() + } +} + +const isTextMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('text') +} + +const isReactionMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('reaction') +} + +const isReplyMessage = ( + message: DecodedMessageUnion +): message is DecodedMessage => { + return message.contentTypeId.includes('reply') } diff --git a/example/yarn.lock b/example/yarn.lock index 98edbc3fe..6da5487d9 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -5521,6 +5521,11 @@ prompts "^2.4.0" semver "^6.3.0" +"@react-native-community/clipboard@^1.5.1": + version "1.5.1" + resolved "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz#32abb3ea2eb91ee3f9c5fb1d32d5783253c9fabe" + integrity sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA== + "@react-native-community/netinfo@^11.2.0": version "11.2.1" resolved "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz" @@ -14508,16 +14513,7 @@ strict-uri-encode@^2.0.0: resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -14591,7 +14587,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -14605,13 +14601,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" @@ -15882,7 +15871,7 @@ word-wrap@~1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -15900,15 +15889,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" diff --git a/ios/Wrappers/AuthParamsWrapper.swift b/ios/Wrappers/AuthParamsWrapper.swift index 4a9ccfd1e..04d1f517d 100644 --- a/ios/Wrappers/AuthParamsWrapper.swift +++ b/ios/Wrappers/AuthParamsWrapper.swift @@ -13,48 +13,77 @@ struct AuthParamsWrapper { let appVersion: String? let dbDirectory: String? let historySyncUrl: String? - let walletType: WalletType - let chainId: Int64? - let blockNumber: Int64? - - init(environment: String, appVersion: String?, dbDirectory: String?, historySyncUrl: String?, walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { + + init( + environment: String, appVersion: String?, dbDirectory: String?, + historySyncUrl: String? + ) { self.environment = environment self.appVersion = appVersion self.dbDirectory = dbDirectory self.historySyncUrl = historySyncUrl - self.walletType = walletType - self.chainId = chainId - self.blockNumber = blockNumber } 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, dbDirectory: nil, historySyncUrl: nil, walletType: WalletType.EOA, chainId: nil, blockNumber: nil) + let jsonOptions = try? JSONSerialization.jsonObject( + with: data, options: []) as? [String: Any] + else { + return AuthParamsWrapper( + environment: "dev", appVersion: nil, dbDirectory: nil, + historySyncUrl: nil) } let environment = jsonOptions["environment"] as? String ?? "dev" let appVersion = jsonOptions["appVersion"] as? String let dbDirectory = jsonOptions["dbDirectory"] as? String let historySyncUrl = jsonOptions["historySyncUrl"] as? String + + return AuthParamsWrapper( + environment: environment, + appVersion: appVersion, + dbDirectory: dbDirectory, + historySyncUrl: historySyncUrl + ) + } +} + +struct WalletParamsWrapper { + let walletType: WalletType + let chainId: Int64? + let blockNumber: Int64? + + init(walletType: WalletType, chainId: Int64?, blockNumber: Int64?) { + self.walletType = walletType + self.chainId = chainId + self.blockNumber = blockNumber + } + + static func walletParamsFromJson(_ walletParams: String) + -> WalletParamsWrapper + { + guard let data = walletParams.data(using: .utf8), + let jsonOptions = try? JSONSerialization.jsonObject( + with: data, options: []) as? [String: Any] + else { + return WalletParamsWrapper( + walletType: WalletType.EOA, chainId: nil, blockNumber: nil) + } + let walletTypeString = jsonOptions["walletType"] as? String ?? "EOA" let chainId = jsonOptions["chainId"] as? Int64 let blockNumber = jsonOptions["blockNumber"] as? Int64 - - let walletType = { switch walletTypeString { + + let walletType = { + switch walletTypeString { case "SCW": - return WalletType.SCW + return WalletType.SCW default: return WalletType.EOA } }() - - return AuthParamsWrapper( - environment: environment, - appVersion: appVersion, - dbDirectory: dbDirectory, - historySyncUrl: historySyncUrl, + return WalletParamsWrapper( walletType: walletType, chainId: chainId, blockNumber: blockNumber diff --git a/ios/Wrappers/ConsentWrapper.swift b/ios/Wrappers/ConsentWrapper.swift index c683925a4..2eb85e2a7 100644 --- a/ios/Wrappers/ConsentWrapper.swift +++ b/ios/Wrappers/ConsentWrapper.swift @@ -2,7 +2,7 @@ import Foundation import XMTP struct ConsentWrapper { - static func encodeToObj(_ entry: XMTP.ConsentListEntry) throws -> [String: Any] { + static func encodeToObj(_ entry: XMTP.ConsentRecord) throws -> [String: Any] { return [ "type": entry.entryType.rawValue, "value": entry.value, @@ -10,7 +10,7 @@ struct ConsentWrapper { ] } - static func encode(_ entry: XMTP.ConsentListEntry) throws -> String { + static func encode(_ entry: XMTP.ConsentRecord) throws -> String { let obj = try encodeToObj(entry) let data = try JSONSerialization.data(withJSONObject: obj) guard let result = String(data: data, encoding: .utf8) else { diff --git a/ios/Wrappers/ConversationWrapper.swift b/ios/Wrappers/ConversationWrapper.swift index c4e3af3e5..0900c1cf8 100644 --- a/ios/Wrappers/ConversationWrapper.swift +++ b/ios/Wrappers/ConversationWrapper.swift @@ -3,17 +3,16 @@ 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) async throws -> [String: Any] { + static func encodeToObj(_ conversation: XMTP.Conversation, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> [String: Any] { switch conversation { case .group(let group): - return try await GroupWrapper.encodeToObj(group, client: client) + return try await GroupWrapper.encodeToObj(group, client: client, conversationParams: conversationParams) case .dm(let dm): - return try await DmWrapper.encodeToObj(dm, client: client) + return try await DmWrapper.encodeToObj(dm, client: client, conversationParams: conversationParams) } } - - static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client) async throws -> String { - let obj = try await encodeToObj(conversation, client: client) + static func encode(_ conversation: XMTP.Conversation, client: XMTP.Client, conversationParams: ConversationParamsWrapper = ConversationParamsWrapper()) async throws -> String { + let obj = try await encodeToObj(conversation, client: client, conversationParams: conversationParams) 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 89b611009..21f3bf535 100644 --- a/ios/Wrappers/DecodedMessageWrapper.swift +++ b/ios/Wrappers/DecodedMessageWrapper.swift @@ -220,15 +220,10 @@ struct EncryptedAttachmentMetadata { } static func fromJsonObj(_ obj: [String: Any]) throws -> EncryptedAttachmentMetadata { - guard let secret = (obj["secret"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } - guard let salt = (obj["salt"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } - guard let nonce = (obj["nonce"] as? String ?? "").web3.hexData else { - throw Error.badRemoteAttachmentMetadata - } + let secret = (obj["secret"] as? String ?? "").hexToData + let salt = (obj["salt"] as? String ?? "").hexToData + let nonce = (obj["nonce"] as? String ?? "").hexToData + return EncryptedAttachmentMetadata( filename: obj["filename"] as? String ?? "", secret: secret, diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 94f6910a9..5a51e5546 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -4,12 +4,15 @@ import OSLog import XMTP extension Conversation { - static func cacheKeyForTopic(inboxId: String, topic: String) -> String { - return "\(inboxId):\(topic)" + static func cacheKeyForTopic(installationId: String, topic: String) + -> String + { + return "\(installationId):\(topic)" } - func cacheKey(_ inboxId: String) -> String { - return Conversation.cacheKeyForTopic(inboxId: inboxId, topic: topic) + func cacheKey(_ installationId: String) -> String { + return Conversation.cacheKeyForTopic( + installationId: installationId, topic: topic) } } @@ -71,13 +74,24 @@ public class XMTPModule: Module { } } - enum Error: Swift.Error { + enum Error: Swift.Error, LocalizedError { case noClient case conversationNotFound(String) - case noMessage, invalidKeyBundle, invalidDigest - case badPreparation(String) - case mlsNotEnabled(String) - case invalidString, invalidPermissionOption + case noMessage + case invalidPermissionOption + + var errorDescription: String? { + switch self { + case .noClient: + return "No client is available." + case .conversationNotFound(let id): + return "Conversation with ID '\(id)' was not found." + case .noMessage: + return "No message was provided." + case .invalidPermissionOption: + return "The permission option is invalid." + } + } } public func definition() -> ModuleDefinition { @@ -89,19 +103,22 @@ public class XMTPModule: Module { "preAuthenticateToInboxCallback", "conversation", "message", - "conversationMessage" + "conversationMessage", + "consent" ) - AsyncFunction("address") { (inboxId: String) -> String in - if let client = await clientsManager.getClient(key: inboxId) { + AsyncFunction("address") { (installationId: String) -> String in + if let client = await clientsManager.getClient(key: installationId) + { return client.address } else { return "No Client." } } - AsyncFunction("inboxId") { (inboxId: String) -> String in - if let client = await clientsManager.getClient(key: inboxId) { + AsyncFunction("inboxId") { (installationId: String) -> String in + if let client = await clientsManager.getClient(key: installationId) + { return client.inboxID } else { return "No Client." @@ -109,62 +126,56 @@ public class XMTPModule: Module { } AsyncFunction("findInboxIdFromAddress") { - (inboxId: String, address: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, address: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } return try await client.inboxIdFromAddress(address: address) } - AsyncFunction("deleteLocalDatabase") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("deleteLocalDatabase") { (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } try client.deleteLocalDatabase() } - AsyncFunction("dropLocalDatabaseConnection") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("dropLocalDatabaseConnection") { + (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } try client.dropLocalDatabaseConnection() } - AsyncFunction("reconnectLocalDatabase") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("reconnectLocalDatabase") { (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } try await client.reconnectLocalDatabase() } - AsyncFunction("requestMessageHistorySync") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("requestMessageHistorySync") { (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } try await client.requestMessageHistorySync() } - AsyncFunction("revokeAllOtherInstallations") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) - else { - throw Error.noClient - } - 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) + (installationId: String, refreshFromNetwork: Bool) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -174,9 +185,13 @@ public class XMTPModule: Module { } AsyncFunction("getInboxStates") { - (inboxId: String, refreshFromNetwork: Bool, inboxIds: [String]) + ( + installationId: String, refreshFromNetwork: Bool, + inboxIds: [String] + ) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -219,35 +234,33 @@ public class XMTPModule: Module { hasAuthenticateToInboxCallback ?? false ? self.preAuthenticateToInboxCallback : nil let encryptionKeyData = Data(dbEncryptionKey) - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) - let options = createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + let options = self.createClientConfig( + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await Client.create( account: privateKey, options: options) await clientsManager.updateClient( - key: client.inboxID, client: client) + key: client.installationID, client: client) return try ClientWrapper.encodeToObj(client) } AsyncFunction("create") { ( address: String, hasAuthenticateToInboxCallback: Bool?, - dbEncryptionKey: [UInt8], authParams: String + dbEncryptionKey: [UInt8], authParams: String, + walletParams: String ) in - let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) + let walletOptions = WalletParamsWrapper.walletParamsFromJson( + walletParams) let signer = ReactNativeSigner( module: self, address: address, - walletType: authOptions.walletType, - chainId: authOptions.chainId, - blockNumber: authOptions.blockNumber) + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) self.signer = signer if hasAuthenticateToInboxCallback ?? false { self.preAuthenticateToInboxCallbackDeferred = DispatchSemaphore( @@ -259,50 +272,137 @@ public class XMTPModule: Module { let encryptionKeyData = Data(dbEncryptionKey) let options = self.createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await XMTP.Client.create( account: signer, options: options) await self.clientsManager.updateClient( - key: client.inboxID, client: client) + key: client.installationID, client: client) self.signer = nil self.sendEvent("authed", try ClientWrapper.encodeToObj(client)) } AsyncFunction("build") { - (address: String, dbEncryptionKey: [UInt8], authParams: String) + ( + address: String, inboxId: String?, dbEncryptionKey: [UInt8], + authParams: String + ) -> [String: String] in let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) let encryptionKeyData = Data(dbEncryptionKey) let options = self.createClientConfig( - env: authOptions.environment, - appVersion: authOptions.appVersion, - preAuthenticateToInboxCallback: nil, + authParams: authParams, dbEncryptionKey: encryptionKeyData, - dbDirectory: authOptions.dbDirectory, - historySyncUrl: authOptions.historySyncUrl + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback ) let client = try await XMTP.Client.build( - address: address, options: options) + address: address, options: options, inboxId: inboxId) await clientsManager.updateClient( - key: client.inboxID, client: client) + key: client.installationID, client: client) return try ClientWrapper.encodeToObj(client) } + AsyncFunction("revokeAllOtherInstallations") { + (installationId: String, walletParams: String) in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson( + walletParams) + let signer = ReactNativeSigner( + module: self, address: client.address, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.revokeAllOtherInstallations(signingKey: signer) + self.signer = nil + } + + AsyncFunction("addAccount") { + (installationId: String, newAddress: String, walletParams: String) + in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson( + walletParams) + let signer = ReactNativeSigner( + module: self, address: newAddress, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.addAccount(newAccount: signer) + self.signer = nil + } + + AsyncFunction("removeAccount") { + ( + installationId: String, addressToRemove: String, + walletParams: String + ) in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let walletOptions = WalletParamsWrapper.walletParamsFromJson( + walletParams) + let signer = ReactNativeSigner( + module: self, address: client.address, + walletType: walletOptions.walletType, + chainId: walletOptions.chainId, + blockNumber: walletOptions.blockNumber) + self.signer = signer + + try await client.removeAccount( + recoveryAccount: signer, addressToRemove: addressToRemove) + self.signer = nil + } + // Remove a client from memory for a given inboxId - AsyncFunction("dropClient") { (inboxId: String) in - await clientsManager.dropClient(key: inboxId) + AsyncFunction("dropClient") { (installationId: String) in + await clientsManager.dropClient(key: installationId) + } + + AsyncFunction("signWithInstallationKey") { + (installationId: String, message: String) -> [UInt8] in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + let signature = try client.signWithInstallationKey(message: message) + return [UInt8](signature) + } + + AsyncFunction("verifySignature") { + (installationId: String, message: String, signature: [UInt8]) + -> Bool in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + return try client.verifySignature( + message: message, signature: Data(signature)) } AsyncFunction("canMessage") { - (inboxId: String, peerAddresses: [String]) -> [String: Bool] in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, peerAddresses: [String]) -> [String: Bool] + in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -310,6 +410,14 @@ public class XMTPModule: Module { return try await client.canMessage(addresses: peerAddresses) } + AsyncFunction("staticCanMessage") { + (environment: String, peerAddresses: [String]) -> [String: Bool] in + return try await XMTP.Client.canMessage( + accountAddresses: peerAddresses, + api: createApiClient(env: environment) + ) + } + AsyncFunction("getOrCreateInboxId") { (address: String, environment: String) -> String in do { @@ -322,8 +430,9 @@ public class XMTPModule: Module { } AsyncFunction("encryptAttachment") { - (inboxId: String, fileJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, fileJson: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -352,8 +461,9 @@ public class XMTPModule: Module { } AsyncFunction("decryptAttachment") { - (inboxId: String, encryptedFileJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, encryptedFileJson: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -384,10 +494,12 @@ public class XMTPModule: Module { AsyncFunction("listGroups") { ( - inboxId: String, groupParams: String?, sortOrder: String?, - limit: Int? + installationId: String, groupParams: String?, + sortOrder: String?, + limit: Int?, consentState: String? ) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -395,9 +507,14 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } var groupList: [Group] = try await client.conversations.listGroups( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for group in groupList { @@ -410,10 +527,12 @@ public class XMTPModule: Module { AsyncFunction("listDms") { ( - inboxId: String, groupParams: String?, sortOrder: String?, - limit: Int? + installationId: String, groupParams: String?, + sortOrder: String?, + limit: Int?, consentState: String? ) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -421,9 +540,14 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( groupParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") - + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } var dmList: [Dm] = try await client.conversations.listDms( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for dm in dmList { @@ -436,10 +560,11 @@ public class XMTPModule: Module { AsyncFunction("listConversations") { ( - inboxId: String, conversationParams: String?, - sortOrder: String?, limit: Int? + installationId: String, conversationParams: String?, + sortOrder: String?, limit: Int?, consentState: String? ) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -447,14 +572,21 @@ public class XMTPModule: Module { let params = ConversationParamsWrapper.conversationParamsFromJson( conversationParams ?? "") let order = getConversationSortOrder(order: sortOrder ?? "") + let consent: ConsentState? + if let state = consentState { + consent = try getConsentState(state: state) + } else { + consent = nil + } let conversations = try await client.conversations.list( - limit: limit, order: order) + limit: limit, order: order, consentState: consent) var results: [String] = [] for conversation in conversations { let encodedConversationContainer = try await ConversationWrapper.encode( - conversation, client: client) + conversation, client: client, conversationParams: params + ) results.append(encodedConversationContainer) } return results @@ -462,10 +594,11 @@ public class XMTPModule: Module { AsyncFunction("conversationMessages") { ( - inboxId: String, conversationId: String, limit: Int?, + installationId: String, conversationId: String, limit: Int?, beforeNs: Double?, afterNs: Double?, direction: String? ) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -498,8 +631,9 @@ public class XMTPModule: Module { } AsyncFunction("findMessage") { - (inboxId: String, messageId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, messageId: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -512,8 +646,9 @@ public class XMTPModule: Module { } AsyncFunction("findGroup") { - (inboxId: String, groupId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, groupId: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -525,8 +660,9 @@ public class XMTPModule: Module { } AsyncFunction("findConversation") { - (inboxId: String, conversationId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, conversationId: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -542,8 +678,9 @@ public class XMTPModule: Module { } AsyncFunction("findConversationByTopic") { - (inboxId: String, topic: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, topic: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -558,8 +695,9 @@ public class XMTPModule: Module { } AsyncFunction("findDmByInboxId") { - (inboxId: String, peerInboxId: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, peerInboxId: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -571,8 +709,9 @@ public class XMTPModule: Module { } AsyncFunction("findDmByAddress") { - (inboxId: String, peerAddress: String) -> String? in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, peerAddress: String) -> String? in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -584,8 +723,10 @@ public class XMTPModule: Module { } AsyncFunction("sendMessage") { - (inboxId: String, id: String, contentJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, contentJson: String) -> String + in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -605,8 +746,9 @@ public class XMTPModule: Module { } AsyncFunction("publishPreparedMessages") { - (inboxId: String, id: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -622,8 +764,10 @@ public class XMTPModule: Module { } AsyncFunction("prepareMessage") { - (inboxId: String, id: String, contentJson: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, contentJson: String) -> String + in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -643,8 +787,9 @@ public class XMTPModule: Module { } AsyncFunction("findOrCreateDm") { - (inboxId: String, peerAddress: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, peerAddress: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -661,10 +806,12 @@ public class XMTPModule: Module { AsyncFunction("createGroup") { ( - inboxId: String, peerAddresses: [String], permission: String, + installationId: String, peerAddresses: [String], + permission: String, groupOptionsJson: String ) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -697,10 +844,11 @@ public class XMTPModule: Module { AsyncFunction("createGroupCustomPermissions") { ( - inboxId: String, peerAddresses: [String], + installationId: String, peerAddresses: [String], permissionPolicySetJson: String, groupOptionsJson: String ) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -728,8 +876,9 @@ public class XMTPModule: Module { } AsyncFunction("listMemberInboxIds") { - (inboxId: String, groupId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, groupId: String) -> [String] in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -741,8 +890,9 @@ public class XMTPModule: Module { } AsyncFunction("dmPeerInboxId") { - (inboxId: String, dmId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, dmId: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -763,13 +913,15 @@ public class XMTPModule: Module { } AsyncFunction("listConversationMembers") { - (inboxId: String, conversationId: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, conversationId: String) -> [String] in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -785,24 +937,29 @@ public class XMTPModule: Module { } } - AsyncFunction("syncConversations") { (inboxId: String) in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("syncConversations") { (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } try await client.conversations.sync() } - AsyncFunction("syncAllConversations") { (inboxId: String) -> UInt32 in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("syncAllConversations") { + (installationId: String) -> UInt32 in + guard + let client = await clientsManager.getClient(key: installationId) 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) + AsyncFunction("syncConversation") { + (installationId: String, id: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -817,8 +974,9 @@ public class XMTPModule: Module { } AsyncFunction("addGroupMembers") { - (inboxId: String, id: String, peerAddresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, peerAddresses: [String]) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -830,8 +988,9 @@ public class XMTPModule: Module { } AsyncFunction("removeGroupMembers") { - (inboxId: String, id: String, peerAddresses: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, peerAddresses: [String]) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -843,8 +1002,9 @@ public class XMTPModule: Module { } AsyncFunction("addGroupMembersByInboxId") { - (inboxId: String, id: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, inboxIds: [String]) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -856,8 +1016,9 @@ public class XMTPModule: Module { } AsyncFunction("removeGroupMembersByInboxId") { - (inboxId: String, id: String, inboxIds: [String]) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, inboxIds: [String]) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -869,8 +1030,10 @@ public class XMTPModule: Module { try await group.removeMembersByInboxId(inboxIds: inboxIds) } - AsyncFunction("groupName") { (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + AsyncFunction("groupName") { + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -883,8 +1046,9 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupName") { - (inboxId: String, id: String, groupName: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, groupName: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -897,8 +1061,9 @@ public class XMTPModule: Module { } AsyncFunction("groupImageUrlSquare") { - (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -911,8 +1076,9 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupImageUrlSquare") { - (inboxId: String, id: String, groupImageUrl: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, groupImageUrl: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -926,8 +1092,9 @@ public class XMTPModule: Module { } AsyncFunction("groupDescription") { - (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -940,8 +1107,9 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupDescription") { - (inboxId: String, id: String, description: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, description: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -955,8 +1123,9 @@ public class XMTPModule: Module { } AsyncFunction("groupPinnedFrameUrl") { - (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -969,8 +1138,9 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupPinnedFrameUrl") { - (inboxId: String, id: String, pinnedFrameUrl: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, pinnedFrameUrl: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -984,8 +1154,9 @@ public class XMTPModule: Module { } AsyncFunction("isGroupActive") { - (inboxId: String, id: String) -> Bool in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> Bool in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -998,8 +1169,9 @@ public class XMTPModule: Module { } AsyncFunction("addedByInboxId") { - (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1012,8 +1184,9 @@ public class XMTPModule: Module { } AsyncFunction("creatorInboxId") { - (inboxId: String, id: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1025,9 +1198,11 @@ public class XMTPModule: Module { } AsyncFunction("isAdmin") { - (clientInboxId: String, id: String, inboxId: String) -> Bool in + (clientInstallationId: String, id: String, inboxId: String) -> Bool + in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1039,9 +1214,11 @@ public class XMTPModule: Module { } AsyncFunction("isSuperAdmin") { - (clientInboxId: String, id: String, inboxId: String) -> Bool in + (clientInstallationId: String, id: String, inboxId: String) -> Bool + in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1053,8 +1230,9 @@ public class XMTPModule: Module { } AsyncFunction("listAdmins") { - (inboxId: String, id: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> [String] in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1066,8 +1244,9 @@ public class XMTPModule: Module { } AsyncFunction("listSuperAdmins") { - (inboxId: String, id: String) -> [String] in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) -> [String] in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1079,9 +1258,10 @@ public class XMTPModule: Module { } AsyncFunction("addAdmin") { - (clientInboxId: String, id: String, inboxId: String) in + (clientInstallationId: String, id: String, inboxId: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1093,9 +1273,10 @@ public class XMTPModule: Module { } AsyncFunction("addSuperAdmin") { - (clientInboxId: String, id: String, inboxId: String) in + (clientInstallationId: String, id: String, inboxId: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1107,9 +1288,10 @@ public class XMTPModule: Module { } AsyncFunction("removeAdmin") { - (clientInboxId: String, id: String, inboxId: String) in + (clientInstallationId: String, id: String, inboxId: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1121,9 +1303,10 @@ public class XMTPModule: Module { } AsyncFunction("removeSuperAdmin") { - (clientInboxId: String, id: String, inboxId: String) in + (clientInstallationId: String, id: String, inboxId: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1135,9 +1318,10 @@ public class XMTPModule: Module { } AsyncFunction("updateAddMemberPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1151,9 +1335,10 @@ public class XMTPModule: Module { } AsyncFunction("updateRemoveMemberPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1167,9 +1352,10 @@ public class XMTPModule: Module { } AsyncFunction("updateAddAdminPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1183,9 +1369,10 @@ public class XMTPModule: Module { } AsyncFunction("updateRemoveAdminPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1199,9 +1386,10 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupNamePermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1215,9 +1403,10 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupImageUrlSquarePermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1231,9 +1420,10 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupDescriptionPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1247,9 +1437,10 @@ public class XMTPModule: Module { } AsyncFunction("updateGroupPinnedFrameUrlPermission") { - (clientInboxId: String, id: String, newPermission: String) in + (clientInstallationId: String, id: String, newPermission: String) in guard - let client = await clientsManager.getClient(key: clientInboxId) + let client = await clientsManager.getClient( + key: clientInstallationId) else { throw Error.noClient } @@ -1263,8 +1454,9 @@ public class XMTPModule: Module { } AsyncFunction("permissionPolicySet") { - (inboxId: String, id: String) async throws -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String) async throws -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1280,8 +1472,10 @@ public class XMTPModule: Module { } AsyncFunction("processMessage") { - (inboxId: String, id: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, id: String, encryptedMessage: String) + -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1307,8 +1501,9 @@ public class XMTPModule: Module { } AsyncFunction("processWelcomeMessage") { - (inboxId: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, encryptedMessage: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1329,12 +1524,23 @@ public class XMTPModule: Module { conversation, client: client) } + AsyncFunction("syncConsent") { (installationId: String) in + guard + let client = await clientsManager.getClient(key: installationId) + else { + throw Error.noClient + } + + try await client.preferences.syncConsent() + } + AsyncFunction("setConsentState") { ( - inboxId: String, value: String, entryType: String, + installationId: String, value: String, entryType: String, consentType: String ) in - guard let client = await clientsManager.getClient(key: inboxId) + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1342,9 +1548,9 @@ public class XMTPModule: Module { let resolvedEntryType = try getEntryType(type: entryType) let resolvedConsentState = try getConsentState(state: consentType) - try await client.preferences.consentList.setConsentState( + try await client.preferences.setConsentState( entries: [ - ConsentListEntry( + ConsentRecord( value: value, entryType: resolvedEntryType, consentType: resolvedConsentState @@ -1354,42 +1560,46 @@ public class XMTPModule: Module { } AsyncFunction("consentAddressState") { - (inboxId: String, address: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, address: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } return try await ConsentWrapper.consentStateToString( - state: client.preferences.consentList.addressState( + state: client.preferences.addressState( address: address)) } AsyncFunction("consentInboxIdState") { - (inboxId: String, peerInboxId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, peerInboxId: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } return try await ConsentWrapper.consentStateToString( - state: client.preferences.consentList.inboxIdState( + state: client.preferences.inboxIdState( inboxId: peerInboxId)) } AsyncFunction("consentConversationIdState") { - (inboxId: String, conversationId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, conversationId: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } return try await ConsentWrapper.consentStateToString( - state: client.preferences.consentList.conversationState( + state: client.preferences.conversationState( conversationId: conversationId)) } AsyncFunction("conversationConsentState") { - (inboxId: String, conversationId: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, conversationId: String) -> String in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1406,8 +1616,9 @@ public class XMTPModule: Module { } AsyncFunction("updateConversationConsent") { - (inboxId: String, conversationId: String, state: String) in - guard let client = await clientsManager.getClient(key: inboxId) + (installationId: String, conversationId: String, state: String) in + guard + let client = await clientsManager.getClient(key: installationId) else { throw Error.noClient } @@ -1424,37 +1635,57 @@ public class XMTPModule: Module { state: getConsentState(state: state)) } + AsyncFunction("subscribeToConsent") { + (installationId: String) in + + try await subscribeToConsent( + installationId: installationId) + } + AsyncFunction("subscribeToConversations") { - (inboxId: String, type: String) in + (installationId: String, type: String) in try await subscribeToConversations( - inboxId: inboxId, type: getConversationType(type: type)) + installationId: installationId, + type: getConversationType(type: type)) } AsyncFunction("subscribeToAllMessages") { - (inboxId: String, type: String) in + (installationId: String, type: String) in try await subscribeToAllMessages( - inboxId: inboxId, type: getConversationType(type: type)) + installationId: installationId, + type: getConversationType(type: type)) } AsyncFunction("subscribeToMessages") { - (inboxId: String, id: String) in - try await subscribeToMessages(inboxId: inboxId, id: id) + (installationId: String, id: String) in + try await subscribeToMessages( + installationId: installationId, id: id) } - AsyncFunction("unsubscribeFromConversations") { (inboxId: String) in + AsyncFunction("unsubscribeFromConsent") { (installationId: String) in await subscriptionsManager.get( - getConversationsKey(inboxId: inboxId))?.cancel() + getConsentKey(installationId: installationId))? + .cancel() + } + + AsyncFunction("unsubscribeFromConversations") { + (installationId: String) in + await subscriptionsManager.get( + getConversationsKey(installationId: installationId))?.cancel() } - AsyncFunction("unsubscribeFromAllMessages") { (inboxId: String) in - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))? + AsyncFunction("unsubscribeFromAllMessages") { + (installationId: String) in + await subscriptionsManager.get( + getMessagesKey(installationId: installationId))? .cancel() } AsyncFunction("unsubscribeFromMessages") { - (inboxId: String, id: String) in - try await unsubscribeFromMessages(inboxId: inboxId, id: id) + (installationId: String, id: String) in + try await unsubscribeFromMessages( + installationId: installationId, id: id) } AsyncFunction("registerPushToken") { @@ -1628,30 +1859,70 @@ public class XMTPModule: Module { } func createClientConfig( - env: String, appVersion: String?, - preAuthenticateToInboxCallback: PreEventCallback? = nil, - dbEncryptionKey: Data, dbDirectory: String? = nil, - historySyncUrl: String? = nil + authParams: String, dbEncryptionKey: Data, + preAuthenticateToInboxCallback: PreEventCallback? = nil ) -> XMTP.ClientOptions { + let authOptions = AuthParamsWrapper.authParamsFromJson(authParams) return XMTP.ClientOptions( - api: createApiClient(env: env, appVersion: appVersion), + api: createApiClient( + env: authOptions.environment, appVersion: authOptions.appVersion + ), preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - dbEncryptionKey: dbEncryptionKey, dbDirectory: dbDirectory, - historySyncUrl: historySyncUrl) + dbEncryptionKey: dbEncryptionKey, + dbDirectory: authOptions.dbDirectory, + historySyncUrl: authOptions.historySyncUrl) + } + + func subscribeToConsent(installationId: String) + async throws + { + guard let client = await clientsManager.getClient(key: installationId) + else { + return + } + + await subscriptionsManager.get( + getConsentKey(installationId: installationId))? + .cancel() + await subscriptionsManager.set( + getConsentKey(installationId: installationId), + Task { + do { + for try await consent in await client.preferences + .streamConsent() + { + try sendEvent( + "consent", + [ + "installationId": installationId, + "consent": ConsentWrapper.encodeToObj( + consent), + ]) + } + } catch { + print("Error in consent subscription: \(error)") + await subscriptionsManager.get( + getConsentKey(installationId: installationId))?.cancel() + } + }) } - func subscribeToConversations(inboxId: String, type: ConversationType) + func subscribeToConversations( + installationId: String, type: ConversationType + ) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: installationId) + else { return } - await subscriptionsManager.get(getConversationsKey(inboxId: inboxId))? + await subscriptionsManager.get( + getConversationsKey(installationId: installationId))? .cancel() await subscriptionsManager.set( - getConversationsKey(inboxId: inboxId), + getConversationsKey(installationId: installationId), Task { do { for try await conversation in await client.conversations @@ -1660,7 +1931,7 @@ public class XMTPModule: Module { try await sendEvent( "conversation", [ - "inboxId": inboxId, + "installationId": installationId, "conversation": ConversationWrapper.encodeToObj( conversation, client: client), ]) @@ -1668,22 +1939,25 @@ public class XMTPModule: Module { } catch { print("Error in all conversations subscription: \(error)") await subscriptionsManager.get( - getConversationsKey(inboxId: inboxId))?.cancel() + getConversationsKey(installationId: installationId))? + .cancel() } }) } - func subscribeToAllMessages(inboxId: String, type: ConversationType) + func subscribeToAllMessages(installationId: String, type: ConversationType) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { + guard let client = await clientsManager.getClient(key: installationId) + else { return } - await subscriptionsManager.get(getMessagesKey(inboxId: inboxId))? + await subscriptionsManager.get( + getMessagesKey(installationId: installationId))? .cancel() await subscriptionsManager.set( - getMessagesKey(inboxId: inboxId), + getMessagesKey(installationId: installationId), Task { do { for try await message in await client.conversations @@ -1692,7 +1966,7 @@ public class XMTPModule: Module { try sendEvent( "message", [ - "inboxId": inboxId, + "installationId": installationId, "message": DecodedMessageWrapper.encodeToObj( message, client: client), ]) @@ -1700,13 +1974,15 @@ public class XMTPModule: Module { } catch { print("Error in all messages subscription: \(error)") await subscriptionsManager.get( - getMessagesKey(inboxId: inboxId))?.cancel() + getMessagesKey(installationId: installationId))? + .cancel() } }) } - func subscribeToMessages(inboxId: String, id: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { + func subscribeToMessages(installationId: String, id: String) async throws { + guard let client = await clientsManager.getClient(key: installationId) + else { throw Error.noClient } @@ -1715,10 +1991,10 @@ public class XMTPModule: Module { return } - await subscriptionsManager.get(converation.cacheKey(client.inboxID))? + await subscriptionsManager.get(converation.cacheKey(installationId))? .cancel() await subscriptionsManager.set( - converation.cacheKey(client.inboxID), + converation.cacheKey(installationId), Task { do { for try await message in converation.streamMessages() { @@ -1726,7 +2002,7 @@ public class XMTPModule: Module { try sendEvent( "conversationMessage", [ - "inboxId": inboxId, + "installationId": installationId, "message": DecodedMessageWrapper.encodeToObj( message, client: client), @@ -1741,13 +2017,16 @@ public class XMTPModule: Module { } catch { print("Error in group messages subscription: \(error)") await subscriptionsManager.get( - converation.cacheKey(inboxId))?.cancel() + converation.cacheKey(installationId))?.cancel() } }) } - func unsubscribeFromMessages(inboxId: String, id: String) async throws { - guard let client = await clientsManager.getClient(key: inboxId) else { + func unsubscribeFromMessages(installationId: String, id: String) + async throws + { + guard let client = await clientsManager.getClient(key: installationId) + else { throw Error.noClient } @@ -1756,20 +2035,24 @@ public class XMTPModule: Module { return } - await subscriptionsManager.get(converation.cacheKey(inboxId))? + await subscriptionsManager.get(converation.cacheKey(installationId))? .cancel() } - func getMessagesKey(inboxId: String) -> String { - return "messages:\(inboxId)" + func getConsentKey(installationId: String) -> String { + return "consent:\(installationId)" + } + + func getMessagesKey(installationId: String) -> String { + return "messages:\(installationId)" } - func getConversationsKey(inboxId: String) -> String { - return "conversations:\(inboxId)" + func getConversationsKey(installationId: String) -> String { + return "conversations:\(installationId)" } - func getConversationMessagesKey(inboxId: String) -> String { - return "conversationMessages:\(inboxId)" + func getConversationMessagesKey(installationId: String) -> String { + return "conversationMessages:\(installationId)" } func preAuthenticateToInboxCallback() { diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 9bf755fcb..4756572ce 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -24,7 +24,9 @@ Pod::Spec.new do |s| } s.source_files = "**/*.{h,m,swift}" - s.dependency 'secp256k1.swift' + s.dependency "MessagePacker" - s.dependency "XMTP", "= 3.0.4" + s.dependency "XMTP", "= 3.0.14" + s.dependency 'CSecp256k1', '~> 0.2' + s.dependency "SQLCipher", "= 4.5.7" end diff --git a/package.json b/package.json index 595978c2f..06364eff7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xmtp/react-native-sdk", - "version": "0.1.0", + "version": "3.1.1", "description": "Wraps for native xmtp sdks for react native", "main": "build/index.js", "types": "build/index.d.ts", @@ -10,6 +10,7 @@ "lint": "expo-module lint", "test": "expo-module test", "prepare": "expo-module prepare", + "publish": "expo-module build && changeset publish", "prepublishOnly": "expo-module prepublishOnly", "pretty": "npx prettier '**/*.{js,ts,tsx}' --write", "expo-module": "expo-module", @@ -39,12 +40,14 @@ ] }, "publishConfig": { - "access": "public" + "access": "public", + "provenance": true }, "author": "XMTP Labs ", "license": "MIT", "homepage": "https://github.com/xmtp/xmtp-react-native#readme", "dependencies": { + "@changesets/cli": "^2.27.10", "@ethersproject/bytes": "^5.7.0", "@msgpack/msgpack": "^3.0.0-beta2", "@noble/hashes": "^1.3.3", diff --git a/src/index.ts b/src/index.ts index 89772ae92..005d59d3c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,8 @@ import { EventEmitter, NativeModulesProxy } from 'expo-modules-core' import { Client } from '.' import XMTPModule from './XMTPModule' -import { Address, InboxId, XMTPEnvironment } from './lib/Client' -import { - ConsentListEntry, - ConsentListEntryType, - ConsentState, -} from './lib/ConsentListEntry' +import { Address, InboxId, InstallationId, XMTPEnvironment } from './lib/Client' +import { ConsentRecord, ConsentState, ConsentType } from './lib/ConsentRecord' import { ContentCodec, DecryptedLocalAttachment, @@ -27,6 +23,7 @@ import { ConversationId, ConversationTopic, } from './lib/types/ConversationOptions' +import { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' import { DefaultContentTypes } from './lib/types/DefaultContentType' import { MessageId, MessageOrder } from './lib/types/MessagesOptions' import { PermissionPolicySet } from './lib/types/PermissionPolicySet' @@ -53,46 +50,53 @@ export function inboxId(): InboxId { } export async function findInboxIdFromAddress( - inboxId: InboxId, + installationId: InstallationId, address: string ): Promise { - return XMTPModule.findInboxIdFromAddress(inboxId, address) + return XMTPModule.findInboxIdFromAddress(installationId, address) } -export async function deleteLocalDatabase(inboxId: InboxId) { - return XMTPModule.deleteLocalDatabase(inboxId) +export async function deleteLocalDatabase(installationId: InstallationId) { + return XMTPModule.deleteLocalDatabase(installationId) } -export async function dropLocalDatabaseConnection(inboxId: InboxId) { - return XMTPModule.dropLocalDatabaseConnection(inboxId) -} - -export async function reconnectLocalDatabase(inboxId: InboxId) { - return XMTPModule.reconnectLocalDatabase(inboxId) +export async function dropLocalDatabaseConnection( + installationId: InstallationId +) { + return XMTPModule.dropLocalDatabaseConnection(installationId) } -export async function requestMessageHistorySync(inboxId: InboxId) { - return XMTPModule.requestMessageHistorySync(inboxId) +export async function reconnectLocalDatabase(installationId: InstallationId) { + return XMTPModule.reconnectLocalDatabase(installationId) } -export async function revokeAllOtherInstallations(inboxId: InboxId) { - return XMTPModule.revokeAllOtherInstallations(inboxId) +export async function requestMessageHistorySync( + installationId: InstallationId +) { + return XMTPModule.requestMessageHistorySync(installationId) } export async function getInboxState( - inboxId: InboxId, + installationId: InstallationId, refreshFromNetwork: boolean ): Promise { - const inboxState = await XMTPModule.getInboxState(inboxId, refreshFromNetwork) + const inboxState = await XMTPModule.getInboxState( + installationId, + refreshFromNetwork + ) return InboxState.from(inboxState) } export async function getInboxStates( - inboxId: InboxId, + installationId: InstallationId, refreshFromNetwork: boolean, - inboxIds: InboxId[], + inboxIds: InboxId[] ): Promise { - const inboxStates = await XMTPModule.getInboxStates(inboxId, refreshFromNetwork, inboxIds) + const inboxStates = await XMTPModule.getInboxStates( + installationId, + refreshFromNetwork, + inboxIds + ) return inboxStates.map((json: string) => { return InboxState.from(json) }) @@ -151,6 +155,8 @@ export async function create( appVersion, dbDirectory, historySyncUrl, + } + const walletParams: WalletParams = { walletType, chainId: typeof chainId === 'number' ? chainId : undefined, blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, @@ -159,7 +165,8 @@ export async function create( address, hasPreAuthenticateToInboxCallback, Array.from(dbEncryptionKey), - JSON.stringify(authParams) + JSON.stringify(authParams), + JSON.stringify(walletParams) ) } @@ -169,7 +176,8 @@ export async function build( dbEncryptionKey: Uint8Array, appVersion?: string | undefined, dbDirectory?: string | undefined, - historySyncUrl?: string | undefined + historySyncUrl?: string | undefined, + inboxId?: InboxId | undefined ): Promise { const authParams: AuthParams = { environment, @@ -179,20 +187,106 @@ export async function build( } return await XMTPModule.build( address, + inboxId, Array.from(dbEncryptionKey), JSON.stringify(authParams) ) } -export async function dropClient(inboxId: InboxId) { - return await XMTPModule.dropClient(inboxId) +export async function revokeAllOtherInstallations( + installationId: InstallationId, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.revokeAllOtherInstallations( + installationId, + JSON.stringify(walletParams) + ) +} + +export async function addAccount( + installationId: InstallationId, + newAddress: Address, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.addAccount( + installationId, + newAddress, + JSON.stringify(walletParams) + ) +} + +export async function removeAccount( + installationId: InstallationId, + addressToRemove: Address, + walletType?: WalletType | undefined, + chainId?: number | undefined, + blockNumber?: number | undefined +) { + const walletParams: WalletParams = { + walletType, + chainId: typeof chainId === 'number' ? chainId : undefined, + blockNumber: typeof blockNumber === 'number' ? blockNumber : undefined, + } + return XMTPModule.removeAccount( + installationId, + addressToRemove, + JSON.stringify(walletParams) + ) +} + +export async function dropClient(installationId: InstallationId) { + return await XMTPModule.dropClient(installationId) +} + +export async function signWithInstallationKey( + installationId: InstallationId, + message: string +): Promise { + const signatureArray = await XMTPModule.signWithInstallationKey( + installationId, + message + ) + return new Uint8Array(signatureArray) +} + +export async function verifySignature( + installationId: InstallationId, + message: string, + signature: Uint8Array +): Promise { + return await XMTPModule.verifySignature( + installationId, + message, + Array.from(signature) + ) } export async function canMessage( - inboxId: InboxId, + installationId: InstallationId, + peerAddresses: Address[] +): Promise<{ [key: Address]: boolean }> { + return await XMTPModule.canMessage(installationId, peerAddresses) +} + +export async function staticCanMessage( + environment: XMTPEnvironment, peerAddresses: Address[] ): Promise<{ [key: Address]: boolean }> { - return await XMTPModule.canMessage(inboxId, peerAddresses) + return await XMTPModule.staticCanMessage(environment, peerAddresses) } export async function getOrCreateInboxId( @@ -203,24 +297,24 @@ export async function getOrCreateInboxId( } export async function encryptAttachment( - inboxId: InboxId, + installationId: InstallationId, file: DecryptedLocalAttachment ): Promise { const fileJson = JSON.stringify(file) const encryptedFileJson = await XMTPModule.encryptAttachment( - inboxId, + installationId, fileJson ) return JSON.parse(encryptedFileJson) } export async function decryptAttachment( - inboxId: InboxId, + installationId: InstallationId, encryptedFile: EncryptedLocalAttachment ): Promise { const encryptedFileJson = JSON.stringify(encryptedFile) const fileJson = await XMTPModule.decryptAttachment( - inboxId, + installationId, encryptedFileJson ) return JSON.parse(fileJson) @@ -232,14 +326,16 @@ export async function listGroups< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( await XMTPModule.listGroups( - client.inboxId, + client.installationId, JSON.stringify(opts), order, - limit + limit, + consentState ) ).map((json: string) => { const group = JSON.parse(json) @@ -257,10 +353,17 @@ export async function listDms< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( - await XMTPModule.listDms(client.inboxId, JSON.stringify(opts), order, limit) + await XMTPModule.listDms( + client.installationId, + JSON.stringify(opts), + order, + limit, + consentState + ) ).map((json: string) => { const group = JSON.parse(json) @@ -277,14 +380,16 @@ export async function listConversations< client: Client, opts?: ConversationOptions | undefined, order?: ConversationOrder | undefined, - limit?: number | undefined + limit?: number | undefined, + consentState?: ConsentState | undefined ): Promise[]> { return ( await XMTPModule.listConversations( - client.inboxId, + client.installationId, JSON.stringify(opts), order, - limit + limit, + consentState ) ).map((json: string) => { const jsonObj = JSON.parse(json) @@ -310,9 +415,9 @@ export async function conversationMessages< beforeNs?: number | undefined, afterNs?: number | undefined, direction?: MessageOrder | undefined -): Promise[]> { +): Promise[]> { const messages = await XMTPModule.conversationMessages( - client.inboxId, + client.installationId, conversationId, limit, beforeNs, @@ -325,12 +430,13 @@ export async function conversationMessages< } export async function findMessage< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, + ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = [ContentType], // Adjusted to work with arrays >( client: Client, messageId: MessageId -): Promise | undefined> { - const message = await XMTPModule.findMessage(client.inboxId, messageId) +): Promise | undefined> { + const message = await XMTPModule.findMessage(client.installationId, messageId) return DecodedMessage.from(message, client) } @@ -340,7 +446,7 @@ export async function findGroup< client: Client, groupId: ConversationId ): Promise | undefined> { - const json = await XMTPModule.findGroup(client.inboxId, groupId) + const json = await XMTPModule.findGroup(client.installationId, groupId) const group = JSON.parse(json) if (!group || Object.keys(group).length === 0) { return undefined @@ -355,7 +461,10 @@ export async function findConversation< client: Client, conversationId: ConversationId ): Promise | undefined> { - const json = await XMTPModule.findConversation(client.inboxId, conversationId) + const json = await XMTPModule.findConversation( + client.installationId, + conversationId + ) const conversation = JSON.parse(json) if (!conversation || Object.keys(conversation).length === 0) { return undefined @@ -374,7 +483,10 @@ export async function findConversationByTopic< client: Client, topic: ConversationTopic ): Promise | undefined> { - const json = await XMTPModule.findConversationByTopic(client.inboxId, topic) + const json = await XMTPModule.findConversationByTopic( + client.installationId, + topic + ) const conversation = JSON.parse(json) if (!conversation || Object.keys(conversation).length === 0) { return undefined @@ -393,7 +505,10 @@ export async function findDmByInboxId< client: Client, peerInboxId: InboxId ): Promise | undefined> { - const json = await XMTPModule.findDmByInboxId(client.inboxId, peerInboxId) + const json = await XMTPModule.findDmByInboxId( + client.installationId, + peerInboxId + ) const dm = JSON.parse(json) if (!dm || Object.keys(dm).length === 0) { return undefined @@ -408,7 +523,7 @@ export async function findDmByAddress< client: Client, address: Address ): Promise | undefined> { - const json = await XMTPModule.findDmByAddress(client.inboxId, address) + const json = await XMTPModule.findDmByAddress(client.installationId, address) const dm = JSON.parse(json) if (!dm || Object.keys(dm).length === 0) { return undefined @@ -440,28 +555,39 @@ export async function sendWithContentType( } export async function sendMessage( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId, content: any ): Promise { const contentJson = JSON.stringify(content) - return await XMTPModule.sendMessage(inboxId, conversationId, contentJson) + return await XMTPModule.sendMessage( + installationId, + conversationId, + contentJson + ) } export async function publishPreparedMessages( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId ) { - return await XMTPModule.publishPreparedMessages(inboxId, conversationId) + return await XMTPModule.publishPreparedMessages( + installationId, + conversationId + ) } export async function prepareMessage( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId, content: any ): Promise { const contentJson = JSON.stringify(content) - return await XMTPModule.prepareMessage(inboxId, conversationId, contentJson) + return await XMTPModule.prepareMessage( + installationId, + conversationId, + contentJson + ) } export async function findOrCreateDm< @@ -471,7 +597,7 @@ export async function findOrCreateDm< peerAddress: Address ): Promise> { const dm = JSON.parse( - await XMTPModule.findOrCreateDm(client.inboxId, peerAddress) + await XMTPModule.findOrCreateDm(client.installationId, peerAddress) ) return new Dm(client, dm) } @@ -495,7 +621,7 @@ export async function createGroup< } const group = JSON.parse( await XMTPModule.createGroup( - client.inboxId, + client.installationId, peerAddresses, permissionLevel, JSON.stringify(options) @@ -524,7 +650,7 @@ export async function createGroupCustomPermissions< } const group = JSON.parse( await XMTPModule.createGroupCustomPermissions( - client.inboxId, + client.installationId, peerAddresses, JSON.stringify(permissionPolicySet), JSON.stringify(options) @@ -537,314 +663,327 @@ export async function createGroupCustomPermissions< export async function listMemberInboxIds< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client, id: ConversationId): Promise { - return XMTPModule.listMemberInboxIds(client.inboxId, id) + return XMTPModule.listMemberInboxIds(client.installationId, id) } export async function dmPeerInboxId< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client, dmId: ConversationId): Promise { - return XMTPModule.dmPeerInboxId(client.inboxId, dmId) + return XMTPModule.dmPeerInboxId(client.installationId, dmId) } export async function listConversationMembers( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - const members = await XMTPModule.listConversationMembers(inboxId, id) + const members = await XMTPModule.listConversationMembers(installationId, id) return members.map((json: string) => { return Member.from(json) }) } -export async function syncConversations(inboxId: InboxId) { - await XMTPModule.syncConversations(inboxId) +export async function syncConversations(installationId: InstallationId) { + await XMTPModule.syncConversations(installationId) } -export async function syncAllConversations(inboxId: InboxId): Promise { - return await XMTPModule.syncAllConversations(inboxId) +export async function syncAllConversations( + installationId: InstallationId +): Promise { + return await XMTPModule.syncAllConversations(installationId) } -export async function syncConversation(inboxId: InboxId, id: ConversationId) { - await XMTPModule.syncConversation(inboxId, id) +export async function syncConversation( + installationId: InstallationId, + id: ConversationId +) { + await XMTPModule.syncConversation(installationId, id) } export async function addGroupMembers( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, addresses: Address[] ): Promise { - return XMTPModule.addGroupMembers(inboxId, id, addresses) + return XMTPModule.addGroupMembers(installationId, id, addresses) } export async function removeGroupMembers( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, addresses: Address[] ): Promise { - return XMTPModule.removeGroupMembers(inboxId, id, addresses) + return XMTPModule.removeGroupMembers(installationId, id, addresses) } export async function addGroupMembersByInboxId( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, inboxIds: InboxId[] ): Promise { - return XMTPModule.addGroupMembersByInboxId(inboxId, id, inboxIds) + return XMTPModule.addGroupMembersByInboxId(installationId, id, inboxIds) } export async function removeGroupMembersByInboxId( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, inboxIds: InboxId[] ): Promise { - return XMTPModule.removeGroupMembersByInboxId(inboxId, id, inboxIds) + return XMTPModule.removeGroupMembersByInboxId(installationId, id, inboxIds) } export function groupName( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): string | PromiseLike { - return XMTPModule.groupName(inboxId, id) + return XMTPModule.groupName(installationId, id) } export function updateGroupName( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, groupName: string ): Promise { - return XMTPModule.updateGroupName(inboxId, id, groupName) + return XMTPModule.updateGroupName(installationId, id, groupName) } export function groupImageUrlSquare( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): string | PromiseLike { - return XMTPModule.groupImageUrlSquare(inboxId, id) + return XMTPModule.groupImageUrlSquare(installationId, id) } export function updateGroupImageUrlSquare( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, imageUrlSquare: string ): Promise { - return XMTPModule.updateGroupImageUrlSquare(inboxId, id, imageUrlSquare) + return XMTPModule.updateGroupImageUrlSquare( + installationId, + id, + imageUrlSquare + ) } export function groupDescription( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): string | PromiseLike { - return XMTPModule.groupDescription(inboxId, id) + return XMTPModule.groupDescription(installationId, id) } export function updateGroupDescription( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, description: string ): Promise { - return XMTPModule.updateGroupDescription(inboxId, id, description) + return XMTPModule.updateGroupDescription(installationId, id, description) } export function groupPinnedFrameUrl( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): string | PromiseLike { - return XMTPModule.groupPinnedFrameUrl(inboxId, id) + return XMTPModule.groupPinnedFrameUrl(installationId, id) } export function updateGroupPinnedFrameUrl( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId, pinnedFrameUrl: string ): Promise { - return XMTPModule.updateGroupPinnedFrameUrl(inboxId, id, pinnedFrameUrl) + return XMTPModule.updateGroupPinnedFrameUrl( + installationId, + id, + pinnedFrameUrl + ) } export function isGroupActive( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - return XMTPModule.isGroupActive(inboxId, id) + return XMTPModule.isGroupActive(installationId, id) } export async function addedByInboxId( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - return XMTPModule.addedByInboxId(inboxId, id) as InboxId + return XMTPModule.addedByInboxId(installationId, id) as InboxId } export async function creatorInboxId( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - return XMTPModule.creatorInboxId(inboxId, id) as InboxId + return XMTPModule.creatorInboxId(installationId, id) as InboxId } export async function isAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.isAdmin(clientInboxId, id, inboxId) + return XMTPModule.isAdmin(clientInstallationId, id, inboxId) } export async function isSuperAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.isSuperAdmin(clientInboxId, id, inboxId) + return XMTPModule.isSuperAdmin(clientInstallationId, id, inboxId) } export async function listAdmins( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - return XMTPModule.listAdmins(inboxId, id) + return XMTPModule.listAdmins(installationId, id) } export async function listSuperAdmins( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ): Promise { - return XMTPModule.listSuperAdmins(inboxId, id) + return XMTPModule.listSuperAdmins(installationId, id) } export async function addAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.addAdmin(clientInboxId, id, inboxId) + return XMTPModule.addAdmin(clientInstallationId, id, inboxId) } export async function addSuperAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.addSuperAdmin(clientInboxId, id, inboxId) + return XMTPModule.addSuperAdmin(clientInstallationId, id, inboxId) } export async function removeAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.removeAdmin(clientInboxId, id, inboxId) + return XMTPModule.removeAdmin(clientInstallationId, id, inboxId) } export async function removeSuperAdmin( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, inboxId: InboxId ): Promise { - return XMTPModule.removeSuperAdmin(clientInboxId, id, inboxId) + return XMTPModule.removeSuperAdmin(clientInstallationId, id, inboxId) } export async function updateAddMemberPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateAddMemberPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateRemoveMemberPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateRemoveMemberPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateAddAdminPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateAddAdminPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateRemoveAdminPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateRemoveAdminPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateGroupNamePermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupNamePermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateGroupImageUrlSquarePermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupImageUrlSquarePermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateGroupDescriptionPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupDescriptionPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function updateGroupPinnedFrameUrlPermission( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId, permissionOption: PermissionUpdateOption ): Promise { return XMTPModule.updateGroupPinnedFrameUrlPermission( - clientInboxId, + clientInstallationId, id, permissionOption ) } export async function permissionPolicySet( - clientInboxId: InboxId, + clientInstallationId: InstallationId, id: ConversationId ): Promise { - const json = await XMTPModule.permissionPolicySet(clientInboxId, id) + const json = await XMTPModule.permissionPolicySet(clientInstallationId, id) return JSON.parse(json) } @@ -854,8 +993,12 @@ export async function processMessage< client: Client, id: ConversationId, encryptedMessage: string -): Promise> { - const json = XMTPModule.processMessage(client.inboxId, id, encryptedMessage) +): Promise> { + const json = await XMTPModule.processMessage( + client.installationId, + id, + encryptedMessage + ) return DecodedMessage.from(json, client) } @@ -866,7 +1009,7 @@ export async function processWelcomeMessage< encryptedMessage: string ): Promise>> { const json = await XMTPModule.processWelcomeMessage( - client.inboxId, + client.installationId, encryptedMessage ) const conversation = JSON.parse(json) @@ -878,14 +1021,20 @@ export async function processWelcomeMessage< } } +export async function syncConsent( + installationId: InstallationId +): Promise { + return await XMTPModule.syncConsent(installationId) +} + export async function setConsentState( - inboxId: InboxId, + installationId: InstallationId, value: string, - entryType: ConsentListEntryType, + entryType: ConsentType, consentType: ConsentState ): Promise { return await XMTPModule.setConsentState( - inboxId, + installationId, value, entryType, consentType @@ -893,72 +1042,90 @@ export async function setConsentState( } export async function consentAddressState( - inboxId: InboxId, + installationId: InstallationId, address: Address ): Promise { - return await XMTPModule.consentAddressState(inboxId, address) + return await XMTPModule.consentAddressState(installationId, address) } export async function consentInboxIdState( - inboxId: InboxId, + installationId: InstallationId, peerInboxId: InboxId ): Promise { - return await XMTPModule.consentInboxIdState(inboxId, peerInboxId) + return await XMTPModule.consentInboxIdState(installationId, peerInboxId) } export async function consentConversationIdState( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId ): Promise { - return await XMTPModule.consentConversationIdState(inboxId, conversationId) + return await XMTPModule.consentConversationIdState( + installationId, + conversationId + ) } export async function conversationConsentState( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId ): Promise { - return await XMTPModule.conversationConsentState(inboxId, conversationId) + return await XMTPModule.conversationConsentState( + installationId, + conversationId + ) } export async function updateConversationConsent( - inboxId: InboxId, + installationId: InstallationId, conversationId: ConversationId, state: ConsentState ): Promise { - return XMTPModule.updateConversationConsent(inboxId, conversationId, state) + return XMTPModule.updateConversationConsent( + installationId, + conversationId, + state + ) +} + +export function subscribeToConsent(installationId: InstallationId) { + return XMTPModule.subscribeToConsent(installationId) } export function subscribeToConversations( - inboxId: InboxId, + installationId: InstallationId, type: ConversationType ) { - return XMTPModule.subscribeToConversations(inboxId, type) + return XMTPModule.subscribeToConversations(installationId, type) } export function subscribeToAllMessages( - inboxId: InboxId, + installationId: InstallationId, type: ConversationType ) { - return XMTPModule.subscribeToAllMessages(inboxId, type) + return XMTPModule.subscribeToAllMessages(installationId, type) } export async function subscribeToMessages( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ) { - return await XMTPModule.subscribeToMessages(inboxId, id) + return await XMTPModule.subscribeToMessages(installationId, id) +} + +export function unsubscribeFromConsent(installationId: InstallationId) { + return XMTPModule.unsubscribeFromConsent(installationId) } -export function unsubscribeFromConversations(inboxId: InboxId) { - return XMTPModule.unsubscribeFromConversations(inboxId) +export function unsubscribeFromConversations(installationId: InstallationId) { + return XMTPModule.unsubscribeFromConversations(installationId) } -export function unsubscribeFromAllMessages(inboxId: InboxId) { - return XMTPModule.unsubscribeFromAllMessages(inboxId) +export function unsubscribeFromAllMessages(installationId: InstallationId) { + return XMTPModule.unsubscribeFromAllMessages(installationId) } export async function unsubscribeFromMessages( - inboxId: InboxId, + installationId: InstallationId, id: ConversationId ) { - return await XMTPModule.unsubscribeFromMessages(inboxId, id) + return await XMTPModule.unsubscribeFromMessages(installationId, id) } export function registerPushToken(pushServer: string, token: string) { @@ -980,6 +1147,9 @@ interface AuthParams { appVersion?: string dbDirectory?: string historySyncUrl?: string +} + +interface WalletParams { walletType?: string chainId?: number blockNumber?: number @@ -996,7 +1166,7 @@ export { Client } from './lib/Client' export * from './lib/ContentCodec' export { Conversation, ConversationVersion } from './lib/Conversation' export { XMTPPush } from './lib/XMTPPush' -export { ConsentListEntry, DecodedMessage, MessageDeliveryStatus, ConsentState } +export { ConsentRecord, DecodedMessage, MessageDeliveryStatus, ConsentState } export { Group } from './lib/Group' export { Dm } from './lib/Dm' export { Member } from './lib/Member' @@ -1009,3 +1179,4 @@ export { ConversationType, } from './lib/types/ConversationOptions' export { MessageId, MessageOrder } from './lib/types/MessagesOptions' +export { DecodedMessageUnion } from './lib/types/DecodedMessageUnion' diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 6f9ed29e7..eaf6492de 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -23,15 +23,16 @@ export type GetMessageContentTypeFromClient = export type ExtractDecodedType = C extends XMTPModule.ContentCodec ? T : never -export type InboxId = string & { readonly brand: unique symbol } +export type InstallationId = string & { readonly brand: unique symbol } +export type InboxId = string export type Address = string export class Client< ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { - address: string + address: Address inboxId: InboxId - installationId: string + installationId: InstallationId dbPath: string conversations: Conversations preferences: PrivatePreferences @@ -163,7 +164,7 @@ export class Client< new Client( message.address, message.inboxId as InboxId, - message.installationId, + message.installationId as InstallationId, message.dbPath, options.codecs || [] ) @@ -203,7 +204,8 @@ export class Client< ContentCodecs extends DefaultContentTypes = DefaultContentTypes, >( address: Address, - options: ClientOptions & { codecs?: ContentCodecs } + options: ClientOptions & { codecs?: ContentCodecs }, + inboxId?: InboxId | undefined ): Promise> { if (options.dbEncryptionKey.length !== 32) { throw new Error('Must pass an encryption key that is exactly 32 bytes.') @@ -214,7 +216,8 @@ export class Client< options.dbEncryptionKey, options.appVersion, options.dbDirectory, - options.historySyncUrl + options.historySyncUrl, + inboxId ) return new Client( @@ -229,8 +232,8 @@ 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: InboxId) { - return await XMTPModule.dropClient(inboxId) + static async dropClient(installationId: InstallationId) { + return await XMTPModule.dropClient(installationId) } private static addSubscription( @@ -279,7 +282,7 @@ export class Client< * Static method to determine the inboxId for the address. * * @param {Address} peerAddress - The address of the peer to check for messaging eligibility. - * @param {Partial} opts - Optional configuration options for the Client. + * @param {XMTPEnvironment} env - Environment to get the inboxId from * @returns {Promise} */ static async getOrCreateInboxId( @@ -289,10 +292,26 @@ export class Client< return await XMTPModule.getOrCreateInboxId(address, env) } + /** + * Determines whether the current user can send messages to the specified peers. + * + * This method checks if the specified peers are using clients that are on the network. + * + * @param {Address[]} addresses - The addresses of the peers to check for messaging eligibility. + * @param {XMTPEnvironment} env - Environment to see if the address is on the network for + * @returns {Promise<{ [key: Address]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the network. + */ + static async canMessage( + env: XMTPEnvironment, + addresses: Address[] + ): Promise<{ [key: Address]: boolean }> { + return await XMTPModule.staticCanMessage(env, addresses) + } + constructor( address: Address, inboxId: InboxId, - installationId: string, + installationId: InstallationId, dbPath: string, codecs: XMTPModule.ContentCodec[] = [] ) { @@ -317,47 +336,95 @@ export class Client< } /** - * 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. + * Add this account to the current inboxId. + * @param {Signer} newAccount - The signer of the new account to be added. */ - async findInboxIdFromAddress( - peerAddress: Address - ): Promise { - return await XMTPModule.findInboxIdFromAddress(this.inboxId, peerAddress) - } + async addAccount(newAccount: Signer | WalletClient) { + const signer = getSigner(newAccount) + if (!signer) { + throw new Error('Signer is not configured') + } - /** - * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. - */ - async deleteLocalDatabase() { - return await XMTPModule.deleteLocalDatabase(this.inboxId) - } + return new Promise((resolve, reject) => { + ;(async () => { + Client.signSubscription = XMTPModule.emitter.addListener( + 'sign', + async (message: { id: string; message: string }) => { + try { + await Client.handleSignatureRequest(signer, message) + } catch (e) { + const errorMessage = + 'ERROR in addAccount. User rejected signature' + console.info(errorMessage, e) + Client.signSubscription?.remove() + reject(errorMessage) + } + } + ) - /** - * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() - */ - async dropLocalDatabaseConnection() { - return await XMTPModule.dropLocalDatabaseConnection(this.inboxId) + await XMTPModule.addAccount( + this.installationId, + await signer.getAddress(), + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) + Client.signSubscription?.remove() + resolve() + })().catch((error) => { + Client.signSubscription?.remove() + reject(error) + }) + }) } /** - * Reconnects the local database after being dropped. + * Remove this account from the current inboxId. + * @param {Signer} wallet - The signer object used for authenticate the removal. + * @param {Address} addressToRemove - The address of the wallet you'd like to remove from the account. */ - async reconnectLocalDatabase() { - return await XMTPModule.reconnectLocalDatabase(this.inboxId) - } + async removeAccount(wallet: Signer | WalletClient, addressToRemove: Address) { + const signer = getSigner(wallet) + if (!signer) { + throw new Error('Signer is not configured') + } - /** - * Make a request for a message history sync. - */ - async requestMessageHistorySync() { - return await XMTPModule.requestMessageHistorySync(this.inboxId) + return new Promise((resolve, reject) => { + ;(async () => { + Client.signSubscription = XMTPModule.emitter.addListener( + 'sign', + async (message: { id: string; message: string }) => { + try { + await Client.handleSignatureRequest(signer, message) + } catch (e) { + const errorMessage = + 'ERROR in revokeAllOtherInstallations. User rejected signature' + console.info(errorMessage, e) + Client.signSubscription?.remove() + reject(errorMessage) + } + } + ) + + await XMTPModule.removeAccount( + this.installationId, + addressToRemove, + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) + Client.signSubscription?.remove() + resolve() + })().catch((error) => { + Client.signSubscription?.remove() + reject(error) + }) + }) } /** * Revoke all other installations but the current one. + * @param {Signer} signer - The signer object used for authenticate the revoke. */ async revokeAllOtherInstallations(wallet: Signer | WalletClient | null) { const signer = getSigner(wallet) @@ -382,7 +449,12 @@ export class Client< } ) - await XMTPModule.revokeAllOtherInstallations(this.inboxId) + await XMTPModule.revokeAllOtherInstallations( + this.installationId, + signer.walletType?.(), + signer.getChainId?.(), + signer.getBlockNumber?.() + ) Client.signSubscription?.remove() resolve() })().catch((error) => { @@ -392,6 +464,78 @@ export class Client< }) } + /** + * Sign this message with the current installation key. + * @param {string} message - The message to sign. + * @returns {Promise} A Promise resolving to the signature bytes. + */ + async signWithInstallationKey(message: string): Promise { + return await XMTPModule.signWithInstallationKey( + this.installationId, + message + ) + } + + /** + * Verify the signature was signed with this clients installation key. + * @param {string} message - The message that was signed. + * @param {Uint8Array} signature - The signature. + * @returns {Promise} A Promise resolving to a boolean if the signature verified or not. + */ + async verifySignature( + message: string, + signature: Uint8Array + ): Promise { + return await XMTPModule.verifySignature( + this.installationId, + message, + signature + ) + } + + /** + * 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: Address + ): Promise { + return await XMTPModule.findInboxIdFromAddress( + this.installationId, + peerAddress + ) + } + + /** + * Deletes the local database. This cannot be undone and these stored messages will not be refetched from the network. + */ + async deleteLocalDatabase() { + return await XMTPModule.deleteLocalDatabase(this.installationId) + } + + /** + * Drop the local database connection. This function is delicate and should be used with caution. App will error if database not properly reconnected. See: reconnectLocalDatabase() + */ + async dropLocalDatabaseConnection() { + return await XMTPModule.dropLocalDatabaseConnection(this.installationId) + } + + /** + * Reconnects the local database after being dropped. + */ + async reconnectLocalDatabase() { + return await XMTPModule.reconnectLocalDatabase(this.installationId) + } + + /** + * Make a request for a message history sync. + */ + async requestMessageHistorySync() { + return await XMTPModule.requestMessageHistorySync(this.installationId) + } + /** * Make a request for your inbox state. * @@ -399,7 +543,10 @@ export class Client< * @returns {Promise} A Promise resolving to a InboxState. */ async inboxState(refreshFromNetwork: boolean): Promise { - return await XMTPModule.getInboxState(this.inboxId, refreshFromNetwork) + return await XMTPModule.getInboxState( + this.installationId, + refreshFromNetwork + ) } /** @@ -409,20 +556,27 @@ export class Client< * @param {boolean} refreshFromNetwork - If you want to refresh the current state the inbox from the network or not. * @returns {Promise} A Promise resolving to a list of InboxState. */ - async inboxStates(refreshFromNetwork: boolean, inboxIds: InboxId[]): Promise { - return await XMTPModule.getInboxStates(this.inboxId, refreshFromNetwork, inboxIds) + async inboxStates( + refreshFromNetwork: boolean, + inboxIds: InboxId[] + ): Promise { + return await XMTPModule.getInboxStates( + this.installationId, + refreshFromNetwork, + inboxIds + ) } /** - * Determines whether the current user can send messages to the specified peers over groups. + * Determines whether the current user can send messages to the specified peers. * - * This method checks if the specified peers are using clients that support group messaging. + * This method checks if the specified peers are using clients that are on the network. * * @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. + * @returns {Promise<{ [key: Address]: boolean }>} A Promise resolving to a hash of addresses and booleans if they can message on the network. */ async canMessage(addresses: Address[]): Promise<{ [key: Address]: boolean }> { - return await XMTPModule.canMessage(this.inboxId, addresses) + return await XMTPModule.canMessage(this.installationId, addresses) } /** @@ -440,7 +594,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(this.installationId, file) } /** @@ -457,7 +611,10 @@ 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) + return await XMTPModule.decryptAttachment( + this.installationId, + encryptedFile + ) } } diff --git a/src/lib/ConsentListEntry.ts b/src/lib/ConsentListEntry.ts deleted file mode 100644 index 2393fc098..000000000 --- a/src/lib/ConsentListEntry.ts +++ /dev/null @@ -1,24 +0,0 @@ -export type ConsentState = 'allowed' | 'denied' | 'unknown' - -export type ConsentListEntryType = 'address' | 'conversation_id' | 'inbox_id' - -export class ConsentListEntry { - value: string - entryType: ConsentListEntryType - permissionType: ConsentState - - constructor( - value: string, - entryType: ConsentListEntryType, - permissionType: ConsentState - ) { - this.value = value - this.entryType = entryType - this.permissionType = permissionType - } - - static from(json: string): ConsentListEntry { - const entry = JSON.parse(json) - return new ConsentListEntry(entry.value, entry.type, entry.state) - } -} diff --git a/src/lib/ConsentRecord.ts b/src/lib/ConsentRecord.ts new file mode 100644 index 000000000..607b8b343 --- /dev/null +++ b/src/lib/ConsentRecord.ts @@ -0,0 +1,20 @@ +export type ConsentState = 'allowed' | 'denied' | 'unknown' + +export type ConsentType = 'address' | 'conversation_id' | 'inbox_id' + +export class ConsentRecord { + value: string + entryType: ConsentType + state: ConsentState + + constructor(value: string, entryType: ConsentType, state: ConsentState) { + this.value = value + this.entryType = entryType + this.state = state + } + + static from(json: string): ConsentRecord { + const entry = JSON.parse(json) + return new ConsentRecord(entry.value, entry.type, entry.state) + } +} diff --git a/src/lib/Conversation.ts b/src/lib/Conversation.ts index b03930c10..a230d673b 100644 --- a/src/lib/Conversation.ts +++ b/src/lib/Conversation.ts @@ -1,5 +1,11 @@ -import { ConsentState } from './ConsentListEntry' -import { ConversationSendPayload, MessageId, MessagesOptions, SendOptions } from './types' +import { ConsentState } from './ConsentRecord' +import { + ConversationSendPayload, + MessageId, + MessagesOptions, + SendOptions, +} from './types' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import * as XMTP from '../index' import { DecodedMessage, Member, Dm, Group } from '../index' @@ -16,22 +22,24 @@ export interface ConversationBase { version: ConversationVersion id: string state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessage send( content: ConversationSendPayload, opts?: SendOptions ): Promise sync() - messages(opts?: MessagesOptions): Promise[]> + messages(opts?: MessagesOptions): Promise[]> streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> consentState(): Promise updateConsent(state: ConsentState): Promise processMessage( encryptedMessage: string - ): Promise> + ): Promise> members(): Promise } diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index e585c124d..dd25aebc1 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -8,12 +8,14 @@ import { ConversationOptions, } from './types/ConversationOptions' import { CreateGroupOptions } from './types/CreateGroupOptions' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' +import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { PermissionPolicySet } from './types/PermissionPolicySet' import * as XMTPModule from '../index' import { Address, - ContentCodec, + ConsentState, Conversation, ConversationId, ConversationTopic, @@ -23,7 +25,7 @@ import { import { getAddress } from '../utils/address' export default class Conversations< - ContentTypes extends ContentCodec[] = [], + ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { client: Client private subscriptions: { [key: string]: { remove: () => void } } = {} @@ -32,67 +34,6 @@ export default class Conversations< this.client = client } - /** - * Creates a new conversation. - * - * This method creates a new conversation with the specified peer address and context. - * - * @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: Address - ): Promise> { - const checksumAddress = getAddress(peerAddress) - return await XMTPModule.findOrCreateDm(this.client, checksumAddress) - } - - /** - * Creates a new conversation. - * - * This method creates a new conversation with the specified peer address. - * - * @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: 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 {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?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listGroups(this.client, opts, order, limit) - } - - /** - * This method returns a list of all dms that the client is a member of. - * To get the latest list of dms from the network, call sync() first. - * @param {ConversationOptions} opts - The options to specify what fields you want returned for the dms in the list. - * @param {ConversationOrder} order - The order to specify if you want dms listed by last message or by created at. - * @param {number} limit - Limit the number of dms returned in the list. - * - * @returns {Promise} A Promise that resolves to an array of Dms objects. - */ - async listDms( - opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listDms(this.client, opts, order, limit) - } - /** * This method returns a group by the group id if that group exists in the local database. * To get the latest list of groups from the network, call sync() first. @@ -161,58 +102,49 @@ export default class Conversations< */ async findMessage( messageId: MessageId - ): Promise | undefined> { + ): Promise | undefined> { return await XMTPModule.findMessage(this.client, messageId) } + async fromWelcome( + encryptedMessage: string + ): Promise> { + try { + return await XMTPModule.processWelcomeMessage( + this.client, + encryptedMessage + ) + } catch (e) { + console.info('ERROR in processWelcomeMessage()', e) + throw e + } + } + /** - * This method returns a list of all V3 conversations that the client is a member of. - * To include the latest conversations from the network in the returned list, call sync() first. + * Creates a new conversation. * - * @returns {Promise} A Promise that resolves to an array of Conversation objects. + * This method creates a new conversation with the specified peer address and context. + * + * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Conversation object. */ - async list( - opts?: ConversationOptions | undefined, - order?: ConversationOrder | undefined, - limit?: number | undefined - ): Promise[]> { - return await XMTPModule.listConversations(this.client, opts, order, limit) + async newConversation( + peerAddress: Address + ): Promise> { + const checksumAddress = getAddress(peerAddress) + return await XMTPModule.findOrCreateDm(this.client, checksumAddress) } /** - * 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. + * Creates a new conversation. + * + * This method creates a new conversation with the specified peer address. + * + * @param {Address} peerAddress - The address of the peer to create a conversation with. + * @returns {Promise} A Promise that resolves to a Dm object. */ - async stream( - callback: (conversation: Conversation) => Promise, - type: ConversationType = 'all' - ): Promise { - XMTPModule.subscribeToConversations(this.client.inboxId, type) - const subscription = XMTPModule.emitter.addListener( - EventTypes.Conversation, - async ({ - inboxId, - conversation, - }: { - inboxId: string - conversation: Conversation - }) => { - if (inboxId !== this.client.inboxId) { - return - } - if (conversation.version === ConversationVersion.GROUP) { - return await callback( - new Group(this.client, conversation as unknown as GroupParams) - ) - } else if (conversation.version === ConversationVersion.DM) { - return await callback( - new Dm(this.client, conversation as unknown as DmParams) - ) - } - } - ) - this.subscriptions[EventTypes.Conversation] = subscription + async findOrCreateDm(peerAddress: Address): Promise> { + return await XMTPModule.findOrCreateDm(this.client, peerAddress) } /** @@ -265,12 +197,81 @@ export default class Conversations< ) } + /** + * 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 listGroups( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listGroups( + this.client, + opts, + order, + limit, + consentState + ) + } + + /** + * This method returns a list of all dms that the client is a member of. + * To get the latest list of dms from the network, call sync() first. + * @param {ConversationOptions} opts - The options to specify what fields you want returned for the dms in the list. + * @param {ConversationOrder} order - The order to specify if you want dms listed by last message or by created at. + * @param {number} limit - Limit the number of dms returned in the list. + * + * @returns {Promise} A Promise that resolves to an array of Dms objects. + */ + async listDms( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listDms( + this.client, + opts, + order, + limit, + consentState + ) + } + + /** + * This method returns a list of all V3 conversations that the client is a member of. + * To include the latest conversations from the network in the returned list, call sync() first. + * + * @returns {Promise} A Promise that resolves to an array of Conversation objects. + */ + async list( + opts?: ConversationOptions | undefined, + order?: ConversationOrder | undefined, + limit?: number | undefined, + consentState?: ConsentState | undefined + ): Promise[]> { + return await XMTPModule.listConversations( + this.client, + opts, + order, + limit, + consentState + ) + } + /** * Executes a network request to fetch the latest list of conversations associated with the client * and save them to the local state. */ async sync() { - await XMTPModule.syncConversations(this.client.inboxId) + await XMTPModule.syncConversations(this.client.installationId) } /** @@ -279,7 +280,43 @@ export default class Conversations< * @returns {Promise} A Promise that resolves to the number of conversations synced. */ async syncAllConversations(): Promise { - return await XMTPModule.syncAllConversations(this.client.inboxId) + return await XMTPModule.syncAllConversations(this.client.installationId) + } + + /** + * 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 stream( + callback: (conversation: Conversation) => Promise, + type: ConversationType = 'all' + ): Promise { + XMTPModule.subscribeToConversations(this.client.installationId, type) + const subscription = XMTPModule.emitter.addListener( + EventTypes.Conversation, + async ({ + installationId, + conversation, + }: { + installationId: string + conversation: Conversation + }) => { + if (installationId !== this.client.installationId) { + return + } + if (conversation.version === ConversationVersion.GROUP) { + return await callback( + new Group(this.client, conversation as unknown as GroupParams) + ) + } else if (conversation.version === ConversationVersion.DM) { + return await callback( + new Dm(this.client, conversation as unknown as DmParams) + ) + } + } + ) + this.subscriptions[EventTypes.Conversation] = subscription } /** @@ -291,42 +328,33 @@ export default class Conversations< * @returns {Promise} A Promise that resolves when the stream is set up. */ async streamAllMessages( - callback: (message: DecodedMessage) => Promise, + callback: (message: DecodedMessageUnion) => Promise, type: ConversationType = 'all' ): Promise { - XMTPModule.subscribeToAllMessages(this.client.inboxId, type) + XMTPModule.subscribeToAllMessages(this.client.installationId, type) const subscription = XMTPModule.emitter.addListener( EventTypes.Message, async ({ - inboxId, + installationId, message, }: { - inboxId: string + installationId: string message: DecodedMessage }) => { - if (inboxId !== this.client.inboxId) { + if (installationId !== this.client.installationId) { return } - await callback(DecodedMessage.fromObject(message, this.client)) + await callback( + DecodedMessage.fromObject( + message, + this.client + ) as DecodedMessageUnion + ) } ) this.subscriptions[EventTypes.Message] = subscription } - async fromWelcome( - encryptedMessage: string - ): Promise> { - try { - return await XMTPModule.processWelcomeMessage( - this.client, - encryptedMessage - ) - } catch (e) { - console.info('ERROR in processWelcomeMessage()', e) - throw e - } - } - /** * Cancels the stream for new conversations. */ @@ -335,7 +363,7 @@ export default class Conversations< this.subscriptions[EventTypes.Conversation].remove() delete this.subscriptions[EventTypes.Conversation] } - XMTPModule.unsubscribeFromConversations(this.client.inboxId) + XMTPModule.unsubscribeFromConversations(this.client.installationId) } /** @@ -346,6 +374,6 @@ export default class Conversations< this.subscriptions[EventTypes.Message].remove() delete this.subscriptions[EventTypes.Message] } - XMTPModule.unsubscribeFromAllMessages(this.client.inboxId) + XMTPModule.unsubscribeFromAllMessages(this.client.installationId) } } diff --git a/src/lib/DecodedMessage.ts b/src/lib/DecodedMessage.ts index c8644f782..f5dd48976 100644 --- a/src/lib/DecodedMessage.ts +++ b/src/lib/DecodedMessage.ts @@ -6,7 +6,7 @@ import { NativeContentCodec, NativeMessageContent, } from './ContentCodec' -import { TextCodec } from './NativeCodecs/TextCodec' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' const allowEmptyProperties: (keyof NativeMessageContent)[] = [ @@ -21,6 +21,7 @@ export enum MessageDeliveryStatus { } export class DecodedMessage< + ContentType extends DefaultContentTypes[number] = DefaultContentTypes[number], ContentTypes extends DefaultContentTypes = DefaultContentTypes, > { client: Client @@ -33,12 +34,16 @@ export class DecodedMessage< fallback: string | undefined deliveryStatus: MessageDeliveryStatus = MessageDeliveryStatus.PUBLISHED - static from( + static from< + ContentType extends + DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = ContentType[], + >( json: string, client: Client - ): DecodedMessage { + ): DecodedMessageUnion { const decoded = JSON.parse(json) - return new DecodedMessage( + return new DecodedMessage( client, decoded.id, decoded.topic, @@ -48,11 +53,13 @@ export class DecodedMessage< decoded.content, decoded.fallback, decoded.deliveryStatus - ) + ) as DecodedMessageUnion } static fromObject< - ContentTypes extends DefaultContentTypes = DefaultContentTypes, + ContentType extends + DefaultContentTypes[number] = DefaultContentTypes[number], + ContentTypes extends DefaultContentTypes = [ContentType], >( object: { id: string @@ -65,7 +72,7 @@ export class DecodedMessage< deliveryStatus: MessageDeliveryStatus | undefined }, client: Client - ): DecodedMessage { + ): DecodedMessage { return new DecodedMessage( client, object.id, @@ -102,15 +109,13 @@ export class DecodedMessage< this.deliveryStatus = deliveryStatus } - content(): ExtractDecodedType<[...ContentTypes, TextCodec][number] | string> { + content(): ExtractDecodedType { const encodedJSON = this.nativeContent.encoded if (encodedJSON) { const encoded = JSON.parse(encodedJSON) const codec = this.client.codecRegistry[ this.contentTypeId - ] as JSContentCodec< - ExtractDecodedType<[...ContentTypes, TextCodec][number]> - > + ] as JSContentCodec> if (!codec) { throw new Error( `no content type found ${JSON.stringify(this.contentTypeId)}` @@ -129,9 +134,7 @@ export class DecodedMessage< ) ) { return ( - codec as NativeContentCodec< - ExtractDecodedType<[...ContentTypes, TextCodec][number]> - > + codec as NativeContentCodec> ).decode(this.nativeContent) } } diff --git a/src/lib/Dm.ts b/src/lib/Dm.ts index faa8344fb..3e44a22f2 100644 --- a/src/lib/Dm.ts +++ b/src/lib/Dm.ts @@ -1,9 +1,10 @@ import { InboxId } from './Client' -import { ConsentState } from './ConsentListEntry' +import { ConsentState } from './ConsentRecord' import { ConversationVersion, ConversationBase } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' @@ -28,12 +29,12 @@ export class Dm version = ConversationVersion.DM as const topic: ConversationTopic state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion constructor( client: XMTP.Client, params: DmParams, - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion ) { this.client = client this.id = params.id @@ -71,7 +72,11 @@ export class Dm content = { text: content } } - return await XMTP.sendMessage(this.client.inboxId, this.id, content) + return await XMTP.sendMessage( + this.client.installationId, + this.id, + content + ) } catch (e) { console.info('ERROR in send()', e.message) throw e @@ -119,7 +124,11 @@ export class Dm content = { text: content } } - return await XMTP.prepareMessage(this.client.inboxId, this.id, content) + return await XMTP.prepareMessage( + this.client.installationId, + this.id, + content + ) } catch (e) { console.info('ERROR in prepareMessage()', e.message) throw e @@ -133,7 +142,10 @@ export class Dm */ async publishPreparedMessages() { try { - return await XMTP.publishPreparedMessages(this.client.inboxId, this.id) + return await XMTP.publishPreparedMessages( + this.client.installationId, + this.id + ) } catch (e) { console.info('ERROR in publishPreparedMessages()', e.message) throw e @@ -152,7 +164,7 @@ export class Dm */ async messages( opts?: MessagesOptions - ): Promise[]> { + ): Promise[]> { return await XMTP.conversationMessages( this.client, this.id, @@ -168,7 +180,7 @@ export class Dm * associated with the dm and saves them to the local state. */ async sync() { - await XMTP.syncConversation(this.client.inboxId, this.id) + await XMTP.syncConversation(this.client.installationId, this.id) } /** @@ -182,21 +194,23 @@ export class Dm * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. */ async streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> { - await XMTP.subscribeToMessages(this.client.inboxId, this.id) + await XMTP.subscribeToMessages(this.client.installationId, this.id) const messageSubscription = XMTP.emitter.addListener( EventTypes.ConversationMessage, async ({ - inboxId, + installationId, message, conversationId, }: { - inboxId: string - message: DecodedMessage + installationId: string + message: DecodedMessage conversationId: string }) => { - if (inboxId !== this.client.inboxId) { + if (installationId !== this.client.installationId) { return } if (conversationId !== this.id) { @@ -209,13 +223,13 @@ export class Dm ) return async () => { messageSubscription.remove() - await XMTP.unsubscribeFromMessages(this.client.inboxId, this.id) + await XMTP.unsubscribeFromMessages(this.client.installationId, this.id) } } async processMessage( encryptedMessage: string - ): Promise> { + ): Promise> { try { return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { @@ -225,12 +239,15 @@ export class Dm } async consentState(): Promise { - return await XMTP.conversationConsentState(this.client.inboxId, this.id) + return await XMTP.conversationConsentState( + this.client.installationId, + this.id + ) } async updateConsent(state: ConsentState): Promise { return await XMTP.updateConversationConsent( - this.client.inboxId, + this.client.installationId, this.id, state ) @@ -242,6 +259,9 @@ export class Dm * To get the latest member list from the network, call sync() first. */ async members(): Promise { - return await XMTP.listConversationMembers(this.client.inboxId, this.id) + return await XMTP.listConversationMembers( + this.client.installationId, + this.id + ) } } diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 44763e335..f83d1d0b2 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -1,9 +1,10 @@ import { InboxId } from './Client' -import { ConsentState } from './ConsentListEntry' +import { ConsentState } from './ConsentRecord' import { ConversationBase, ConversationVersion } from './Conversation' import { DecodedMessage } from './DecodedMessage' import { Member } from './Member' import { ConversationSendPayload } from './types/ConversationCodecs' +import { DecodedMessageUnion } from './types/DecodedMessageUnion' import { DefaultContentTypes } from './types/DefaultContentType' import { EventTypes } from './types/EventTypes' import { MessageId, MessagesOptions } from './types/MessagesOptions' @@ -42,12 +43,12 @@ export class Group< imageUrlSquare: string description: string state: ConsentState - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion constructor( client: XMTP.Client, params: GroupParams, - lastMessage?: DecodedMessage + lastMessage?: DecodedMessageUnion ) { this.client = client this.id = params.id @@ -76,7 +77,7 @@ export class Group< * @returns {Promise} A Promise that resolves to a InboxId. */ async creatorInboxId(): Promise { - return XMTP.creatorInboxId(this.client.inboxId, this.id) + return XMTP.creatorInboxId(this.client.installationId, this.id) } /** @@ -99,7 +100,11 @@ export class Group< content = { text: content } } - return await XMTP.sendMessage(this.client.inboxId, this.id, content) + return await XMTP.sendMessage( + this.client.installationId, + this.id, + content + ) } catch (e) { console.info('ERROR in send()', e.message) throw e @@ -147,7 +152,11 @@ export class Group< content = { text: content } } - return await XMTP.prepareMessage(this.client.inboxId, this.id, content) + return await XMTP.prepareMessage( + this.client.installationId, + this.id, + content + ) } catch (e) { console.info('ERROR in prepareGroupMessage()', e.message) throw e @@ -161,7 +170,10 @@ export class Group< */ async publishPreparedMessages() { try { - return await XMTP.publishPreparedMessages(this.client.inboxId, this.id) + return await XMTP.publishPreparedMessages( + this.client.installationId, + this.id + ) } catch (e) { console.info('ERROR in publishPreparedMessages()', e.message) throw e @@ -178,9 +190,10 @@ export class Group< * @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. */ + async messages( opts?: MessagesOptions - ): Promise[]> { + ): Promise[]> { return await XMTP.conversationMessages( this.client, this.id, @@ -196,7 +209,7 @@ export class Group< * associated with the group and saves them to the local state. */ async sync() { - await XMTP.syncConversation(this.client.inboxId, this.id) + await XMTP.syncConversation(this.client.installationId, this.id) } /** @@ -210,21 +223,23 @@ export class Group< * @returns {Function} A function that, when called, unsubscribes from the message stream and ends real-time updates. */ async streamMessages( - callback: (message: DecodedMessage) => Promise + callback: ( + message: DecodedMessage + ) => Promise ): Promise<() => void> { - await XMTP.subscribeToMessages(this.client.inboxId, this.id) + await XMTP.subscribeToMessages(this.client.installationId, this.id) const messageSubscription = XMTP.emitter.addListener( EventTypes.ConversationMessage, async ({ - inboxId, + installationId, message, conversationId, }: { - inboxId: string - message: DecodedMessage + installationId: string + message: DecodedMessage conversationId: string }) => { - if (inboxId !== this.client.inboxId) { + if (installationId !== this.client.installationId) { return } if (conversationId !== this.id) { @@ -237,7 +252,7 @@ export class Group< ) return async () => { messageSubscription.remove() - await XMTP.unsubscribeFromMessages(this.client.inboxId, this.id) + await XMTP.unsubscribeFromMessages(this.client.installationId, this.id) } } /** @@ -246,7 +261,7 @@ export class Group< * @returns */ async addMembers(addresses: Address[]): Promise { - return XMTP.addGroupMembers(this.client.inboxId, this.id, addresses) + return XMTP.addGroupMembers(this.client.installationId, this.id, addresses) } /** @@ -255,7 +270,11 @@ export class Group< * @returns */ async removeMembers(addresses: Address[]): Promise { - return XMTP.removeGroupMembers(this.client.inboxId, this.id, addresses) + return XMTP.removeGroupMembers( + this.client.installationId, + this.id, + addresses + ) } /** @@ -264,7 +283,11 @@ export class Group< * @returns */ async addMembersByInboxId(inboxIds: InboxId[]): Promise { - return XMTP.addGroupMembersByInboxId(this.client.inboxId, this.id, inboxIds) + return XMTP.addGroupMembersByInboxId( + this.client.installationId, + this.id, + inboxIds + ) } /** @@ -274,7 +297,7 @@ export class Group< */ async removeMembersByInboxId(inboxIds: InboxId[]): Promise { return XMTP.removeGroupMembersByInboxId( - this.client.inboxId, + this.client.installationId, this.id, inboxIds ) @@ -286,7 +309,7 @@ export class Group< * @returns {string} A Promise that resolves to the group name. */ async groupName(): Promise { - return XMTP.groupName(this.client.inboxId, this.id) + return XMTP.groupName(this.client.installationId, this.id) } /** @@ -297,16 +320,16 @@ export class Group< */ async updateGroupName(groupName: string): Promise { - return XMTP.updateGroupName(this.client.inboxId, this.id, groupName) + return XMTP.updateGroupName(this.client.installationId, this.id, groupName) } /** * Returns the group image url square. * To get the latest group image url square from the network, call sync() first. - * @returns {string} A Promise that resolves to the group name. + * @returns {string} A Promise that resolves to the group image url. */ async groupImageUrlSquare(): Promise { - return XMTP.groupImageUrlSquare(this.client.inboxId, this.id) + return XMTP.groupImageUrlSquare(this.client.installationId, this.id) } /** @@ -318,7 +341,7 @@ export class Group< async updateGroupImageUrlSquare(imageUrlSquare: string): Promise { return XMTP.updateGroupImageUrlSquare( - this.client.inboxId, + this.client.installationId, this.id, imageUrlSquare ) @@ -330,7 +353,7 @@ export class Group< * @returns {string} A Promise that resolves to the group description. */ async groupDescription(): Promise { - return XMTP.groupDescription(this.client.inboxId, this.id) + return XMTP.groupDescription(this.client.installationId, this.id) } /** @@ -342,7 +365,7 @@ export class Group< async updateGroupDescription(description: string): Promise { return XMTP.updateGroupDescription( - this.client.inboxId, + this.client.installationId, this.id, description ) @@ -354,7 +377,7 @@ export class Group< * @returns {string} A Promise that resolves to the group pinned frame url. */ async groupPinnedFrameUrl(): Promise { - return XMTP.groupPinnedFrameUrl(this.client.inboxId, this.id) + return XMTP.groupPinnedFrameUrl(this.client.installationId, this.id) } /** @@ -366,7 +389,7 @@ export class Group< async updateGroupPinnedFrameUrl(pinnedFrameUrl: string): Promise { return XMTP.updateGroupPinnedFrameUrl( - this.client.inboxId, + this.client.installationId, this.id, pinnedFrameUrl ) @@ -379,7 +402,7 @@ export class Group< */ async isActive(): Promise { - return XMTP.isGroupActive(this.client.inboxId, this.id) + return XMTP.isGroupActive(this.client.installationId, this.id) } /** @@ -389,7 +412,7 @@ export class Group< * To get the latest admin status from the network, call sync() first. */ async isAdmin(inboxId: InboxId): Promise { - return XMTP.isAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.isAdmin(this.client.installationId, this.id, inboxId) } /** @@ -399,7 +422,7 @@ export class Group< * To get the latest super admin status from the network, call sync() first. */ async isSuperAdmin(inboxId: InboxId): Promise { - return XMTP.isSuperAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.isSuperAdmin(this.client.installationId, this.id, inboxId) } /** @@ -408,7 +431,7 @@ export class Group< * To get the latest admin list from the network, call sync() first. */ async listAdmins(): Promise { - return XMTP.listAdmins(this.client.inboxId, this.id) + return XMTP.listAdmins(this.client.installationId, this.id) } /** @@ -417,7 +440,7 @@ export class Group< * To get the latest super admin list from the network, call sync() first. */ async listSuperAdmins(): Promise { - return XMTP.listSuperAdmins(this.client.inboxId, this.id) + return XMTP.listSuperAdmins(this.client.installationId, this.id) } /** @@ -427,7 +450,7 @@ export class Group< * Will throw if the user does not have the required permissions. */ async addAdmin(inboxId: InboxId): Promise { - return XMTP.addAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.addAdmin(this.client.installationId, this.id, inboxId) } /** @@ -437,7 +460,7 @@ export class Group< * Will throw if the user does not have the required permissions. */ async addSuperAdmin(inboxId: InboxId): Promise { - return XMTP.addSuperAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.addSuperAdmin(this.client.installationId, this.id, inboxId) } /** @@ -447,7 +470,7 @@ export class Group< * Will throw if the user does not have the required permissions. */ async removeAdmin(inboxId: InboxId): Promise { - return XMTP.removeAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.removeAdmin(this.client.installationId, this.id, inboxId) } /** @@ -457,7 +480,7 @@ export class Group< * Will throw if the user does not have the required permissions. */ async removeSuperAdmin(inboxId: InboxId): Promise { - return XMTP.removeSuperAdmin(this.client.inboxId, this.id, inboxId) + return XMTP.removeSuperAdmin(this.client.installationId, this.id, inboxId) } /** @@ -470,7 +493,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateAddMemberPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -486,7 +509,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateRemoveMemberPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -502,7 +525,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateAddAdminPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -518,7 +541,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateRemoveAdminPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -534,7 +557,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateGroupNamePermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -550,7 +573,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateGroupImageUrlSquarePermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -566,7 +589,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateGroupDescriptionPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -582,7 +605,7 @@ export class Group< permissionOption: PermissionUpdateOption ): Promise { return XMTP.updateGroupPinnedFrameUrlPermission( - this.client.inboxId, + this.client.installationId, this.id, permissionOption ) @@ -593,12 +616,12 @@ export class Group< * @returns {Promise} A {PermissionPolicySet} object representing the group's permission policy set. */ async permissionPolicySet(): Promise { - return XMTP.permissionPolicySet(this.client.inboxId, this.id) + return XMTP.permissionPolicySet(this.client.installationId, this.id) } async processMessage( encryptedMessage: string - ): Promise> { + ): Promise> { try { return await XMTP.processMessage(this.client, this.id, encryptedMessage) } catch (e) { @@ -608,12 +631,15 @@ export class Group< } async consentState(): Promise { - return await XMTP.conversationConsentState(this.client.inboxId, this.id) + return await XMTP.conversationConsentState( + this.client.installationId, + this.id + ) } async updateConsent(state: ConsentState): Promise { return await XMTP.updateConversationConsent( - this.client.inboxId, + this.client.installationId, this.id, state ) @@ -625,6 +651,9 @@ export class Group< * To get the latest member list from the network, call sync() first. */ async members(): Promise { - return await XMTP.listConversationMembers(this.client.inboxId, this.id) + return await XMTP.listConversationMembers( + this.client.installationId, + this.id + ) } } diff --git a/src/lib/Member.ts b/src/lib/Member.ts index 416431be6..514a037b4 100644 --- a/src/lib/Member.ts +++ b/src/lib/Member.ts @@ -1,5 +1,5 @@ import { Address, InboxId } from './Client' -import { ConsentState } from './ConsentListEntry' +import { ConsentState } from './ConsentRecord' export type PermissionLevel = 'member' | 'admin' | 'super_admin' diff --git a/src/lib/PrivatePreferences.ts b/src/lib/PrivatePreferences.ts index 04c8e7825..3820fa5e7 100644 --- a/src/lib/PrivatePreferences.ts +++ b/src/lib/PrivatePreferences.ts @@ -1,11 +1,13 @@ import { Address, Client, InboxId } from './Client' -import { ConsentListEntry, ConsentState } from './ConsentListEntry' +import { ConsentRecord, ConsentState } from './ConsentRecord' +import { EventTypes } from './types/EventTypes' import * as XMTPModule from '../index' import { ConversationId } from '../index' import { getAddress } from '../utils/address' export default class PrivatePreferences { client: Client + private subscriptions: { [key: string]: { remove: () => void } } = {} constructor(client: Client) { this.client = client @@ -15,28 +17,74 @@ export default class PrivatePreferences { conversationId: ConversationId ): Promise { return await XMTPModule.consentConversationIdState( - this.client.inboxId, + this.client.installationId, conversationId ) } async inboxIdConsentState(inboxId: InboxId): Promise { - return await XMTPModule.consentInboxIdState(this.client.inboxId, inboxId) + return await XMTPModule.consentInboxIdState( + this.client.installationId, + inboxId + ) } async addressConsentState(address: Address): Promise { return await XMTPModule.consentAddressState( - this.client.inboxId, + this.client.installationId, getAddress(address) ) } - async setConsentState(consentEntry: ConsentListEntry): Promise { + async setConsentState(consentRecord: ConsentRecord): Promise { return await XMTPModule.setConsentState( - this.client.inboxId, - consentEntry.value, - consentEntry.entryType, - consentEntry.permissionType + this.client.installationId, + consentRecord.value, + consentRecord.entryType, + consentRecord.state + ) + } + + async syncConsent(): Promise { + return await XMTPModule.syncConsent(this.client.installationId) + } + + /** + * This method streams consent. + * @returns {Promise} A Promise that resolves to an array of ConsentRecord objects. + */ + async streamConsent( + callback: (consent: ConsentRecord) => Promise + ): Promise { + XMTPModule.subscribeToConsent(this.client.installationId) + const subscription = XMTPModule.emitter.addListener( + EventTypes.Consent, + async ({ + installationId, + consent, + }: { + installationId: string + consent: ConsentRecord + }) => { + if (installationId !== this.client.installationId) { + return + } + return await callback( + new ConsentRecord(consent.value, consent.entryType, consent.state) + ) + } ) + this.subscriptions[EventTypes.Consent] = subscription + } + + /** + * Cancels the stream for new consent records. + */ + cancelStreamConsent() { + if (this.subscriptions[EventTypes.Consent]) { + this.subscriptions[EventTypes.Consent].remove() + delete this.subscriptions[EventTypes.Consent] + } + XMTPModule.unsubscribeFromConsent(this.client.installationId) } } diff --git a/src/lib/types/DecodedMessageUnion.ts b/src/lib/types/DecodedMessageUnion.ts new file mode 100644 index 000000000..52c359f04 --- /dev/null +++ b/src/lib/types/DecodedMessageUnion.ts @@ -0,0 +1,11 @@ +import { DefaultContentTypes } from './DefaultContentType' +import { ContentCodec } from '../ContentCodec' +import { DecodedMessage } from '../DecodedMessage' + +export type DecodedMessageUnion< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +> = { + [K in keyof ContentTypes]: ContentTypes[K] extends ContentCodec + ? DecodedMessage + : never +}[number] diff --git a/src/lib/types/EventTypes.ts b/src/lib/types/EventTypes.ts index f6cbed4b3..9f2c0d3f3 100644 --- a/src/lib/types/EventTypes.ts +++ b/src/lib/types/EventTypes.ts @@ -17,4 +17,8 @@ export enum EventTypes { * A new message is sent to a specific conversation */ ConversationMessage = 'conversationMessage', + /** + * A inboxId or conversation has been approved or denied + */ + Consent = 'consent', } diff --git a/yarn.lock b/yarn.lock index 5a80a1db8..6b3235ac4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1190,6 +1190,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.5.5": + version "7.26.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.0.tgz#8600c2f595f277c60815256418b85356a65173c1" + integrity sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.0.0", "@babel/template@^7.22.15", "@babel/template@^7.23.9", "@babel/template@^7.3.3": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" @@ -1224,6 +1231,202 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@changesets/apply-release-plan@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@changesets/apply-release-plan/-/apply-release-plan-7.0.6.tgz#39af3f80f3ba287920271d1a542ef5394eb0bf8a" + integrity sha512-TKhVLtiwtQOgMAC0fCJfmv93faiViKSDqr8oMEqrnNs99gtSC1sZh/aEMS9a+dseU1ESZRCK+ofLgGY7o0fw/Q== + dependencies: + "@changesets/config" "^3.0.4" + "@changesets/get-version-range-type" "^0.4.0" + "@changesets/git" "^3.0.2" + "@changesets/should-skip-package" "^0.1.1" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + detect-indent "^6.0.0" + fs-extra "^7.0.1" + lodash.startcase "^4.4.0" + outdent "^0.5.0" + prettier "^2.7.1" + resolve-from "^5.0.0" + semver "^7.5.3" + +"@changesets/assemble-release-plan@^6.0.5": + version "6.0.5" + resolved "https://registry.yarnpkg.com/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.5.tgz#d987b01c2d91c8b2f81eedd2df56ba355e4ce536" + integrity sha512-IgvBWLNKZd6k4t72MBTBK3nkygi0j3t3zdC1zrfusYo0KpdsvnDjrMM9vPnTCLCMlfNs55jRL4gIMybxa64FCQ== + dependencies: + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.2" + "@changesets/should-skip-package" "^0.1.1" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + semver "^7.5.3" + +"@changesets/changelog-git@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@changesets/changelog-git/-/changelog-git-0.2.0.tgz#1f3de11becafff5a38ebe295038a602403c93a86" + integrity sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ== + dependencies: + "@changesets/types" "^6.0.0" + +"@changesets/cli@^2.27.10": + version "2.27.10" + resolved "https://registry.yarnpkg.com/@changesets/cli/-/cli-2.27.10.tgz#b2b98caaf6f8a6630592456f07a881e7684f6ada" + integrity sha512-PfeXjvs9OfQJV8QSFFHjwHX3QnUL9elPEQ47SgkiwzLgtKGyuikWjrdM+lO9MXzOE22FO9jEGkcs4b+B6D6X0Q== + dependencies: + "@changesets/apply-release-plan" "^7.0.6" + "@changesets/assemble-release-plan" "^6.0.5" + "@changesets/changelog-git" "^0.2.0" + "@changesets/config" "^3.0.4" + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.2" + "@changesets/get-release-plan" "^4.0.5" + "@changesets/git" "^3.0.2" + "@changesets/logger" "^0.1.1" + "@changesets/pre" "^2.0.1" + "@changesets/read" "^0.6.2" + "@changesets/should-skip-package" "^0.1.1" + "@changesets/types" "^6.0.0" + "@changesets/write" "^0.3.2" + "@manypkg/get-packages" "^1.1.3" + ansi-colors "^4.1.3" + ci-info "^3.7.0" + enquirer "^2.3.0" + external-editor "^3.1.0" + fs-extra "^7.0.1" + mri "^1.2.0" + p-limit "^2.2.0" + package-manager-detector "^0.2.0" + picocolors "^1.1.0" + resolve-from "^5.0.0" + semver "^7.5.3" + spawndamnit "^3.0.1" + term-size "^2.1.0" + +"@changesets/config@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@changesets/config/-/config-3.0.4.tgz#2acfdf3e09424149684b3bd10c88074becf251aa" + integrity sha512-+DiIwtEBpvvv1z30f8bbOsUQGuccnZl9KRKMM/LxUHuDu5oEjmN+bJQ1RIBKNJjfYMQn8RZzoPiX0UgPaLQyXw== + dependencies: + "@changesets/errors" "^0.2.0" + "@changesets/get-dependents-graph" "^2.1.2" + "@changesets/logger" "^0.1.1" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + fs-extra "^7.0.1" + micromatch "^4.0.8" + +"@changesets/errors@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@changesets/errors/-/errors-0.2.0.tgz#3c545e802b0f053389cadcf0ed54e5636ff9026a" + integrity sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow== + dependencies: + extendable-error "^0.1.5" + +"@changesets/get-dependents-graph@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@changesets/get-dependents-graph/-/get-dependents-graph-2.1.2.tgz#108304652d4bf22c9fee9f1d31dcf9908c24ca51" + integrity sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ== + dependencies: + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + picocolors "^1.1.0" + semver "^7.5.3" + +"@changesets/get-release-plan@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@changesets/get-release-plan/-/get-release-plan-4.0.5.tgz#2c857ce2f1942b88ff6ffcb24667edc52bcbfaea" + integrity sha512-E6wW7JoSMcctdVakut0UB76FrrN3KIeJSXvB+DHMFo99CnC3ZVnNYDCVNClMlqAhYGmLmAj77QfApaI3ca4Fkw== + dependencies: + "@changesets/assemble-release-plan" "^6.0.5" + "@changesets/config" "^3.0.4" + "@changesets/pre" "^2.0.1" + "@changesets/read" "^0.6.2" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + +"@changesets/get-version-range-type@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz#429a90410eefef4368502c41c63413e291740bf5" + integrity sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ== + +"@changesets/git@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@changesets/git/-/git-3.0.2.tgz#669c700049dc3b8ba53f46de45f5c4b1e6ddea3b" + integrity sha512-r1/Kju9Y8OxRRdvna+nxpQIsMsRQn9dhhAZt94FLDeu0Hij2hnOozW8iqnHBgvu+KdnJppCveQwK4odwfw/aWQ== + dependencies: + "@changesets/errors" "^0.2.0" + "@manypkg/get-packages" "^1.1.3" + is-subdir "^1.1.1" + micromatch "^4.0.8" + spawndamnit "^3.0.1" + +"@changesets/logger@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@changesets/logger/-/logger-0.1.1.tgz#9926ac4dc8fb00472fe1711603b6b4755d64b435" + integrity sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg== + dependencies: + picocolors "^1.1.0" + +"@changesets/parse@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@changesets/parse/-/parse-0.4.0.tgz#5cabbd9844b3b213cb83f5edb5768454c70dd2b4" + integrity sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw== + dependencies: + "@changesets/types" "^6.0.0" + js-yaml "^3.13.1" + +"@changesets/pre@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@changesets/pre/-/pre-2.0.1.tgz#3ed60f9d218b3b81d3074d72139582da11a94d5f" + integrity sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ== + dependencies: + "@changesets/errors" "^0.2.0" + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + fs-extra "^7.0.1" + +"@changesets/read@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@changesets/read/-/read-0.6.2.tgz#816cf75dd22a70e75ac279474e44be52fb3fb91b" + integrity sha512-wjfQpJvryY3zD61p8jR87mJdyx2FIhEcdXhKUqkja87toMrP/3jtg/Yg29upN+N4Ckf525/uvV7a4tzBlpk6gg== + dependencies: + "@changesets/git" "^3.0.2" + "@changesets/logger" "^0.1.1" + "@changesets/parse" "^0.4.0" + "@changesets/types" "^6.0.0" + fs-extra "^7.0.1" + p-filter "^2.1.0" + picocolors "^1.1.0" + +"@changesets/should-skip-package@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@changesets/should-skip-package/-/should-skip-package-0.1.1.tgz#76218ef4ce7691351a6dffdb356e8893267b0b3a" + integrity sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg== + dependencies: + "@changesets/types" "^6.0.0" + "@manypkg/get-packages" "^1.1.3" + +"@changesets/types@^4.0.1": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@changesets/types/-/types-4.1.0.tgz#fb8f7ca2324fd54954824e864f9a61a82cb78fe0" + integrity sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw== + +"@changesets/types@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@changesets/types/-/types-6.0.0.tgz#e46abda9890610dd1fbe1617730173d2267544bd" + integrity sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ== + +"@changesets/write@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@changesets/write/-/write-0.3.2.tgz#bee64e4ccdff480872df5d1e38f2b913cb940116" + integrity sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw== + dependencies: + "@changesets/types" "^6.0.0" + fs-extra "^7.0.1" + human-id "^1.0.2" + prettier "^2.7.1" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -1560,6 +1763,28 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@manypkg/find-root@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@manypkg/find-root/-/find-root-1.1.0.tgz#a62d8ed1cd7e7d4c11d9d52a8397460b5d4ad29f" + integrity sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA== + dependencies: + "@babel/runtime" "^7.5.5" + "@types/node" "^12.7.1" + find-up "^4.1.0" + fs-extra "^8.1.0" + +"@manypkg/get-packages@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@manypkg/get-packages/-/get-packages-1.1.3.tgz#e184db9bba792fa4693de4658cfb1463ac2c9c47" + integrity sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A== + dependencies: + "@babel/runtime" "^7.5.5" + "@changesets/types" "^4.0.1" + "@manypkg/find-root" "^1.1.0" + fs-extra "^8.1.0" + globby "^11.0.0" + read-yaml-file "^1.1.0" + "@msgpack/msgpack@^3.0.0-beta2": version "3.0.0-beta2" resolved "https://registry.yarnpkg.com/@msgpack/msgpack/-/msgpack-3.0.0-beta2.tgz#5bccee30f84df220b33905e3d8249ba96deca0b7" @@ -2318,6 +2543,11 @@ dependencies: undici-types "~5.26.4" +"@types/node@^12.7.1": + version "12.20.55" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" + integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== + "@types/normalize-package-data@^2.4.0", "@types/normalize-package-data@^2.4.1": version "2.4.4" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" @@ -2686,6 +2916,11 @@ ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ansi-colors@^4.1.1, ansi-colors@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -3060,6 +3295,13 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== +better-path-resolve@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/better-path-resolve/-/better-path-resolve-1.0.0.tgz#13a35a1104cdd48a7b74bf8758f96a1ee613f99d" + integrity sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g== + dependencies: + is-windows "^1.0.0" + big-integer@1.6.x: version "1.6.52" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" @@ -3121,6 +3363,13 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + browserslist@^4.22.2: version "4.22.3" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.3.tgz#299d11b7e947a6b843981392721169e27d60c5a6" @@ -3290,6 +3539,11 @@ char-regex@^2.0.0: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + chokidar@^3.4.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -3310,7 +3564,7 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -ci-info@^3.2.0, ci-info@^3.6.1, ci-info@^3.7.1, ci-info@^3.8.0: +ci-info@^3.2.0, ci-info@^3.6.1, ci-info@^3.7.0, ci-info@^3.7.1, ci-info@^3.8.0: version "3.9.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== @@ -3553,6 +3807,15 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +cross-spawn@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + crypto-random-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" @@ -3683,6 +3946,11 @@ deprecation@^2.0.0: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -3767,6 +4035,14 @@ encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" +enquirer@^2.3.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" @@ -4247,6 +4523,20 @@ exponential-backoff@^3.1.1: resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== +extendable-error@^0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/extendable-error/-/extendable-error-0.1.7.tgz#60b9adf206264ac920058a7395685ae4670c2b96" + integrity sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg== + +external-editor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" @@ -4326,6 +4616,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-cache-dir@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" @@ -4452,6 +4749,24 @@ fs-extra@^11.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -4675,7 +4990,7 @@ globalthis@^1.0.3: dependencies: define-properties "^1.1.3" -globby@^11.1.0: +globby@^11.0.0, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -4711,7 +5026,7 @@ graceful-fs@4.2.10: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.5, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4865,6 +5180,11 @@ https-proxy-agent@^7.0.0: agent-base "^7.0.2" debug "4" +human-id@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/human-id/-/human-id-1.0.2.tgz#e654d4b2b0d8b07e45da9f6020d8af17ec0a5df3" + integrity sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw== + human-signals@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" @@ -4889,6 +5209,13 @@ iconv-lite@0.6.3, iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -5197,6 +5524,13 @@ is-string@^1.0.5, is-string@^1.0.7: dependencies: has-tostringtag "^1.0.0" +is-subdir@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-subdir/-/is-subdir-1.2.0.tgz#b791cd28fab5202e91a08280d51d9d7254fd20d4" + integrity sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw== + dependencies: + better-path-resolve "1.0.0" + is-symbol@^1.0.2, is-symbol@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" @@ -5243,6 +5577,11 @@ is-weakset@^2.0.1: call-bind "^1.0.2" get-intrinsic "^1.1.1" +is-windows@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" @@ -5488,7 +5827,7 @@ jest-worker@^29.7.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1: +js-yaml@^3.13.1, js-yaml@^3.6.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -5632,6 +5971,13 @@ jsonc-parser@^3.2.0: resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -5906,6 +6252,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.startcase@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" + integrity sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg== + lodash.uniqby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz#d99c07a669e9e6d24e1362dfe266c67616af1302" @@ -6092,6 +6443,14 @@ micromatch@^4.0.2, micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mime-db@1.52.0: version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" @@ -6254,6 +6613,11 @@ modify-values@^1.0.1: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +mri@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -6690,11 +7054,28 @@ optionator@^0.9.3: prelude-ls "^1.2.1" type-check "^0.4.0" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + +outdent@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.5.0.tgz#9e10982fdc41492bb473ad13840d22f9655be2ff" + integrity sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q== + p-each-series@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-3.0.0.tgz#d1aed5e96ef29864c897367a7d2a628fdc960806" integrity sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw== +p-filter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" + integrity sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw== + dependencies: + p-map "^2.0.0" + p-filter@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-4.1.0.tgz#fe0aa794e2dfad8ecf595a39a245484fcd09c6e4" @@ -6770,6 +7151,11 @@ p-locate@^6.0.0: dependencies: p-limit "^4.0.0" +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -6797,6 +7183,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-manager-detector@^0.2.0: + version "0.2.7" + resolved "https://registry.yarnpkg.com/package-manager-detector/-/package-manager-detector-0.2.7.tgz#6c3e47d7794fdd513512d02e2160c24ba559e39b" + integrity sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ== + pacote@^15.0.0, pacote@^15.0.8, pacote@^15.2.0: version "15.2.0" resolved "https://registry.yarnpkg.com/pacote/-/pacote-15.2.0.tgz#0f0dfcc3e60c7b39121b2ac612bf8596e95344d3" @@ -6931,6 +7322,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -6995,6 +7391,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@^2.7.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + prettier@^3.1.0: version "3.2.5" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" @@ -7229,6 +7630,16 @@ read-pkg@^8.0.0, read-pkg@^8.1.0: parse-json "^7.0.0" type-fest "^4.2.0" +read-yaml-file@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-1.1.0.tgz#9362bbcbdc77007cc8ea4519fe1c0b821a7ce0d8" + integrity sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA== + dependencies: + graceful-fs "^4.1.5" + js-yaml "^3.6.1" + pify "^4.0.1" + strip-bom "^3.0.0" + read@^2.0.0, read@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/read/-/read-2.1.0.tgz#69409372c54fe3381092bc363a00650b6ac37218" @@ -7478,7 +7889,7 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3.0.0": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -7746,6 +8157,14 @@ spawn-error-forwarder@~1.0.0: resolved "https://registry.yarnpkg.com/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz#1afd94738e999b0346d7b9fc373be55e07577029" integrity sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g== +spawndamnit@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spawndamnit/-/spawndamnit-3.0.1.tgz#44410235d3dc4e21f8e4f740ae3266e4486c2aed" + integrity sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg== + dependencies: + cross-spawn "^7.0.5" + signal-exit "^4.0.1" + spdx-correct@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" @@ -7880,16 +8299,7 @@ string-length@^5.0.1: char-regex "^2.0.0" strip-ansi "^7.0.1" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7963,14 +8373,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8110,6 +8513,11 @@ tempy@^3.0.0: type-fest "^2.12.2" unique-string "^3.0.0" +term-size@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" + integrity sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg== + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -8166,6 +8574,13 @@ tiny-relative-date@^1.3.0: resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -8469,6 +8884,11 @@ universal-user-agent@^6.0.0: resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.1.tgz#15f20f55da3c930c57bddbf1734c6654d5fd35aa" integrity sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ== +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -8683,16 +9103,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==