From 3c6b74124d288f7f8c2746348b5914227f091574 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Tue, 2 Jul 2024 10:08:48 -0700 Subject: [PATCH] added update permissions functions and updated tests --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 16 +++ example/src/tests/groupPermissionsTests.ts | 95 +++++++++++-- example/src/tests/groupTests.ts | 4 +- ios/XMTPModule.swift | 118 +++++++++++++++-- src/index.ts | 117 ++++++++++++++-- src/lib/Group.ts | 125 +++++++++++++++++- 6 files changed, 439 insertions(+), 36 deletions(-) diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 4a8ec3b69..3055e5af9 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -25,6 +25,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper +import expo.modules.xmtpreactnativesdk.wrappers.PermissionPolicySetWrapper import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -1165,6 +1166,21 @@ class XMTPModule : Module() { } } + AsyncFunction("permissionPolicySet") Coroutine { inboxId: String, id: String -> + withContext(Dispatchers.IO) { + logV("groupImageUrlSquare") + val client = clients[inboxId] ?: throw XMTPException("No client") + val group = findGroup(inboxId, id) + + val permissionPolicySet = group?.permissionPolicySet() + if (permissionPolicySet != null) { + PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) + } else { + throw XMTPException("Permission policy set not found for group: $id") + } + } + } + AsyncFunction("processGroupMessage") Coroutine { inboxId: String, id: String, encryptedMessage: String -> withContext(Dispatchers.IO) { logV("processGroupMessage") diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 38abed82b..bd6ff470b 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -21,12 +21,8 @@ test('new group has expected admin list and super admin list', async () => { const superAdminList = await alixGroup.listSuperAdmins() assert( - adminList.length === 1, - `adminList.length should be 1 but was ${adminList.length}` - ) - assert( - adminList[0] === alix.inboxId, - `adminList[0] should be ${alix.address} but was ${adminList[0]}` + adminList.length === 0, + `adminList.length should be 0 but was ${adminList.length}` ) assert( superAdminList.length === 1, @@ -88,9 +84,9 @@ test('in admin only group, members can not update group name unless they are an { permissionLevel: 'admin_only' } ) - if (alixGroup.permissionLevel !== 'admin_only') { + if ((await alixGroup.permissionPolicySet()).addMemberPolicy !== 'admin') { throw Error( - `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + `Group add member policy should be admin but was ${(await alixGroup.permissionPolicySet()).addMemberPolicy}` ) } @@ -123,9 +119,11 @@ test('in admin only group, members can update group name once they are an admin' { permissionLevel: 'admin_only' } ) - if (alixGroup.permissionLevel !== 'admin_only') { + if ( + (await alixGroup.permissionPolicySet()).updateGroupNamePolicy !== 'admin' + ) { throw Error( - `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + `Group update name policy should be admin but was ${(await alixGroup.permissionPolicySet()).updateGroupNamePolicy}` ) } @@ -174,9 +172,11 @@ test('in admin only group, members can not update group name after admin status { permissionLevel: 'admin_only' } ) - if (alixGroup.permissionLevel !== 'admin_only') { + if ( + (await alixGroup.permissionPolicySet()).updateGroupNamePolicy !== 'admin' + ) { throw Error( - `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + `Group update name policy should be admin but was ${(await alixGroup.permissionPolicySet()).updateGroupNamePolicy}` ) } @@ -392,3 +392,74 @@ test('group with All Members policy has remove function that is admin only', asy return true }) + +test('can update group permissions', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Bo creates a group with Alix and Caro + const boGroup = await bo.conversations.newGroup( + [alix.address, caro.address], + { permissionLevel: 'admin_only' } + ) + + // Verify that bo is a super admin + assert( + (await boGroup.isSuperAdmin(bo.inboxId)) === true, + `bo should be a super admin` + ) + + // Verify that group has the expected group description permission + assert( + (await boGroup.permissionPolicySet()).updateGroupDescriptionPolicy === + 'admin', + `boGroup.permissionPolicySet.updateGroupDescriptionPolicy should be admin but was ${(await boGroup.permissionPolicySet()).updateGroupDescriptionPolicy}` + ) + + // Verify that Bo can update the group description + await boGroup.updateGroupDescription('new description') + await boGroup.sync() + assert( + (await boGroup.groupDescription()) === 'new description', + `boGroup.groupDescription should be "new description" but was ${boGroup.groupDescription}` + ) + + // Verify that alix can not update the group description + 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') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // 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') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // Verify that bo can update permissions + await boGroup.updateGroupDescriptionPermission('allow') + await boGroup.sync() + assert( + (await boGroup.permissionPolicySet()).updateGroupDescriptionPolicy === + 'allow', + `boGroup.permissionPolicySet.updateGroupDescriptionPolicy should be allow but was ${(await boGroup.permissionPolicySet()).updateGroupDescriptionPolicy}` + ) + + // Verify that alix can now update the group description + await alixGroup.updateGroupDescription('new description 2') + await alixGroup.sync() + assert( + (await alixGroup.groupDescription()) === 'new description 2', + `alixGroup.groupDescription should be "new description 2" but was ${alixGroup.groupDescription}` + ) + + return true +}) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 5aa2a2ad1..4a6dfbfa0 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -1137,9 +1137,9 @@ test('can make a group with admin permissions', async () => { { permissionLevel: 'admin_only' } ) - if (group.permissionPolicySet.addMemberPolicy !== 'admin') { + if ((await group.permissionPolicySet()).addMemberPolicy !== 'admin') { throw Error( - `Group permission level should be admin but was ${group.permissionPolicySet.addMemberPolicy}` + `Group permission level should be admin but was ${(await group.permissionPolicySet()).addMemberPolicy}` ) } diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 28ee6b12d..18c367819 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -59,7 +59,7 @@ public class XMTPModule: Module { } enum Error: Swift.Error { - case noClient, conversationNotFound(String), noMessage, invalidKeyBundle, invalidDigest, badPreparation(String), mlsNotEnabled(String), invalidString + case noClient, conversationNotFound(String), noMessage, invalidKeyBundle, invalidDigest, badPreparation(String), mlsNotEnabled(String), invalidString, invalidPermissionOption } public func definition() -> ModuleDefinition { @@ -987,13 +987,100 @@ public class XMTPModule: Module { } try await group.removeSuperAdmin(inboxId: inboxId) } - - AsyncFunction("processGroupMessage") { (inboxId: String, id: String, encryptedMessage: String) -> String in - guard let client = await clientsManager.getClient(key: inboxId) else { - throw Error.noClient - } - - guard let group = try await findGroup(inboxId: inboxId, id: id) else { + + AsyncFunction("updateAddMemberPermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateAddMemberPermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateRemoveMemberPermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateRemoveMemberPermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateAddAdminPermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateAddAdminPermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateRemoveAdminPermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateRemoveAdminPermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateGroupNamePermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateGroupNamePermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateGroupImageUrlSquarePermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateGroupImageUrlSquarePermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("updateGroupDescriptionPermission") { (clientInboxId: String, id: String, newPermission: String) in + guard let client = await clientsManager.getClient(key: clientInboxId) else { + throw Error.noClient + } + guard let group = try await findGroup(inboxId: clientInboxId, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.updateGroupDescriptionPermission(newPermissionOption: getPermissionOption(permission: newPermission)) + } + + AsyncFunction("permissionPolicySet") { (inboxId: String, id: String) async throws -> String in + + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + guard let group = try await findGroup(inboxId: inboxId, id: id) else { + throw Error.conversationNotFound("Permission policy set not found for group: \(id)") + } + + let permissionPolicySet = try group.permissionPolicySet() + + return try PermissionPolicySetWrapper.encodeToJsonString(permissionPolicySet) + } + + + + AsyncFunction("processGroupMessage") { (inboxId: String, id: String, encryptedMessage: String) -> String in + guard let client = await clientsManager.getClient(key: inboxId) else { + throw Error.noClient + } + + guard let group = try await findGroup(inboxId: inboxId, id: id) else { throw Error.conversationNotFound("no group found for \(id)") } @@ -1275,6 +1362,21 @@ public class XMTPModule: Module { // // Helpers // + + private func getPermissionOption(permission: String) async throws -> PermissionOption { + switch permission { + case "allow": + return .allow + case "deny": + return .deny + case "admin": + return .admin + case "super_admin": + return .superAdmin + default: + throw Error.invalidPermissionOption + } + } 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/index.ts b/src/index.ts index 2c1052d22..97f21dd15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,11 +18,12 @@ import { ConversationVersion, } from './lib/ConversationContainer' import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage' -import { Group } from './lib/Group' +import { Group, PermissionUpdateOption } from './lib/Group' import { Member } from './lib/Member' import type { Query } from './lib/Query' import { ConversationSendPayload } from './lib/types' import { DefaultContentTypes } from './lib/types/DefaultContentType' +import { PermissionPolicySet } from './lib/types/PermissionPolicySet' import { getAddress } from './utils/address' export * from './context' @@ -170,17 +171,19 @@ export async function createGroup< imageUrlSquare: string = '', description: string = '' ): Promise> { - const groupString = await XMTPModule.createGroup( - client.inboxId, - peerAddresses, - permissionLevel, - name, - imageUrlSquare, - description + return new Group( + client, + JSON.parse( + await XMTPModule.createGroup( + client.inboxId, + peerAddresses, + permissionLevel, + name, + imageUrlSquare, + description + ) + ) ) - const groupObj = JSON.parse(groupString) - groupObj.permissionPolicySet = JSON.parse(groupObj.permissionPolicySet) - return new Group(client, groupObj) } export async function listGroups< @@ -877,6 +880,98 @@ export async function removeSuperAdmin( return XMTPModule.removeSuperAdmin(clientInboxId, id, inboxId) } +export async function updateAddMemberPermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateAddMemberPermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateRemoveMemberPermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateRemoveMemberPermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateAddAdminPermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateAddAdminPermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateRemoveAdminPermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateRemoveAdminPermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateGroupNamePermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateGroupNamePermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateGroupImageUrlSquarePermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateGroupImageUrlSquarePermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function updateGroupDescriptionPermission( + clientInboxId: string, + id: string, + permissionOption: PermissionUpdateOption +): Promise { + return XMTPModule.updateGroupDescriptionPermission( + clientInboxId, + id, + permissionOption + ) +} + +export async function permissionPolicySet( + clientInboxId: string, + id: string +): Promise { + const json = await XMTPModule.permissionPolicySet(clientInboxId, id) + return JSON.parse(json) +} + export async function allowGroups( inboxId: string, groupIds: string[] diff --git a/src/lib/Group.ts b/src/lib/Group.ts index c915e728a..8174e258a 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -13,6 +13,8 @@ import { PermissionPolicySet } from './types/PermissionPolicySet' import { SendOptions } from './types/SendOptions' import * as XMTP from '../index' +export type PermissionUpdateOption = 'allow' | 'deny' | 'admin' | 'super_admin' + export class Group< ContentTypes extends DefaultContentTypes = DefaultContentTypes, > implements ConversationContainer @@ -24,7 +26,6 @@ export class Group< version = ConversationVersion.GROUP topic: string creatorInboxId: InboxId - permissionPolicySet: PermissionPolicySet name: string isGroupActive: boolean imageUrlSquare: string @@ -36,7 +37,6 @@ export class Group< createdAt: number peerInboxIds: InboxId[] creatorInboxId: InboxId - permissionPolicySet: PermissionPolicySet topic: string name: string isGroupActive: boolean @@ -49,7 +49,6 @@ export class Group< this.peerInboxIds = params.peerInboxIds this.topic = params.topic this.creatorInboxId = params.creatorInboxId - this.permissionPolicySet = params.permissionPolicySet this.name = params.name this.isGroupActive = params.isGroupActive this.imageUrlSquare = params.imageUrlSquare @@ -384,6 +383,126 @@ export class Group< return XMTP.removeSuperAdmin(this.client.inboxId, this.id, inboxId) } + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the addMember permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateAddMemberPermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateAddMemberPermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the removeMember permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateRemoveMemberPermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateRemoveMemberPermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the addAdmin permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateAddAdminPermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateAddAdminPermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the removeAdmin permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateRemoveAdminPermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateRemoveAdminPermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the groupName permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateGroupNamePermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateGroupNamePermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the groupImageUrlSquare permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateGroupImageUrlSquarePermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateGroupImageUrlSquarePermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @param {PermissionOption} permissionOption + * @returns {Promise} A Promise that resolves when the groupDescription permission is updated for the group. + * Will throw if the user does not have the required permissions. + */ + async updateGroupDescriptionPermission( + permissionOption: PermissionUpdateOption + ): Promise { + return XMTP.updateGroupDescriptionPermission( + this.client.inboxId, + this.id, + permissionOption + ) + } + + /** + * + * @returns {Promise} A {PermissionPolicySet} object representing the group's permission policy set. + */ + async permissionPolicySet(): Promise { + return XMTP.permissionPolicySet(this.client.inboxId, this.id) + } + async processMessage( encryptedMessage: string ): Promise> {