Skip to content

Commit

Permalink
Adds admin list functionality (#341)
Browse files Browse the repository at this point in the history
* add admin level functions from libxmtp

* bump ios podspec version to 0.11.0

---------

Co-authored-by: cameronvoell <cameronvoell@users.noreply.github.com>
  • Loading branch information
cameronvoell and cameronvoell authored May 29, 2024
1 parent 491bd82 commit 1147eb1
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 97 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/xmtp/libxmtp-swift",
"state" : {
"revision" : "f2b73d14be21b64feecfa70c789b3302525975ab",
"version" : "0.4.4-beta5"
"revision" : "bfee6cde5cadf0e1842b77daf80e6e4a88830194",
"version" : "0.5.0-beta1"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ let package = Package(
.package(url: "https://github.com/1024jp/GzipSwift", from: "5.2.0"),
.package(url: "https://github.com/bufbuild/connect-swift", exact: "0.12.0"),
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/xmtp/libxmtp-swift", exact: "0.5.0-beta0"),
.package(url: "https://github.com/xmtp/libxmtp-swift", exact: "0.5.0-beta1"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
Expand Down
2 changes: 1 addition & 1 deletion Sources/XMTPiOS/Conversations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ public actor Conversations {
}
}

public func newGroup(with addresses: [String], permissions: GroupPermissions = .everyoneIsAdmin) async throws -> Group {
public func newGroup(with addresses: [String], permissions: GroupPermissions = .allMembers) async throws -> Group {
guard let v3Client = client.v3Client else {
throw GroupError.alphaMLSNotEnabled
}
Expand Down
46 changes: 41 additions & 5 deletions Sources/XMTPiOS/Group.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ public struct Group: Identifiable, Equatable, Hashable {
func metadata() throws -> FfiGroupMetadata {
return try ffiGroup.groupMetadata()
}

func permissions() throws -> FfiGroupPermissions {
return try ffiGroup.groupPermissions()
}

public func sync() async throws {
try await ffiGroup.sync()
Expand All @@ -59,15 +63,47 @@ public struct Group: Identifiable, Equatable, Hashable {
return try ffiGroup.isActive()
}

public func isAdmin() throws -> Bool {
public func isCreator() throws -> Bool {
return try metadata().creatorInboxId() == client.inboxID
}

public func isAdmin(inboxId: String) throws -> Bool {
return try ffiGroup.isAdmin(inboxId: inboxId)
}

public func isSuperAdmin(inboxId: String) throws -> Bool {
return try ffiGroup.isSuperAdmin(inboxId: inboxId)
}

public func addAdmin(inboxId: String) async throws {
try await ffiGroup.addAdmin(inboxId: inboxId)
}

public func removeAdmin(inboxId: String) async throws {
try await ffiGroup.removeAdmin(inboxId: inboxId)
}

public func addSuperAdmin(inboxId: String) async throws {
try await ffiGroup.addSuperAdmin(inboxId: inboxId)
}

public func removeSuperAdmin(inboxId: String) async throws {
try await ffiGroup.removeSuperAdmin(inboxId: inboxId)
}

public func listAdmins() throws -> [String] {
try ffiGroup.adminList()
}

public func listSuperAdmins() throws -> [String] {
try ffiGroup.superAdminList()
}

// public func permissionLevel() throws -> GroupPermissions {
// return try metadata().policyType()
// }
public func permissionLevel() throws -> GroupPermissions {
return try permissions().policyType()
}

public func adminInboxId() throws -> String {
public func creatorInboxId() throws -> String {
return try metadata().creatorInboxId()
}

Expand Down
37 changes: 26 additions & 11 deletions Sources/XMTPiOS/Mls/Member.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,34 @@
import Foundation
import LibXMTP

public enum PermissionLevel {
case Member, Admin, SuperAdmin
}

public struct Member {
var ffiGroupMember: FfiGroupMember
init(ffiGroupMember: FfiGroupMember) {
self.ffiGroupMember = ffiGroupMember
}
var ffiGroupMember: FfiGroupMember
init(ffiGroupMember: FfiGroupMember) {
self.ffiGroupMember = ffiGroupMember
}

public var inboxId: String {
ffiGroupMember.inboxId
}

public var addresses: [String] {
ffiGroupMember.accountAddresses
public var inboxId: String {
ffiGroupMember.inboxId
}

public var addresses: [String] {
ffiGroupMember.accountAddresses
}

public var permissionLevel: PermissionLevel {
switch ffiGroupMember.permissionLevel {
case .member:
return PermissionLevel.Member
case .admin:
return PermissionLevel.Admin
case .superAdmin:
return PermissionLevel.SuperAdmin
}
}
}

232 changes: 232 additions & 0 deletions Tests/XMTPTests/GroupPermissionsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
//
// GroupPermissionTests.swift
//
//
// Created by Cameron Voell on 5/29/24.
//

import CryptoKit
import XCTest
@testable import XMTPiOS
import LibXMTP
import XMTPTestHelpers

@available(iOS 16, *)
class GroupPermissionTests: XCTestCase {
// Use these fixtures to talk to the local node
struct LocalFixtures {
var alice: PrivateKey!
var bob: PrivateKey!
var caro: PrivateKey!
var aliceClient: Client!
var bobClient: Client!
var caroClient: Client!
}

func localFixtures() async throws -> LocalFixtures {
let key = try Crypto.secureRandomBytes(count: 32)
let alice = try PrivateKey.generate()
let aliceClient = try await Client.create(
account: alice,
options: .init(
api: .init(env: .local, isSecure: false),
codecs: [GroupUpdatedCodec()],
mlsAlpha: true,
mlsEncryptionKey: key
)
)
let bob = try PrivateKey.generate()
let bobClient = try await Client.create(
account: bob,
options: .init(
api: .init(env: .local, isSecure: false),
codecs: [GroupUpdatedCodec()],
mlsAlpha: true,
mlsEncryptionKey: key
)
)
let caro = try PrivateKey.generate()
let caroClient = try await Client.create(
account: caro,
options: .init(
api: .init(env: .local, isSecure: false),
codecs: [GroupUpdatedCodec()],
mlsAlpha: true,
mlsEncryptionKey: key
)
)

return .init(
alice: alice,
bob: bob,
caro: caro,
aliceClient: aliceClient,
bobClient: bobClient,
caroClient: caroClient
)
}

func testGroupCreatedWithCorrectAdminList() async throws {
let fixtures = try await localFixtures()
let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.walletAddress])
try await fixtures.aliceClient.conversations.sync()
let aliceGroup = try await fixtures.aliceClient.conversations.groups().first!

XCTAssertTrue(try bobGroup.isAdmin(inboxId: fixtures.bobClient.inboxID))
XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID))
XCTAssertFalse(try aliceGroup.isCreator())
XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID))
XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID))

let adminList = try bobGroup.listAdmins()
let superAdminList = try bobGroup.listSuperAdmins()

XCTAssertEqual(adminList.count, 1)
XCTAssertTrue(adminList.contains(fixtures.bobClient.inboxID))
XCTAssertEqual(superAdminList.count, 1)
XCTAssertTrue(superAdminList.contains(fixtures.bobClient.inboxID))
}

func testGroupCanUpdateAdminList() async throws {
let fixtures = try await localFixtures()
let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.walletAddress, fixtures.caro.walletAddress], permissions: .adminOnly)
try await fixtures.aliceClient.conversations.sync()
let aliceGroup = try await fixtures.aliceClient.conversations.groups().first!

XCTAssertTrue(try bobGroup.isAdmin(inboxId: fixtures.bobClient.inboxID))
XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID))
XCTAssertFalse(try aliceGroup.isCreator())
XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID))
XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID))

