From d352e535f64b1cfca4375dd31577e33ae7886020 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 12:01:40 -0700 Subject: [PATCH 1/3] adds newGroupCustomPermissions function --- android/build.gradle | 2 +- .../modules/xmtpreactnativesdk/XMTPModule.kt | 19 ++++ .../wrappers/PermissionPolicySetWrapper.kt | 25 +++++ example/ios/Podfile.lock | 14 +-- example/src/tests/groupPermissionsTests.ts | 95 +++++++++++++++++++ ios/Wrappers/PermissionPolicySetWrapper.swift | 1 + ios/XMTPModule.swift | 28 ++++++ ios/XMTPReactNative.podspec | 2 +- src/index.ts | 30 ++++++ src/lib/Conversations.ts | 30 +++++- src/lib/types/PermissionPolicySet.ts | 9 +- 11 files changed, 241 insertions(+), 14 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 988c5a45..90fc41ef 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.14.9" + implementation "org.xmtp:android:0.14.10" 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 9c3263a1..962269af 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -892,6 +892,25 @@ class XMTPModule : Module() { } } + AsyncFunction("createGroupCustomPermissions") Coroutine { inboxId: String, peerAddresses: List, permissionPolicySetJson: String, groupOptionsJson: String -> + withContext(Dispatchers.IO) { + logV("createGroup") + val client = clients[inboxId] ?: throw XMTPException("No client") + val createGroupParams = + CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + val permissionPolicySet = PermissionPolicySetWrapper.createPermissionPolicySetFromJson(permissionPolicySetJson) + val group = client.conversations.newGroupCustomPermissions( + peerAddresses, + permissionPolicySet, + createGroupParams.groupName, + createGroupParams.groupImageUrlSquare, + createGroupParams.groupDescription, + createGroupParams.groupPinnedFrameUrl + ) + GroupWrapper.encode(client, group) + } + } + AsyncFunction("listMemberInboxIds") Coroutine { inboxId: String, groupId: String -> withContext(Dispatchers.IO) { diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt index 0922d33a..a6f1b978 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/PermissionPolicySetWrapper.kt @@ -1,6 +1,7 @@ package expo.modules.xmtpreactnativesdk.wrappers import com.google.gson.GsonBuilder +import com.google.gson.JsonParser import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionPolicySet @@ -16,6 +17,16 @@ class PermissionPolicySetWrapper { PermissionOption.Unknown -> "unknown" } } + + fun createPermissionOptionFromString(permissionOptionString: String): PermissionOption { + return when (permissionOptionString) { + "allow" -> PermissionOption.Allow + "deny" -> PermissionOption.Deny + "admin" -> PermissionOption.Admin + "superAdmin" -> PermissionOption.SuperAdmin + else -> PermissionOption.Unknown + } + } fun encodeToObj(policySet: PermissionPolicySet): Map { return mapOf( "addMemberPolicy" to fromPermissionOption(policySet.addMemberPolicy), @@ -29,6 +40,20 @@ class PermissionPolicySetWrapper { ) } + fun createPermissionPolicySetFromJson(permissionPolicySetJson: String): PermissionPolicySet { + val jsonObj = JsonParser.parseString(permissionPolicySetJson).asJsonObject + return PermissionPolicySet( + addMemberPolicy = createPermissionOptionFromString(jsonObj.get("addMemberPolicy").asString), + removeMemberPolicy = createPermissionOptionFromString(jsonObj.get("removeMemberPolicy").asString), + addAdminPolicy = createPermissionOptionFromString(jsonObj.get("addAdminPolicy").asString), + removeAdminPolicy = createPermissionOptionFromString(jsonObj.get("removeAdminPolicy").asString), + updateGroupNamePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupNamePolicy").asString), + updateGroupDescriptionPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupDescriptionPolicy").asString), + updateGroupImagePolicy = createPermissionOptionFromString(jsonObj.get("updateGroupImagePolicy").asString), + updateGroupPinnedFrameUrlPolicy = createPermissionOptionFromString(jsonObj.get("updateGroupPinnedFrameUrlPolicy").asString) + ) + } + fun encodeToJsonString(policySet: PermissionPolicySet): String { val gson = GsonBuilder().create() val obj = encodeToObj(policySet) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 03fe186f..8fa3073f 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -56,7 +56,7 @@ PODS: - hermes-engine/Pre-built (= 0.71.14) - hermes-engine/Pre-built (0.71.14) - libevent (2.1.12) - - LibXMTP (0.5.6-beta0) + - LibXMTP (0.5.6-beta1) - Logging (1.0.0) - MessagePacker (0.4.7) - MMKV (1.3.7): @@ -449,16 +449,16 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.13.8): + - XMTP (0.13.9): - Connect-Swift (= 0.12.0) - GzipSwift - - LibXMTP (= 0.5.6-beta0) + - LibXMTP (= 0.5.6-beta1) - web3.swift - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.13.8) + - XMTP (= 0.13.9) - Yoga (1.14.0) DEPENDENCIES: @@ -711,7 +711,7 @@ SPEC CHECKSUMS: GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa hermes-engine: d7cc127932c89c53374452d6f93473f1970d8e88 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - LibXMTP: e7682dedb10e18343c011280d494a8e4a43d9eb7 + LibXMTP: 2205108c6c3a2bcdc405e42d4c718ad87c31a7c2 Logging: 9ef4ecb546ad3169398d5a723bc9bea1c46bef26 MessagePacker: ab2fe250e86ea7aedd1a9ee47a37083edd41fd02 MMKV: 36a22a9ec84c9bb960613a089ddf6f48be9312b0 @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 7d0a3f3b22916acfbb0ae67f1ca6bbd3f5956138 - XMTPReactNative: 51e5b1b8669dab2ad5e2d74b518146388f5f425e + XMTP: 518a21ff9d2b7235dbf8d79fdc388a576c94f1e2 + XMTPReactNative: e3803ae32fa0dd849c2265b8d9109f682840ee24 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 71f3e840..cbff2c77 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -1,4 +1,6 @@ +import { permissionPolicySet } from 'xmtp-react-native-sdk' import { Test, assert, createClients } from './test-utils' +import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' export const groupPermissionsTests: Test[] = [] let counter = 1 @@ -512,3 +514,96 @@ test('can update group pinned frame', async () => { return true }) + +test('can create a group with custom permissions', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + const customPermissionsPolicySet: PermissionPolicySet = { + addMemberPolicy: 'allow', + removeMemberPolicy: 'deny', + addAdminPolicy: 'admin', + removeAdminPolicy: 'superAdmin', + updateGroupNamePolicy: 'admin', + updateGroupDescriptionPolicy: 'allow', + updateGroupImagePolicy: 'admin', + updateGroupPinnedFrameUrlPolicy: 'deny', + } + + // Bo creates a group with Alix and Caro with custom permissions + const boGroup = await bo.conversations.newGroupCustomPermissions( + [alix.address, caro.address], + customPermissionsPolicySet + ) + + // Verify that bo can read the correct permissions + await alix.conversations.syncGroups() + const alixGroup = (await alix.conversations.listGroups())[0] + const permissions = await alixGroup.permissionPolicySet() + assert(permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}`) + assert(permissions.removeMemberPolicy === customPermissionsPolicySet.removeMemberPolicy, `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}`) + assert(permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}`) + assert(permissions.removeAdminPolicy === customPermissionsPolicySet.removeAdminPolicy, `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}`) + assert(permissions.updateGroupNamePolicy === customPermissionsPolicySet.updateGroupNamePolicy, `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}`) + assert(permissions.updateGroupDescriptionPolicy === customPermissionsPolicySet.updateGroupDescriptionPolicy, `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}`) + assert(permissions.updateGroupImagePolicy === customPermissionsPolicySet.updateGroupImagePolicy, `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}`) + assert(permissions.updateGroupPinnedFrameUrlPolicy === customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}`) + + // Verify that bo can not update the pinned frame even though they are a super admin + try { + await boGroup.updateGroupPinnedFrameUrl('new pinned frame') + assert(false, 'Bo should not be able to update the group pinned frame') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // Verify that alix can update the group description + await alixGroup.updateGroupDescription('new description') + await alixGroup.sync() + assert( + (await alixGroup.groupDescription()) === 'new description', + `alixGroup.groupDescription should be "new description" but was ${alixGroup.groupDescription}` + ) + + // Verify that alix can not update the group name + try { + await alixGroup.updateGroupName('new name') + assert(false, 'Alix should not be able to update the group name') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + return true +}) + +test('creating a group with invalid permissions should fail', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Add/Remove admin must be admin or super admin + const customPermissionsPolicySet: PermissionPolicySet = { + addMemberPolicy: 'allow', + removeMemberPolicy: 'deny', + addAdminPolicy: 'allow', + removeAdminPolicy: 'superAdmin', + updateGroupNamePolicy: 'admin', + updateGroupDescriptionPolicy: 'allow', + updateGroupImagePolicy: 'admin', + updateGroupPinnedFrameUrlPolicy: 'deny', + } + + // Bo creates a group with Alix and Caro + try { + const boGroup = await bo.conversations.newGroupCustomPermissions( + [alix.address, caro.address], + customPermissionsPolicySet + ) + assert(false, 'Group creation should fail') + } catch (error) { + // expected + } + return true +}) + diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index 1ca923c8..bac7d584 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -25,6 +25,7 @@ class PermissionPolicySetWrapper { } static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { + return [ "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 97707966..8ad214e1 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -744,6 +744,27 @@ public class XMTPModule: Module { throw error } } + +// AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionJson: String, groupOptionsJson: String) -> String in +// guard let client = await clientsManager.getClient(key: inboxId) else { +// throw Error.noClient +// } +// do { +// let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) +// let group = try await client.conversations.newGroupCustomP( +// with: peerAddresses, +// permissions: permissionLevel, +// name: createGroupParams.groupName, +// imageUrlSquare: createGroupParams.groupImageUrlSquare, +// description: createGroupParams.groupDescription, +// pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl +// ) +// return try GroupWrapper.encode(group, client: client) +// } catch { +// print("ERRRO!: \(error.localizedDescription)") +// throw error +// } +// } AsyncFunction("listMemberInboxIds") { (inboxId: String, groupId: String) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -1431,6 +1452,13 @@ public class XMTPModule: Module { throw Error.invalidPermissionOption } } + +// func createPermissionPolicySetFromJSON(permissionPolicySetJson: String) -> PermissionPolicySet { +// let data = permissionPolicySetJson.data(using: .utf8) ?? Data() +// let jsonObj = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? [:] +// +// return PermissionPolicySet +// } func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, enableV3: Bool = false, dbEncryptionKey: Data? = nil, dbDirectory: String? = nil, historySyncUrl: String? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index c7fa69b3..6a267bd9 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -26,5 +26,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency 'secp256k1.swift' s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.13.8" + s.dependency "XMTP", "= 0.13.10" end diff --git a/src/index.ts b/src/index.ts index 33668b5a..56442197 100644 --- a/src/index.ts +++ b/src/index.ts @@ -191,6 +191,36 @@ export async function createGroup< ) } +export async function createGroupCustomPermissions< + ContentTypes extends DefaultContentTypes = DefaultContentTypes, +>( + client: Client, + peerAddresses: string[], + permissionPolicySet: PermissionPolicySet, + name: string = '', + imageUrlSquare: string = '', + description: string = '', + pinnedFrameUrl: string = '' +): Promise> { + const options: CreateGroupParams = { + name, + imageUrlSquare, + description, + pinnedFrameUrl, + } + return new Group( + client, + JSON.parse( + await XMTPModule.createGroupCustomPermissions( + client.inboxId, + peerAddresses, + JSON.stringify(permissionPolicySet), + JSON.stringify(options) + ) + ) + ) +} + export async function listGroups< ContentTypes extends DefaultContentTypes = DefaultContentTypes, >(client: Client): Promise[]> { diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 15f6030f..7821d190 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -14,6 +14,7 @@ import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' import { getAddress } from '../utils/address' +import { PermissionPolicySet } from './types/PermissionPolicySet' export default class Conversations< ContentTypes extends ContentCodec[] = [], @@ -167,9 +168,10 @@ export default class Conversations< /** * Creates a new group. * - * This method creates a new conversation with the specified peer address and context. + * This method creates a new group with the specified peer addresses and options. * * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ async newGroup( @@ -187,6 +189,32 @@ export default class Conversations< ) } + /** + * Creates a new group with custom permissions. + * + * This method creates a new group with the specified peer addresses and options. + * + * @param {string[]} peerAddresses - The addresses of the peers to create a group with. + * @param {PermissionPolicySet} permissionPolicySet - The permission policy set to use for the group. + * @param {CreateGroupOptions} opts - The options to use for the group. + * @returns {Promise>} A Promise that resolves to a Group object. + */ + async newGroupCustomPermissions( + peerAddresses: string[], + permissionPolicySet: PermissionPolicySet, + opts?: CreateGroupOptions | undefined + ): Promise> { + return await XMTPModule.createGroupCustomPermissions( + this.client, + peerAddresses, + permissionPolicySet, + opts?.name, + opts?.imageUrlSquare, + opts?.description, + opts?.pinnedFrameUrl + ) + } + /** * Executes a network request to fetch the latest list of groups assoociated with the client * and save them to the local state. diff --git a/src/lib/types/PermissionPolicySet.ts b/src/lib/types/PermissionPolicySet.ts index 2e796860..64bc4974 100644 --- a/src/lib/types/PermissionPolicySet.ts +++ b/src/lib/types/PermissionPolicySet.ts @@ -1,10 +1,11 @@ export type PermissionOption = - | 'allow' - | 'deny' - | 'admin' - | 'superAdmin' + | 'allow' // Any members of the group can perform this action + | 'deny' // No members of the group can perform this action + | 'admin' // Only admins or super admins of the group can perform this action + | 'superAdmin' // Only the super admin of the group can perform this action | 'unknown' +// Add Admin and Remove admin must be set to either 'admin' or 'superAdmin' to be valid export type PermissionPolicySet = { addMemberPolicy: PermissionOption removeMemberPolicy: PermissionOption From 44e224c364fac57b10a560b8b4adb419abd528ba Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 12:17:52 -0700 Subject: [PATCH 2/3] lint fixes --- example/src/tests/groupPermissionsTests.ts | 62 ++++++++++++++++------ example/src/tests/groupTests.ts | 2 +- example/src/tests/tests.ts | 7 ++- src/lib/Conversations.ts | 6 +-- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index cbff2c77..22463a9f 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -1,7 +1,7 @@ -import { permissionPolicySet } from 'xmtp-react-native-sdk' -import { Test, assert, createClients } from './test-utils' import { PermissionPolicySet } from 'xmtp-react-native-sdk/lib/types/PermissionPolicySet' +import { Test, assert, createClients } from './test-utils' + export const groupPermissionsTests: Test[] = [] let counter = 1 function test(name: string, perform: () => Promise) { @@ -540,14 +540,44 @@ test('can create a group with custom permissions', async () => { await alix.conversations.syncGroups() const alixGroup = (await alix.conversations.listGroups())[0] const permissions = await alixGroup.permissionPolicySet() - assert(permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}`) - assert(permissions.removeMemberPolicy === customPermissionsPolicySet.removeMemberPolicy, `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}`) - assert(permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}`) - assert(permissions.removeAdminPolicy === customPermissionsPolicySet.removeAdminPolicy, `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}`) - assert(permissions.updateGroupNamePolicy === customPermissionsPolicySet.updateGroupNamePolicy, `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}`) - assert(permissions.updateGroupDescriptionPolicy === customPermissionsPolicySet.updateGroupDescriptionPolicy, `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}`) - assert(permissions.updateGroupImagePolicy === customPermissionsPolicySet.updateGroupImagePolicy, `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}`) - assert(permissions.updateGroupPinnedFrameUrlPolicy === customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}`) + assert( + permissions.addMemberPolicy === customPermissionsPolicySet.addMemberPolicy, + `permissions.addMemberPolicy should be ${customPermissionsPolicySet.addMemberPolicy} but was ${permissions.addMemberPolicy}` + ) + assert( + permissions.removeMemberPolicy === + customPermissionsPolicySet.removeMemberPolicy, + `permissions.removeMemberPolicy should be ${customPermissionsPolicySet.removeMemberPolicy} but was ${permissions.removeMemberPolicy}` + ) + assert( + permissions.addAdminPolicy === customPermissionsPolicySet.addAdminPolicy, + `permissions.addAdminPolicy should be ${customPermissionsPolicySet.addAdminPolicy} but was ${permissions.addAdminPolicy}` + ) + assert( + permissions.removeAdminPolicy === + customPermissionsPolicySet.removeAdminPolicy, + `permissions.removeAdminPolicy should be ${customPermissionsPolicySet.removeAdminPolicy} but was ${permissions.removeAdminPolicy}` + ) + assert( + permissions.updateGroupNamePolicy === + customPermissionsPolicySet.updateGroupNamePolicy, + `permissions.updateGroupNamePolicy should be ${customPermissionsPolicySet.updateGroupNamePolicy} but was ${permissions.updateGroupNamePolicy}` + ) + assert( + permissions.updateGroupDescriptionPolicy === + customPermissionsPolicySet.updateGroupDescriptionPolicy, + `permissions.updateGroupDescriptionPolicy should be ${customPermissionsPolicySet.updateGroupDescriptionPolicy} but was ${permissions.updateGroupDescriptionPolicy}` + ) + assert( + permissions.updateGroupImagePolicy === + customPermissionsPolicySet.updateGroupImagePolicy, + `permissions.updateGroupImagePolicy should be ${customPermissionsPolicySet.updateGroupImagePolicy} but was ${permissions.updateGroupImagePolicy}` + ) + assert( + permissions.updateGroupPinnedFrameUrlPolicy === + customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy, + `permissions.updateGroupPinnedFrameUrlPolicy should be ${customPermissionsPolicySet.updateGroupPinnedFrameUrlPolicy} but was ${permissions.updateGroupPinnedFrameUrlPolicy}` + ) // Verify that bo can not update the pinned frame even though they are a super admin try { @@ -556,7 +586,7 @@ test('can create a group with custom permissions', async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected - } + } // Verify that alix can update the group description await alixGroup.updateGroupDescription('new description') @@ -596,14 +626,14 @@ test('creating a group with invalid permissions should fail', async () => { // Bo creates a group with Alix and Caro try { - const boGroup = await bo.conversations.newGroupCustomPermissions( + await bo.conversations.newGroupCustomPermissions( [alix.address, caro.address], - customPermissionsPolicySet - ) - assert(false, 'Group creation should fail') + customPermissionsPolicySet + ) + assert(false, 'Group creation should fail') + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected } return true }) - diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 756595a3..e171c9e0 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -31,7 +31,7 @@ test('can make a MLS V3 client', async () => { 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]) - const client = await Client.createRandom({ + await Client.createRandom({ env: 'local', appVersion: 'Testing/0.0.0', enableV3: true, diff --git a/example/src/tests/tests.ts b/example/src/tests/tests.ts index b1250276..ad1b6c6c 100644 --- a/example/src/tests/tests.ts +++ b/example/src/tests/tests.ts @@ -1,15 +1,14 @@ -import { sha256 } from '@noble/hashes/sha256' import { FramesClient } from '@xmtp/frames-client' -import { content, invitation, signature as signatureProto } from '@xmtp/proto' +import { content, invitation } from '@xmtp/proto' import { createHmac } from 'crypto' import ReactNativeBlobUtil from 'react-native-blob-util' import Config from 'react-native-config' import { TextEncoder, TextDecoder } from 'text-encoding' -import { createWalletClient, custom, PrivateKeyAccount, toHex } from 'viem' +import { PrivateKeyAccount } from 'viem' import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { DecodedMessage } from 'xmtp-react-native-sdk/lib/DecodedMessage' -import { Test, assert, createClients, delayToPropogate } from './test-utils' +import { Test, assert, delayToPropogate } from './test-utils' import { Query, JSContentCodec, diff --git a/src/lib/Conversations.ts b/src/lib/Conversations.ts index 7821d190..c274fdf8 100644 --- a/src/lib/Conversations.ts +++ b/src/lib/Conversations.ts @@ -10,11 +10,11 @@ import { DecodedMessage } from './DecodedMessage' import { Group } from './Group' import { CreateGroupOptions } from './types/CreateGroupOptions' import { EventTypes } from './types/EventTypes' +import { PermissionPolicySet } from './types/PermissionPolicySet' import { ConversationContext } from '../XMTP.types' import * as XMTPModule from '../index' import { ContentCodec } from '../index' import { getAddress } from '../utils/address' -import { PermissionPolicySet } from './types/PermissionPolicySet' export default class Conversations< ContentTypes extends ContentCodec[] = [], @@ -189,7 +189,7 @@ export default class Conversations< ) } - /** + /** * Creates a new group with custom permissions. * * This method creates a new group with the specified peer addresses and options. @@ -199,7 +199,7 @@ export default class Conversations< * @param {CreateGroupOptions} opts - The options to use for the group. * @returns {Promise>} A Promise that resolves to a Group object. */ - async newGroupCustomPermissions( + async newGroupCustomPermissions( peerAddresses: string[], permissionPolicySet: PermissionPolicySet, opts?: CreateGroupOptions | undefined From 7e46566ea68e910e9dcd0c707a26b05753f3fb72 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Wed, 24 Jul 2024 14:38:34 -0700 Subject: [PATCH 3/3] feat: added ability to set custom permissions on group creation --- example/ios/Podfile.lock | 8 +- example/src/tests/groupPermissionsTests.ts | 30 ++--- ios/Wrappers/PermissionPolicySetWrapper.swift | 110 ++++++++++++------ ios/Wrappers/Wrapper.swift | 1 + ios/XMTPModule.swift | 48 ++++---- src/lib/types/PermissionPolicySet.ts | 2 +- 6 files changed, 118 insertions(+), 81 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8fa3073f..f96e127e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -449,7 +449,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.13.9): + - XMTP (0.13.10): - Connect-Swift (= 0.12.0) - GzipSwift - LibXMTP (= 0.5.6-beta1) @@ -458,7 +458,7 @@ PODS: - ExpoModulesCore - MessagePacker - secp256k1.swift - - XMTP (= 0.13.9) + - XMTP (= 0.13.10) - Yoga (1.14.0) DEPENDENCIES: @@ -763,8 +763,8 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: 407a385e97fd206c4fbe880cc84123989167e0d1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 518a21ff9d2b7235dbf8d79fdc388a576c94f1e2 - XMTPReactNative: e3803ae32fa0dd849c2265b8d9109f682840ee24 + XMTP: 19f9c073262c44fbe98489208cda7a44d079064d + XMTPReactNative: 296aaa356ea5c67c98779665bcb5e1cad140d135 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 22463a9f..bbdc4e81 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -59,9 +59,7 @@ test('super admin can add a new admin', async () => { const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.addAdmin(caro.inboxId) - throw new Error( - 'Expected exception when non-super admin attempts to add an admin.' - ) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -141,6 +139,7 @@ test('in admin only group, members can update group name once they are an admin' const boGroup = (await bo.conversations.listGroups())[0] try { await boGroup.updateGroupName("bo's group") + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -216,6 +215,7 @@ test('in admin only group, members can not update group name after admin status // Bo can no longer update the group name try { await boGroup.updateGroupName('new name 2') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected error @@ -258,6 +258,7 @@ test('can not remove a super admin from a group', async () => { // Bo should not be able to remove alix from the group try { await boGroup.removeMembersByInboxId([alix.inboxId]) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -284,6 +285,7 @@ test('can not remove a super admin from a group', async () => { // Verify bo can not remove alix bc alix is a super admin try { await boGroup.removeMembersByInboxId([alix.inboxId]) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -335,6 +337,7 @@ test('can commit after invalid permissions commit', async () => { ) try { await alixGroup.addAdmin(alix.inboxId) + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -376,7 +379,7 @@ test('group with All Members policy has remove function that is admin only', asy // Verify that Alix cannot remove a member try { await alixGroup.removeMembers([caro.address]) - assert(false, 'Alix should not be able to remove a member') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -429,8 +432,8 @@ test('can update group permissions', async () => { await alix.conversations.syncGroups() const alixGroup = (await alix.conversations.listGroups())[0] try { - await alixGroup.updateGroupDescription('new description') - assert(false, 'Alix should not be able to update the group description') + await alixGroup.updateGroupDescription('new description 2') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -439,7 +442,7 @@ test('can update group permissions', async () => { // Verify that alix can not update permissions try { await alixGroup.updateGroupDescriptionPermission('allow') - assert(false, 'Alix should not be able to update the group name permission') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -480,7 +483,7 @@ test('can update group pinned frame', async () => { const alixGroup = (await alix.conversations.listGroups())[0] try { await alixGroup.updateGroupPinnedFrameUrl('new pinned frame') - assert(false, 'Alix should not be able to update the group pinned frame') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -582,7 +585,7 @@ test('can create a group with custom permissions', async () => { // Verify that bo can not update the pinned frame even though they are a super admin try { await boGroup.updateGroupPinnedFrameUrl('new pinned frame') - assert(false, 'Bo should not be able to update the group pinned frame') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -599,7 +602,7 @@ test('can create a group with custom permissions', async () => { // Verify that alix can not update the group name try { await alixGroup.updateGroupName('new name') - assert(false, 'Alix should not be able to update the group name') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected @@ -612,7 +615,7 @@ test('creating a group with invalid permissions should fail', async () => { // Create clients const [alix, bo, caro] = await createClients(3) - // Add/Remove admin must be admin or super admin + // Add/Remove admin can not be set to allow const customPermissionsPolicySet: PermissionPolicySet = { addMemberPolicy: 'allow', removeMemberPolicy: 'deny', @@ -630,10 +633,11 @@ test('creating a group with invalid permissions should fail', async () => { [alix.address, caro.address], customPermissionsPolicySet ) - assert(false, 'Group creation should fail') + return false // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // expected + console.log('error', error) + return true } - return true }) diff --git a/ios/Wrappers/PermissionPolicySetWrapper.swift b/ios/Wrappers/PermissionPolicySetWrapper.swift index bac7d584..f3d81cd7 100644 --- a/ios/Wrappers/PermissionPolicySetWrapper.swift +++ b/ios/Wrappers/PermissionPolicySetWrapper.swift @@ -9,41 +9,79 @@ import Foundation import XMTP class PermissionPolicySetWrapper { - static func fromPermissionOption(_ permissionOption: XMTP.PermissionOption) -> String { - switch permissionOption { - case .allow: - return "allow" - case .deny: - return "deny" - case .admin: - return "admin" - case .superAdmin: - return "superAdmin" - case .unknown: - return "unknown" - } - } - - static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { + static func fromPermissionOption(_ permissionOption: XMTP.PermissionOption) -> String { + switch permissionOption { + case .allow: + return "allow" + case .deny: + return "deny" + case .admin: + return "admin" + case .superAdmin: + return "superAdmin" + case .unknown: + return "unknown" + } + } + + static func createPermissionOption(from string: String) -> PermissionOption { + switch string { + case "allow": + return .allow + case "deny": + return .deny + case "admin": + return .admin + case "superAdmin": + return .superAdmin + default: + return .unknown + } + } + + + static func encodeToObj(_ policySet: XMTP.PermissionPolicySet) -> [String: Any] { - return [ - "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), - "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), - "addAdminPolicy": fromPermissionOption(policySet.addAdminPolicy), - "removeAdminPolicy": fromPermissionOption(policySet.removeAdminPolicy), - "updateGroupNamePolicy": fromPermissionOption(policySet.updateGroupNamePolicy), - "updateGroupDescriptionPolicy": fromPermissionOption(policySet.updateGroupDescriptionPolicy), - "updateGroupImagePolicy": fromPermissionOption(policySet.updateGroupImagePolicy), - "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy) - ] - } - - static func encodeToJsonString(_ policySet: XMTP.PermissionPolicySet) throws -> String { - let obj = encodeToObj(policySet) - let data = try JSONSerialization.data(withJSONObject: obj) - guard let result = String(data: data, encoding: .utf8) else { - throw WrapperError.encodeError("could not encode permission policy") - } - return result - } + return [ + "addMemberPolicy": fromPermissionOption(policySet.addMemberPolicy), + "removeMemberPolicy": fromPermissionOption(policySet.removeMemberPolicy), + "addAdminPolicy": fromPermissionOption(policySet.addAdminPolicy), + "removeAdminPolicy": fromPermissionOption(policySet.removeAdminPolicy), + "updateGroupNamePolicy": fromPermissionOption(policySet.updateGroupNamePolicy), + "updateGroupDescriptionPolicy": fromPermissionOption(policySet.updateGroupDescriptionPolicy), + "updateGroupImagePolicy": fromPermissionOption(policySet.updateGroupImagePolicy), + "updateGroupPinnedFrameUrlPolicy": fromPermissionOption(policySet.updateGroupPinnedFrameUrlPolicy) + ] + } + public static func createPermissionPolicySet(from json: String) throws -> PermissionPolicySet { + guard let data = json.data(using: .utf8) else { + throw WrapperError.decodeError("Failed to convert PermissionPolicySet JSON string to data") + } + + guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let jsonDict = jsonObject as? [String: Any] else { + throw WrapperError.decodeError("Failed to parse PermissionPolicySet JSON data") + } + + return PermissionPolicySet( + addMemberPolicy: createPermissionOption(from: jsonDict["addMemberPolicy"] as? String ?? ""), + removeMemberPolicy: createPermissionOption(from: jsonDict["removeMemberPolicy"] as? String ?? ""), + addAdminPolicy: createPermissionOption(from: jsonDict["addAdminPolicy"] as? String ?? ""), + removeAdminPolicy: createPermissionOption(from: jsonDict["removeAdminPolicy"] as? String ?? ""), + updateGroupNamePolicy: createPermissionOption(from: jsonDict["updateGroupNamePolicy"] as? String ?? ""), + updateGroupDescriptionPolicy: createPermissionOption(from: jsonDict["updateGroupDescriptionPolicy"] as? String ?? ""), + updateGroupImagePolicy: createPermissionOption(from: jsonDict["updateGroupImagePolicy"] as? String ?? ""), + updateGroupPinnedFrameUrlPolicy: createPermissionOption(from: jsonDict["updateGroupPinnedFrameUrlPolicy"] as? String ?? "") + ) + } + + static func encodeToJsonString(_ policySet: XMTP.PermissionPolicySet) throws -> String { + let obj = encodeToObj(policySet) + let data = try JSONSerialization.data(withJSONObject: obj) + guard let result = String(data: data, encoding: .utf8) else { + throw WrapperError.encodeError("could not encode permission policy") + } + return result + } + } diff --git a/ios/Wrappers/Wrapper.swift b/ios/Wrappers/Wrapper.swift index 87b6e60b..cc5a8249 100644 --- a/ios/Wrappers/Wrapper.swift +++ b/ios/Wrappers/Wrapper.swift @@ -8,6 +8,7 @@ import Foundation enum WrapperError: Swift.Error { case encodeError(String) + case decodeError(String) } protocol Wrapper: Codable { diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 8ad214e1..a34af7ff 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -745,26 +745,27 @@ public class XMTPModule: Module { } } -// AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionJson: String, groupOptionsJson: String) -> String in -// guard let client = await clientsManager.getClient(key: inboxId) else { -// throw Error.noClient -// } -// do { -// let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) -// let group = try await client.conversations.newGroupCustomP( -// with: peerAddresses, -// permissions: permissionLevel, -// name: createGroupParams.groupName, -// imageUrlSquare: createGroupParams.groupImageUrlSquare, -// description: createGroupParams.groupDescription, -// pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl -// ) -// return try GroupWrapper.encode(group, client: client) -// } catch { -// print("ERRRO!: \(error.localizedDescription)") -// throw error -// } -// } + AsyncFunction("createGroupCustomPermissions") { (inboxId: String, peerAddresses: [String], permissionPolicySetJson: String, groupOptionsJson: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + do { + let createGroupParams = CreateGroupParamsWrapper.createGroupParamsFromJson(groupOptionsJson) + let permissionPolicySet = try PermissionPolicySetWrapper.createPermissionPolicySet(from: permissionPolicySetJson) + let group = try await client.conversations.newGroupCustomPermissions( + with: peerAddresses, + permissionPolicySet: permissionPolicySet, + name: createGroupParams.groupName, + imageUrlSquare: createGroupParams.groupImageUrlSquare, + description: createGroupParams.groupDescription, + pinnedFrameUrl: createGroupParams.groupPinnedFrameUrl + ) + return try GroupWrapper.encode(group, client: client) + } catch { + print("ERRRO!: \(error.localizedDescription)") + throw error + } + } AsyncFunction("listMemberInboxIds") { (inboxId: String, groupId: String) -> [String] in guard let client = await clientsManager.getClient(key: inboxId) else { @@ -1452,13 +1453,6 @@ public class XMTPModule: Module { throw Error.invalidPermissionOption } } - -// func createPermissionPolicySetFromJSON(permissionPolicySetJson: String) -> PermissionPolicySet { -// let data = permissionPolicySetJson.data(using: .utf8) ?? Data() -// let jsonObj = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? [:] -// -// return PermissionPolicySet -// } func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil, enableV3: Bool = false, dbEncryptionKey: Data? = nil, dbDirectory: String? = nil, historySyncUrl: String? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. diff --git a/src/lib/types/PermissionPolicySet.ts b/src/lib/types/PermissionPolicySet.ts index 64bc4974..55811c80 100644 --- a/src/lib/types/PermissionPolicySet.ts +++ b/src/lib/types/PermissionPolicySet.ts @@ -5,7 +5,7 @@ export type PermissionOption = | 'superAdmin' // Only the super admin of the group can perform this action | 'unknown' -// Add Admin and Remove admin must be set to either 'admin' or 'superAdmin' to be valid +// Add Admin and Remove admin must be set to either 'admin', 'superAdmin' or 'deny' to be valid export type PermissionPolicySet = { addMemberPolicy: PermissionOption removeMemberPolicy: PermissionOption