From d41603ddb86920be1f535e875a2b00c6ddf3a47a Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 30 May 2024 13:59:51 -0700 Subject: [PATCH 1/4] Added new admin and super admin functions --- .../modules/xmtpreactnativesdk/XMTPModule.kt | 84 ++++++- example/src/TestScreen.tsx | 7 + example/src/tests/groupPermissionsTests.ts | 231 ++++++++++++++++++ example/src/tests/groupTests.ts | 21 +- src/index.ts | 66 ++++- src/lib/Client.ts | 10 +- src/lib/Group.ts | 49 +++- update_test_numbers.sh | 37 +++ 8 files changed, 481 insertions(+), 24 deletions(-) create mode 100644 example/src/tests/groupPermissionsTests.ts create mode 100755 update_test_numbers.sh diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 60e6d3ccd..91affb17b 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -901,13 +901,93 @@ class XMTPModule : Module() { } } - AsyncFunction("isGroupAdmin") Coroutine { clientAddress: String, id: String -> + AsyncFunction("creatorInboxId") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("creatorInboxId") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.creatorInboxId() + } + } + + AsyncFunction("isAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> withContext(Dispatchers.IO) { logV("isGroupAdmin") val client = clients[clientAddress] ?: throw XMTPException("No client") val group = findGroup(clientAddress, id) - group?.isAdmin(client.inboxId) + group?.isAdmin(inboxId) + } + } + + AsyncFunction("isSuperAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> + withContext(Dispatchers.IO) { + logV("isSuperAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.isSuperAdmin(inboxId) + } + } + + AsyncFunction("listAdmins") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("listAdmins") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.listAdmins() + } + } + + AsyncFunction("listSuperAdmins") Coroutine { clientAddress: String, id: String -> + withContext(Dispatchers.IO) { + logV("listSuperAdmins") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.listSuperAdmins() + } + } + + AsyncFunction("addAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> + withContext(Dispatchers.IO) { + logV("addAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.addAdmin(inboxId) + } + } + + AsyncFunction("addSuperAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> + withContext(Dispatchers.IO) { + logV("addSuperAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.addSuperAdmin(inboxId) + } + } + + AsyncFunction("removeAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> + withContext(Dispatchers.IO) { + logV("removeAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.removeAdmin(inboxId) + } + } + + AsyncFunction("removeSuperAdmin") Coroutine { clientAddress: String, id: String, inboxId: String -> + withContext(Dispatchers.IO) { + logV("removeSuperAdmin") + val client = clients[clientAddress] ?: throw XMTPException("No client") + val group = findGroup(clientAddress, id) + + group?.removeSuperAdmin(inboxId) } } diff --git a/example/src/TestScreen.tsx b/example/src/TestScreen.tsx index 41c59131b..1d6b847c6 100644 --- a/example/src/TestScreen.tsx +++ b/example/src/TestScreen.tsx @@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react' import { View, Text, Button, ScrollView } from 'react-native' import { createdAtTests } from './tests/createdAtTests' +import { groupPermissionsTests } from './tests/groupPermissionsTests' import { groupTests } from './tests/groupTests' import { restartStreamTests } from './tests/restartStreamsTests' import { Test } from './tests/test-utils' @@ -108,6 +109,7 @@ export enum TestCategory { group = 'group', createdAt = 'createdAt', restartStreans = 'restartStreams', + groupPermissions = 'groupPermissions', } export default function TestScreen(): JSX.Element { @@ -121,6 +123,7 @@ export default function TestScreen(): JSX.Element { ...groupTests, ...createdAtTests, ...restartStreamTests, + ...groupPermissionsTests, ] let activeTests, title switch (params.testSelection) { @@ -144,6 +147,10 @@ export default function TestScreen(): JSX.Element { activeTests = restartStreamTests title = 'Restart Streams Unit Tests' break + case TestCategory.groupPermissions: + activeTests = groupPermissionsTests + title = 'Group Permissions Unit Tests' + break } return ( diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts new file mode 100644 index 000000000..8ac250c0a --- /dev/null +++ b/example/src/tests/groupPermissionsTests.ts @@ -0,0 +1,231 @@ +import { Test, assert, createClients } from './test-utils' + +export const groupPermissionsTests: Test[] = [] +let counter = 1 +function test(name: string, perform: () => Promise) { + groupPermissionsTests.push({ + name: String(counter++) + '. ' + name, + run: perform, + }) +} + +test('new group has expected admin list and super admin list', async () => { + // Create clients + const [alix, bo] = await createClients(2) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup([bo.address]) + + // Alix is the only admin and the only super admin + const adminList = await alixGroup.listAdmins() + 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]}` + ) + assert( + superAdminList.length === 1, + `superAdminList.length should be 1 but was ${superAdminList.length}` + ) + assert( + superAdminList[0] === alix.inboxId, + `superAdminList[0] should be ${alix.address} but was ${superAdminList[0]}` + ) + return true +}) + +test('super admin can add a new admin', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup([ + bo.address, + caro.address, + ]) + + // Verify alix is a super admin and bo is not + const alixIsSuperAdmin = await alixGroup.isSuperAdmin(alix.inboxId) + const boIsSuperAdmin = await alixGroup.isSuperAdmin(bo.inboxId) + + assert(alixIsSuperAdmin, `alix should be a super admin`) + assert(!boIsSuperAdmin, `bo should not be a super admin`) + + // Verify that bo can not add a new admin + await bo.conversations.syncGroups() + 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.' + ) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // Alix adds bo as an admin + await alixGroup.addAdmin(bo.inboxId) + await alix.conversations.syncGroups() + const alixGroupIsAdmin = await alixGroup.isAdmin(bo.inboxId) + assert(alixGroupIsAdmin, `alix should be an admin`) + + return true +}) + +test('in admin only group, members can not update group name unless they are an admin', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup( + [bo.address, caro.address], + 'admin_only' + ) + + if (alixGroup.permissionLevel !== 'admin_only') { + throw Error( + `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + ) + } + + // Verify group name is New Group + const groupName = await alixGroup.groupName() + assert( + groupName === 'New Group', + `group name should be New Group but was ${groupName}` + ) + + // Verify that bo can not update the group name + await bo.conversations.syncGroups() + const boGroup = (await bo.conversations.listGroups())[0] + try { + await boGroup.updateGroupName("bo's group") + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + return true + } + return false +}) + +test('in admin only group, members can update group name once they are an admin', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup( + [bo.address, caro.address], + 'admin_only' + ) + + if (alixGroup.permissionLevel !== 'admin_only') { + throw Error( + `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + ) + } + + // Verify group name is New Group + let groupName = await alixGroup.groupName() + assert( + groupName === 'New Group', + `group name should be New Group but was ${groupName}` + ) + + // Verify that bo can not update the group name + await bo.conversations.syncGroups() + const boGroup = (await bo.conversations.listGroups())[0] + try { + await boGroup.updateGroupName("bo's group") + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + // Alix adds bo as an admin + await alixGroup.addAdmin(bo.inboxId) + await alix.conversations.syncGroups() + const alixGroupIsAdmin = await alixGroup.isAdmin(bo.inboxId) + assert(alixGroupIsAdmin, `alix should be an admin`) + + // Now bo can update the group name + await boGroup.sync() + await boGroup.updateGroupName("bo's group") + groupName = await boGroup.groupName() + assert( + groupName === "bo's group", + `group name should be bo's group but was ${groupName}` + ) + + return true +}) + +test('in admin only group, members can not update group name after admin status is removed', async () => { + // Create clients + const [alix, bo, caro] = await createClients(3) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup( + [bo.address, caro.address], + 'admin_only' + ) + + if (alixGroup.permissionLevel !== 'admin_only') { + throw Error( + `Group permission level should be admin_only but was ${alixGroup.permissionLevel}` + ) + } + + // Verify group name is New Group + let groupName = await alixGroup.groupName() + assert( + groupName === 'New Group', + `group name should be New Group but was ${groupName}` + ) + + // Alix adds bo as an admin + await alixGroup.addAdmin(bo.inboxId) + await alix.conversations.syncGroups() + let boIsAdmin = await alixGroup.isAdmin(bo.inboxId) + assert(boIsAdmin, `bo should be an admin`) + + // Now bo can update the group name + await bo.conversations.syncGroups() + const boGroup = (await bo.conversations.listGroups())[0] + await boGroup.sync() + await boGroup.updateGroupName("bo's group") + await alixGroup.sync() + groupName = await alixGroup.groupName() + assert( + groupName === "bo's group", + `group name should be bo's group but was ${groupName}` + ) + + // Now alix removed bo as an admin + await alixGroup.removeAdmin(bo.inboxId) + await alix.conversations.syncGroups() + boIsAdmin = await alixGroup.isAdmin(bo.inboxId) + assert(!boIsAdmin, `bo should not be an admin`) + + // Bo can no longer update the group name + try { + await boGroup.updateGroupName('new name 2') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected error + } + + await alixGroup.sync() + groupName = await alixGroup.groupName() + assert( + groupName === "bo's group", + `group name should be bo's group but was ${groupName}` + ) + + // throw new Error('Expected exception when non-admin attempts to update group name.') + return true +}) diff --git a/example/src/tests/groupTests.ts b/example/src/tests/groupTests.ts index 1756c733f..b3a0e28df 100644 --- a/example/src/tests/groupTests.ts +++ b/example/src/tests/groupTests.ts @@ -17,9 +17,9 @@ import { } from '../../../src/index' export const groupTests: Test[] = [] - +let counter = 1 function test(name: string, perform: () => Promise) { - groupTests.push({ name, run: perform }) + groupTests.push({ name: String(counter++) + '. ' + name, run: perform }) } test('can make a MLS V3 client', async () => { @@ -89,9 +89,7 @@ test('can drop a local database', async () => { await group.send('hi') return true } - throw new Error( - 'should throw when local database not connected' - ) + throw new Error('should throw when local database not connected') }) test('can make a MLS V3 client from bundle', async () => { @@ -886,16 +884,17 @@ test('can make a group with admin permissions', async () => { ) } - const isAdmin = await group.isAdmin() + const isAdmin = await group.isAdmin(adminClient.inboxId) if (!isAdmin) { throw Error(`adminClient should be the admin`) } - if (group.creatorInboxId !== adminClient.inboxId) { - throw Error( - `adminClient should be the admin but was ${group.creatorInboxId}` - ) - } + // Creator id not working, see https://github.com/xmtp/libxmtp/issues/788 + // if (group.creatorInboxId !== adminClient.inboxId) { + // throw Error( + // `adminClient should be the creator but was ${group.creatorInboxId}` + // ) + // } return true }) diff --git a/src/index.ts b/src/index.ts index 96ad52471..ae78a91bd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -725,11 +725,73 @@ export async function addedByInboxId( return XMTPModule.addedByInboxId(clientAddress, id) } -export async function isGroupAdmin( +export async function creatorInboxId( clientAddress: string, id: string +): Promise { + return XMTPModule.creatorInboxId(clientAddress, id) +} + +export async function isAdmin( + clientAddress: string, + id: string, + inboxId: string +): Promise { + return XMTPModule.isAdmin(clientAddress, id, inboxId) +} + +export async function isSuperAdmin( + clientAddress: string, + id: string, + inboxId: string ): Promise { - return XMTPModule.isGroupAdmin(clientAddress, id) + return XMTPModule.isSuperAdmin(clientAddress, id, inboxId) +} + +export async function listAdmins( + clientAddress: string, + id: string +): Promise { + return XMTPModule.listAdmins(clientAddress, id) +} + +export async function listSuperAdmins( + clientAddress: string, + id: string +): Promise { + return XMTPModule.listSuperAdmins(clientAddress, id) +} + +export async function addAdmin( + clientAddress: string, + id: string, + inboxId: string +): Promise { + return XMTPModule.addAdmin(clientAddress, id, inboxId) +} + +export async function addSuperAdmin( + clientAddress: string, + id: string, + inboxId: string +): Promise { + return XMTPModule.addSuperAdmin(clientAddress, id, inboxId) +} + +export async function removeAdmin( + clientAddress: string, + id: string, + inboxId: string +): Promise { + return XMTPModule.removeAdmin(clientAddress, id, inboxId) +} + +export async function removeSuperAdmin( + clientAddress: string, + id: string, + inboxId: string +): Promise { + return XMTPModule.removeSuperAdmin(clientAddress, id, inboxId) } export async function allowGroups( diff --git a/src/lib/Client.ts b/src/lib/Client.ts index 1143fd67e..e2675d6c2 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -19,13 +19,11 @@ import { DecodedMessage } from '../index' declare const Buffer -export type GetMessageContentTypeFromClient = C extends Client - ? T - : never +export type GetMessageContentTypeFromClient = + C extends Client ? T : never -export type ExtractDecodedType = C extends XMTPModule.ContentCodec - ? T - : never +export type ExtractDecodedType = + C extends XMTPModule.ContentCodec ? T : never export class Client< ContentTypes extends DefaultContentTypes = DefaultContentTypes, diff --git a/src/lib/Group.ts b/src/lib/Group.ts index 7c4c4f08b..ec0e2dd73 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -203,6 +203,8 @@ export class Group< return XMTP.groupName(this.client.address, this.id) } + // Updates the group name. + // Will throw if the user does not have the required permissions. async updateGroupName(groupName: string): Promise { return XMTP.updateGroupName(this.client.address, this.id, groupName) } @@ -219,10 +221,51 @@ export class Group< return XMTP.addedByInboxId(this.client.address, this.id) } - // Returns whether you are an admin of the group. + // Returns whether a given inboxId is an admin of the group. // To get the latest admin status from the network, call sync() first. - async isAdmin(): Promise { - return XMTP.isGroupAdmin(this.client.address, this.id) + async isAdmin(inboxId: string): Promise { + return XMTP.isAdmin(this.client.address, this.id, inboxId) + } + + // Returns whether a given inboxId is a super admin of the group. + // To get the latest super admin status from the network, call sync() first. + async isSuperAdmin(inboxId: string): Promise { + return XMTP.isSuperAdmin(this.client.address, this.id, inboxId) + } + + // Returns an array of inboxIds that are admins of the group. + // To get the latest admin list from the network, call sync() first. + async listAdmins(): Promise { + return XMTP.listAdmins(this.client.address, this.id) + } + + // Returns an array of inboxIds that are super admins of the group. + // To get the latest super admin list from the network, call sync() first. + async listSuperAdmins(): Promise { + return XMTP.listSuperAdmins(this.client.address, this.id) + } + + // Adds an inboxId to the group admins. + // Will throw if the user does not have the required permissions. + async addAdmin(inboxId: string): Promise { + return XMTP.addAdmin(this.client.address, this.id, inboxId) + } + + // Adds an inboxId to the group super admins. + // Will throw if the user does not have the required permissions. + async addSuperAdmin(inboxId: string): Promise { + return XMTP.addSuperAdmin(this.client.address, this.id, inboxId) + } + + // Removes an inboxId from the group admins. + // Will throw if the user does not have the required permissions. + async removeAdmin(inboxId: string): Promise { + return XMTP.removeAdmin(this.client.address, this.id, inboxId) + } + + // Remove Super Admin + async removeSuperAdmin(inboxId: string): Promise { + return XMTP.removeSuperAdmin(this.client.address, this.id, inboxId) } async processMessage( diff --git a/update_test_numbers.sh b/update_test_numbers.sh new file mode 100755 index 000000000..ebbe44eed --- /dev/null +++ b/update_test_numbers.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Path to the TypeScript test file +file_path="example/src/tests/groupTests.ts" + +# Temporary file to store intermediate results +temp_file=$(mktemp) + +# Counter for test numbers +test_number=1 + +# Process the file +awk -v test_num="$test_number" ' + # Match lines that start with "test(" and are function declarations + /^test\(.*\)/ { + # Check if the previous line is a test number comment + if (prev_line ~ /^\/\/ Test [0-9]+$/) { + # Replace the number in the existing comment + sub(/[0-9]+$/, test_num, prev_line) + print prev_line + } else { + # Print a new test number comment + print "// Test " test_num + } + test_num++ + } + # Store the current line in prev_line before printing + { prev_line = $0; if (!/^\/\/ Test [0-9]+$/) print } +' "$file_path" > "$temp_file" + +# Move the temporary file to the original file +mv "$temp_file" "$file_path" + +# Remove the temporary file if needed (optional, as mv already moved it) +rm -f "$temp_file" + +echo "Updated test numbers in $file_path" From 388c7c4363ec4a75eebe1ad5f767a87840c7aba6 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 30 May 2024 14:36:44 -0700 Subject: [PATCH 2/4] Added ios side and new test for super admin remove --- example/ios/Podfile.lock | 2 +- example/src/tests/groupPermissionsTests.ts | 87 ++++++++++++++++++++++ ios/XMTPModule.swift | 84 ++++++++++++++++++++- 3 files changed, 170 insertions(+), 3 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index cf4d1d6ee..64e25a2a5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -769,4 +769,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 95d6ace79946933ecf80684613842ee553dd76a2 -COCOAPODS: 1.14.2 +COCOAPODS: 1.15.2 diff --git a/example/src/tests/groupPermissionsTests.ts b/example/src/tests/groupPermissionsTests.ts index 8ac250c0a..9b2cc78b9 100644 --- a/example/src/tests/groupPermissionsTests.ts +++ b/example/src/tests/groupPermissionsTests.ts @@ -229,3 +229,90 @@ test('in admin only group, members can not update group name after admin status // throw new Error('Expected exception when non-admin attempts to update group name.') return true }) + +test('can not remove a super admin from a group', async () => { + // Create clients + const [alix, bo] = await createClients(3) + + // Alix Create a group + const alixGroup = await alix.conversations.newGroup( + [bo.address], + 'all_members' + ) + + let alixIsSuperAdmin = await alixGroup.isSuperAdmin(alix.inboxId) + let boIsSuperAdmin = await alixGroup.isSuperAdmin(bo.inboxId) + let numMembers = (await alixGroup.memberInboxIds()).length + assert(alixIsSuperAdmin, `alix should be a super admin`) + assert(!boIsSuperAdmin, `bo should not be a super admin`) + assert( + numMembers === 2, + `number of members should be 2 but was ${numMembers}` + ) + + await bo.conversations.syncGroups() + const boGroup = (await bo.conversations.listGroups())[0] + await boGroup.sync() + + // Bo should not be able to remove alix from the group + try { + await boGroup.removeMembersByInboxId([alix.inboxId]) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (error) { + // expected + } + + await boGroup.sync() + numMembers = (await alixGroup.memberInboxIds()).length + assert(alixIsSuperAdmin, `alix should be a super admin`) + assert(!boIsSuperAdmin, `bo should not be a super admin`) + assert( + numMembers === 2, + `number of members should be 2 but was ${numMembers}` + ) + + // Alix adds bo as a super admin + await alixGroup.addSuperAdmin(bo.inboxId) + await alixGroup.sync() + boIsSuperAdmin = await alixGroup.isSuperAdmin(bo.inboxId) + assert(boIsSuperAdmin, `bo should be a super admin`) + await boGroup.sync() + boIsSuperAdmin = await boGroup.isSuperAdmin(bo.inboxId) + assert(boIsSuperAdmin, `bo should be a super admin`) + + // Uncommenting below causes an error + // intent 3 has reached max publish attempts + // error publishing intents CreateGroupContextExtProposalError(MlsGroupStateError(PendingCommit)) + // try { + // await boGroup.removeMembersByInboxId([alix.inboxId]) + // } catch (error) { + // // expected + // } + await boGroup.sync() + await alixGroup.sync() + numMembers = (await alixGroup.memberInboxIds()).length + assert( + numMembers === 2, + `number of members should be 2 but was ${numMembers}` + ) + + // Bo can remove alix as a super admin + await boGroup.sync() + await boGroup.removeSuperAdmin(alix.inboxId) + await boGroup.sync() + await alixGroup.sync() + alixIsSuperAdmin = await alixGroup.isSuperAdmin(alix.inboxId) + assert(!alixIsSuperAdmin, `alix should not be a super admin`) + + // Now bo can remove Alix from the group + await boGroup.removeMembers([alix.address]) + console.log('alix inbox id:' + String(alix.inboxId)) + await boGroup.sync() + numMembers = (await boGroup.memberInboxIds()).length + assert( + numMembers === 1, + `number of members should be 1 but was ${numMembers}` + ) + + return true +}) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 02f1287fa..c35ad5755 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -805,7 +805,7 @@ public class XMTPModule: Module { return try group.addedByInboxId() } - AsyncFunction("isGroupAdmin") { (clientAddress: String, id: String) -> Bool in + AsyncFunction("creatorInboxId") { (clientAddress: String, id: String) -> String in guard let client = await clientsManager.getClient(key: clientAddress) else { throw Error.noClient } @@ -813,7 +813,87 @@ public class XMTPModule: Module { throw Error.conversationNotFound("no group found for \(id)") } - return try group.isAdmin(inboxId: client.inboxID) + return try group.creatorInboxId() + } + + AsyncFunction("isAdmin") { (clientAddress: String, id: String, inboxId: String) -> Bool in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + return try group.isAdmin(inboxId: inboxId) + } + + AsyncFunction("isSuperAdmin") { (clientAddress: String, id: String, inboxId: String) -> Bool in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + return try group.isSuperAdmin(inboxId: inboxId) + } + + AsyncFunction("listAdmins") { (clientAddress: String, id: String) -> [String] in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + return try group.listAdmins() + } + + AsyncFunction("listSuperAdmins") { (clientAddress: String, id: String) -> [String] in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + return try group.listSuperAdmins() + } + + AsyncFunction("addAdmin") { (clientAddress: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.addAdmin(inboxId: inboxId) + } + + AsyncFunction("addSuperAdmin") { (clientAddress: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.addSuperAdmin(inboxId: inboxId) + } + + AsyncFunction("removeAdmin") { (clientAddress: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.removeAdmin(inboxId: inboxId) + } + + AsyncFunction("removeSuperAdmin") { (clientAddress: String, id: String, inboxId: String) in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + guard let group = try await findGroup(clientAddress: clientAddress, id: id) else { + throw Error.conversationNotFound("no group found for \(id)") + } + try await group.removeSuperAdmin(inboxId: inboxId) } AsyncFunction("processGroupMessage") { (clientAddress: String, id: String, encryptedMessage: String) -> String in From 4c93d11e4788dd72f3b2fc9999e743e1b90b9d96 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 30 May 2024 14:39:59 -0700 Subject: [PATCH 3/4] remove shell script --- update_test_numbers.sh | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100755 update_test_numbers.sh diff --git a/update_test_numbers.sh b/update_test_numbers.sh deleted file mode 100755 index ebbe44eed..000000000 --- a/update_test_numbers.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Path to the TypeScript test file -file_path="example/src/tests/groupTests.ts" - -# Temporary file to store intermediate results -temp_file=$(mktemp) - -# Counter for test numbers -test_number=1 - -# Process the file -awk -v test_num="$test_number" ' - # Match lines that start with "test(" and are function declarations - /^test\(.*\)/ { - # Check if the previous line is a test number comment - if (prev_line ~ /^\/\/ Test [0-9]+$/) { - # Replace the number in the existing comment - sub(/[0-9]+$/, test_num, prev_line) - print prev_line - } else { - # Print a new test number comment - print "// Test " test_num - } - test_num++ - } - # Store the current line in prev_line before printing - { prev_line = $0; if (!/^\/\/ Test [0-9]+$/) print } -' "$file_path" > "$temp_file" - -# Move the temporary file to the original file -mv "$temp_file" "$file_path" - -# Remove the temporary file if needed (optional, as mv already moved it) -rm -f "$temp_file" - -echo "Updated test numbers in $file_path" From e96160ff13523d164f892c5b490789bd9969f003 Mon Sep 17 00:00:00 2001 From: cameronvoell Date: Thu, 30 May 2024 14:48:14 -0700 Subject: [PATCH 4/4] update comment --- src/lib/Group.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/Group.ts b/src/lib/Group.ts index ec0e2dd73..aff2ead89 100644 --- a/src/lib/Group.ts +++ b/src/lib/Group.ts @@ -263,7 +263,8 @@ export class Group< return XMTP.removeAdmin(this.client.address, this.id, inboxId) } - // Remove Super Admin + // Removes an inboxId from the group super admins. + // Will throw if the user does not have the required permissions. async removeSuperAdmin(inboxId: string): Promise { return XMTP.removeSuperAdmin(this.client.address, this.id, inboxId) }