var adminList = try bobGroup.listAdmins()
var superAdminList = try bobGroup.listSuperAdmins()
XCTAssertEqual(adminList.count, 1)
XCTAssertTrue(adminList.contains(fixtures.bobClient.inboxID))
XCTAssertEqual(superAdminList.count, 1)
XCTAssertTrue(superAdminList.contains(fixtures.bobClient.inboxID))

// Verify that alice can NOT update group name
XCTAssertEqual(try bobGroup.groupName(), "New Group")
await assertThrowsAsyncError(
try await aliceGroup.updateGroupName(groupName: "Alice group name")
)

try await aliceGroup.sync()
try await bobGroup.sync()
XCTAssertEqual(try bobGroup.groupName(), "New Group")
XCTAssertEqual(try aliceGroup.groupName(), "New Group")

try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID)
try await bobGroup.sync()
try await aliceGroup.sync()

adminList = try bobGroup.listAdmins()
superAdminList = try bobGroup.listSuperAdmins()

XCTAssertTrue(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID))
XCTAssertEqual(adminList.count, 2)
XCTAssertTrue(adminList.contains(fixtures.aliceClient.inboxID))
XCTAssertEqual(superAdminList.count, 1)

// Verify that alice can now update group name
try await aliceGroup.updateGroupName(groupName: "Alice group name")
try await aliceGroup.sync()
try await bobGroup.sync()
XCTAssertEqual(try bobGroup.groupName(), "Alice group name")
XCTAssertEqual(try aliceGroup.groupName(), "Alice group name")

