diff --git a/.changeset/short-crews-carry.md b/.changeset/short-crews-carry.md new file mode 100644 index 000000000..07a0c0c1a --- /dev/null +++ b/.changeset/short-crews-carry.md @@ -0,0 +1,6 @@ +--- +"@xmtp/browser-sdk": patch +"@xmtp/node-sdk": patch +--- + +Enable group permissions updates diff --git a/sdks/browser-sdk/src/Conversation.ts b/sdks/browser-sdk/src/Conversation.ts index 373f66078..e7ab6fbc4 100644 --- a/sdks/browser-sdk/src/Conversation.ts +++ b/sdks/browser-sdk/src/Conversation.ts @@ -1,6 +1,11 @@ import type { ContentTypeId } from "@xmtp/content-type-primitives"; import { ContentTypeText } from "@xmtp/content-type-text"; -import type { ConsentState } from "@xmtp/wasm-bindings"; +import type { + ConsentState, + MetadataField, + PermissionPolicy, + PermissionUpdateType, +} from "@xmtp/wasm-bindings"; import type { Client } from "@/Client"; import { DecodedMessage } from "@/DecodedMessage"; import type { @@ -28,8 +33,6 @@ export class Conversation { #metadata?: SafeConversation["metadata"]; - #permissions?: SafeConversation["permissions"]; - #createdAtNs?: SafeConversation["createdAtNs"]; #admins: SafeConversation["admins"] = []; @@ -50,7 +53,6 @@ export class Conversation { this.#isActive = data?.isActive ?? undefined; this.#addedByInboxId = data?.addedByInboxId ?? ""; this.#metadata = data?.metadata ?? undefined; - this.#permissions = data?.permissions ?? undefined; this.#createdAtNs = data?.createdAtNs ?? undefined; this.#admins = data?.admins ?? []; this.#superAdmins = data?.superAdmins ?? []; @@ -156,8 +158,23 @@ export class Conversation { this.#superAdmins = superAdmins; } - get permissions() { - return this.#permissions; + async permissions() { + return this.#client.sendMessage("getGroupPermissions", { + id: this.#id, + }); + } + + async updatePermission( + permissionType: PermissionUpdateType, + policy: PermissionPolicy, + metadataField?: MetadataField, + ) { + return this.#client.sendMessage("updateGroupPermissionPolicy", { + id: this.#id, + permissionType, + policy, + metadataField, + }); } async isAdmin(inboxId: string) { diff --git a/sdks/browser-sdk/src/WorkerConversation.ts b/sdks/browser-sdk/src/WorkerConversation.ts index 241dca5c1..08d8bdd58 100644 --- a/sdks/browser-sdk/src/WorkerConversation.ts +++ b/sdks/browser-sdk/src/WorkerConversation.ts @@ -3,6 +3,9 @@ import type { Conversation, EncodedContent, GroupMember, + MetadataField, + PermissionPolicy, + PermissionUpdateType, } from "@xmtp/wasm-bindings"; import { fromSafeListMessagesOptions, @@ -99,6 +102,18 @@ export class WorkerConversation { }; } + async updatePermission( + permissionType: PermissionUpdateType, + policy: PermissionPolicy, + metadataField?: MetadataField, + ) { + return this.#group.updatePermissionPolicy( + permissionType, + policy, + metadataField, + ); + } + isAdmin(inboxId: string) { return this.#group.isAdmin(inboxId); } diff --git a/sdks/browser-sdk/src/index.ts b/sdks/browser-sdk/src/index.ts index 09616d064..2b9af0da0 100644 --- a/sdks/browser-sdk/src/index.ts +++ b/sdks/browser-sdk/src/index.ts @@ -25,6 +25,7 @@ export { ListConversationsOptions, ListMessagesOptions, Message, + MetadataField, PermissionLevel, PermissionPolicy, PermissionPolicySet, diff --git a/sdks/browser-sdk/src/types/clientEvents.ts b/sdks/browser-sdk/src/types/clientEvents.ts index 3e6f1a02e..14ffa0303 100644 --- a/sdks/browser-sdk/src/types/clientEvents.ts +++ b/sdks/browser-sdk/src/types/clientEvents.ts @@ -1,6 +1,9 @@ import type { ConsentEntityType, ConsentState, + MetadataField, + PermissionPolicy, + PermissionUpdateType, SignatureRequestType, } from "@xmtp/wasm-bindings"; import type { @@ -485,6 +488,25 @@ export type ClientEvents = data: { id: string; }; + } + | { + action: "updateGroupPermissionPolicy"; + id: string; + result: undefined; + data: { + id: string; + permissionType: PermissionUpdateType; + policy: PermissionPolicy; + metadataField?: MetadataField; + }; + } + | { + action: "getGroupPermissions"; + id: string; + result: SafeConversation["permissions"]; + data: { + id: string; + }; }; export type ClientEventsActions = ClientEvents["action"]; diff --git a/sdks/browser-sdk/src/workers/client.ts b/sdks/browser-sdk/src/workers/client.ts index ab62b38af..65dc68087 100644 --- a/sdks/browser-sdk/src/workers/client.ts +++ b/sdks/browser-sdk/src/workers/client.ts @@ -826,6 +826,46 @@ self.onmessage = async (event: MessageEvent) => { } break; } + case "updateGroupPermissionPolicy": { + const group = client.conversations.getConversationById(data.id); + if (group) { + await group.updatePermission( + data.permissionType, + data.policy, + data.metadataField, + ); + postMessage({ + id, + action, + result: undefined, + }); + } else { + postMessageError({ + id, + action, + error: "Group not found", + }); + } + break; + } + case "getGroupPermissions": { + const group = client.conversations.getConversationById(data.id); + if (group) { + const safeConversation = await toSafeConversation(group); + postMessage({ + id, + action, + result: safeConversation.permissions, + }); + } else { + postMessageError({ + id, + action, + error: "Group not found", + }); + } + break; + } } } catch (e) { postMessageError({ diff --git a/sdks/browser-sdk/test/Conversation.test.ts b/sdks/browser-sdk/test/Conversation.test.ts index df0bb8dba..586f603c2 100644 --- a/sdks/browser-sdk/test/Conversation.test.ts +++ b/sdks/browser-sdk/test/Conversation.test.ts @@ -1,4 +1,9 @@ -import { ConsentState } from "@xmtp/wasm-bindings"; +import { + ConsentState, + MetadataField, + PermissionPolicy, + PermissionUpdateType, +} from "@xmtp/wasm-bindings"; import { describe, expect, it } from "vitest"; import { ContentTypeTest, @@ -404,4 +409,82 @@ describe.concurrent("Conversation", () => { await dmGroup2!.send("gm!"); expect(await dmGroup2!.consentState()).toBe(ConsentState.Allowed); }); + + it("should update group permission policy", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); + const conversation = await client1.conversations.newGroup([ + user2.account.address, + ]); + + const permissions = await conversation.permissions(); + expect(permissions.policySet).toEqual({ + addMemberPolicy: 0, + removeMemberPolicy: 2, + addAdminPolicy: 3, + removeAdminPolicy: 3, + updateGroupNamePolicy: 0, + updateGroupDescriptionPolicy: 0, + updateGroupImageUrlSquarePolicy: 0, + updateGroupPinnedFrameUrlPolicy: 0, + }); + + await conversation.updatePermission( + PermissionUpdateType.AddMember, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.RemoveMember, + PermissionPolicy.SuperAdmin, + ); + + await conversation.updatePermission( + PermissionUpdateType.AddAdmin, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.RemoveAdmin, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.GroupName, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.Description, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.ImageUrlSquare, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.PinnedFrameUrl, + ); + + const permissions2 = await conversation.permissions(); + expect(permissions2.policySet).toEqual({ + addMemberPolicy: 2, + removeMemberPolicy: 3, + addAdminPolicy: 2, + removeAdminPolicy: 2, + updateGroupNamePolicy: 2, + updateGroupDescriptionPolicy: 2, + updateGroupImageUrlSquarePolicy: 2, + updateGroupPinnedFrameUrlPolicy: 2, + }); + }); }); diff --git a/sdks/browser-sdk/test/Conversations.test.ts b/sdks/browser-sdk/test/Conversations.test.ts index 224ffc13d..cb571fcc6 100644 --- a/sdks/browser-sdk/test/Conversations.test.ts +++ b/sdks/browser-sdk/test/Conversations.test.ts @@ -29,10 +29,9 @@ describe.concurrent("Conversations", () => { expect(conversation.createdAt).toBeDefined(); expect(conversation.isActive).toBe(true); expect(conversation.name).toBe(""); - expect(conversation.permissions?.policyType).toBe( - GroupPermissionsOptions.AllMembers, - ); - expect(conversation.permissions?.policySet).toEqual({ + const permissions = await conversation.permissions(); + expect(permissions.policyType).toBe(GroupPermissionsOptions.AllMembers); + expect(permissions.policySet).toEqual({ addMemberPolicy: 0, removeMemberPolicy: 2, addAdminPolicy: 3, @@ -83,10 +82,9 @@ describe.concurrent("Conversations", () => { expect(group.createdAt).toBeDefined(); expect(group.isActive).toBe(true); expect(group.name).toBe(""); - expect(group.permissions?.policyType).toBe( - GroupPermissionsOptions.CustomPolicy, - ); - expect(group.permissions?.policySet).toEqual({ + const permissions = await group.permissions(); + expect(permissions.policyType).toBe(GroupPermissionsOptions.CustomPolicy); + expect(permissions.policySet).toEqual({ addAdminPolicy: 1, addMemberPolicy: 1, removeAdminPolicy: 1, @@ -217,11 +215,10 @@ describe.concurrent("Conversations", () => { expect(groupWithPermissions).toBeDefined(); expect(groupWithPermissions.name).toBe(""); expect(groupWithPermissions.imageUrl).toBe(""); - expect(groupWithPermissions.permissions?.policyType).toBe( - GroupPermissionsOptions.AdminOnly, - ); - expect(groupWithPermissions.permissions?.policySet).toEqual({ + const permissions = await groupWithPermissions.permissions(); + expect(permissions.policyType).toBe(GroupPermissionsOptions.AdminOnly); + expect(permissions.policySet).toEqual({ addMemberPolicy: 2, removeMemberPolicy: 2, addAdminPolicy: 3, @@ -278,10 +275,10 @@ describe.concurrent("Conversations", () => { }, ); expect(group).toBeDefined(); - expect(group.permissions?.policyType).toBe( - GroupPermissionsOptions.CustomPolicy, - ); - expect(group.permissions?.policySet).toEqual({ + + const permissions = await group.permissions(); + expect(permissions.policyType).toBe(GroupPermissionsOptions.CustomPolicy); + expect(permissions.policySet).toEqual({ addAdminPolicy: 1, addMemberPolicy: 0, removeAdminPolicy: 1, diff --git a/sdks/node-sdk/src/Conversation.ts b/sdks/node-sdk/src/Conversation.ts index 241b43a33..57724167b 100644 --- a/sdks/node-sdk/src/Conversation.ts +++ b/sdks/node-sdk/src/Conversation.ts @@ -4,6 +4,9 @@ import type { ConsentState, Conversation as Group, ListMessagesOptions, + MetadataField, + PermissionPolicy, + PermissionUpdateType, } from "@xmtp/node-bindings"; import { AsyncStream, type StreamCallback } from "@/AsyncStream"; import type { Client } from "@/Client"; @@ -92,12 +95,25 @@ export class Conversation { } get permissions() { + const permissions = this.#group.groupPermissions(); return { - policyType: this.#group.groupPermissions().policyType(), - policySet: this.#group.groupPermissions().policySet(), + policyType: permissions.policyType(), + policySet: permissions.policySet(), }; } + async updatePermission( + permissionType: PermissionUpdateType, + policy: PermissionPolicy, + metadataField?: MetadataField, + ) { + return this.#group.updatePermissionPolicy( + permissionType, + policy, + metadataField, + ); + } + isAdmin(inboxId: string) { return this.#group.isAdmin(inboxId); } diff --git a/sdks/node-sdk/src/index.ts b/sdks/node-sdk/src/index.ts index 8a78d996e..00f7e2269 100644 --- a/sdks/node-sdk/src/index.ts +++ b/sdks/node-sdk/src/index.ts @@ -35,6 +35,7 @@ export { GroupPermissions, GroupPermissionsOptions, LogLevel, + MetadataField, PermissionLevel, PermissionPolicy, PermissionUpdateType, diff --git a/sdks/node-sdk/test/Conversation.test.ts b/sdks/node-sdk/test/Conversation.test.ts index 6f7943b8a..b1938bd60 100644 --- a/sdks/node-sdk/test/Conversation.test.ts +++ b/sdks/node-sdk/test/Conversation.test.ts @@ -1,4 +1,9 @@ -import { ConsentState } from "@xmtp/node-bindings"; +import { + ConsentState, + MetadataField, + PermissionPolicy, + PermissionUpdateType, +} from "@xmtp/node-bindings"; import { describe, expect, it } from "vitest"; import { ContentTypeTest, @@ -427,4 +432,80 @@ describe("Conversation", () => { await dmGroup2!.send("gm!"); expect(dmGroup2!.consentState).toBe(ConsentState.Allowed); }); + + it("should update group permissions", async () => { + const user1 = createUser(); + const user2 = createUser(); + const client1 = await createRegisteredClient(user1); + await createRegisteredClient(user2); + const conversation = await client1.conversations.newGroup([ + user2.account.address, + ]); + + expect(conversation.permissions.policySet).toEqual({ + addMemberPolicy: 0, + removeMemberPolicy: 2, + addAdminPolicy: 3, + removeAdminPolicy: 3, + updateGroupNamePolicy: 0, + updateGroupDescriptionPolicy: 0, + updateGroupImageUrlSquarePolicy: 0, + updateGroupPinnedFrameUrlPolicy: 0, + }); + + await conversation.updatePermission( + PermissionUpdateType.AddMember, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.RemoveMember, + PermissionPolicy.SuperAdmin, + ); + + await conversation.updatePermission( + PermissionUpdateType.AddAdmin, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.RemoveAdmin, + PermissionPolicy.Admin, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.GroupName, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.Description, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.ImageUrlSquare, + ); + + await conversation.updatePermission( + PermissionUpdateType.UpdateMetadata, + PermissionPolicy.Admin, + MetadataField.PinnedFrameUrl, + ); + + expect(conversation.permissions.policySet).toEqual({ + addMemberPolicy: 2, + removeMemberPolicy: 3, + addAdminPolicy: 2, + removeAdminPolicy: 2, + updateGroupNamePolicy: 2, + updateGroupDescriptionPolicy: 2, + updateGroupImageUrlSquarePolicy: 2, + updateGroupPinnedFrameUrlPolicy: 2, + }); + }); });