try await bobGroup.removeAdmin(inboxId: fixtures.aliceClient.inboxID)
try await bobGroup.sync()
try await aliceGroup.sync()

adminList = try bobGroup.listAdmins()
superAdminList = try bobGroup.listSuperAdmins()

XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID))
XCTAssertEqual(adminList.count, 1)
XCTAssertFalse(adminList.contains(fixtures.aliceClient.inboxID))
XCTAssertEqual(superAdminList.count, 1)

// Verify that alice can NOT update group name
await assertThrowsAsyncError(
try await aliceGroup.updateGroupName(groupName: "Alice group name 2")
)
}

func testGroupCanUpdateSuperAdminList() async throws {
let fixtures = try await localFixtures()
let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.walletAddress, fixtures.caro.walletAddress], permissions: .adminOnly)
try await fixtures.aliceClient.conversations.sync()
let aliceGroup = try await fixtures.aliceClient.conversations.groups().first!

XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID))
XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID))

// Attempt to remove bob as a super admin by alice should fail since she is not a super admin
await assertThrowsAsyncError(
try await aliceGroup.removeSuperAdmin(inboxId: fixtures.bobClient.inboxID)
)

// Make alice a super admin
try await bobGroup.addSuperAdmin(inboxId: fixtures.aliceClient.inboxID)
try await bobGroup.sync()
try await aliceGroup.sync()
XCTAssertTrue(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID))

// Now alice should be able to remove bob as a super admin
try await aliceGroup.removeSuperAdmin(inboxId: fixtures.bobClient.inboxID)
try await aliceGroup.sync()
try await bobGroup.sync()

let superAdminList = try bobGroup.listSuperAdmins()
XCTAssertFalse(superAdminList.contains(fixtures.bobClient.inboxID))
XCTAssertTrue(superAdminList.contains(fixtures.aliceClient.inboxID))
}

func testGroupMembersAndPermissionLevel() async throws {
let fixtures = try await localFixtures()
let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.walletAddress, fixtures.caro.walletAddress], permissions: .adminOnly)
try await fixtures.aliceClient.conversations.sync()
let aliceGroup = try await fixtures.aliceClient.conversations.groups().first!

// Initial checks for group members and their permissions
var members = try bobGroup.members
var admins = members.filter { $0.permissionLevel == PermissionLevel.Admin }
var superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin }
var regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member }

XCTAssertEqual(admins.count, 0)
XCTAssertEqual(superAdmins.count, 1)
XCTAssertEqual(regularMembers.count, 2)

// Add alice as an admin
try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID)
try await bobGroup.sync()
try await aliceGroup.sync()

members = try bobGroup.members
admins = members.filter { $0.permissionLevel == PermissionLevel.Admin }
superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin }
regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member }

XCTAssertEqual(admins.count, 1)
XCTAssertEqual(superAdmins.count, 1)
XCTAssertEqual(regularMembers.count, 1)

// Add caro as a super admin
try await bobGroup.addSuperAdmin(inboxId: fixtures.caroClient.inboxID)
try await bobGroup.sync()
try await aliceGroup.sync()

members = try bobGroup.members
admins = members.filter { $0.permissionLevel == PermissionLevel.Admin }
superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin }
regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member }

XCTAssertEqual(admins.count, 1)
XCTAssertEqual(superAdmins.count, 2)
XCTAssertTrue(regularMembers.isEmpty)
}

}
Loading

0 comments on commit 1147eb1

Please sign in to comment.