diff --git a/.vscode/settings.json b/.vscode/settings.json index 34f9ee388..2a034caa1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,6 @@ { "rust-analyzer.cargo.sysroot": "discover", - "rust-analyzer.linkedProjects": [ - "bindings_ffi/Cargo.toml", - "bindings_node/Cargo.toml", - "bindings_wasm/Cargo.toml", - "examples/cli/Cargo.toml" - ], + "rust-analyzer.linkedProjects": ["Cargo.toml"], "rust-analyzer.procMacro.enable": true, "rust-analyzer.procMacro.attributes.enable": true, "rust-analyzer.procMacro.ignored": { diff --git a/Cargo.lock b/Cargo.lock index 3f3816f8a..2955f0040 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -452,8 +452,12 @@ dependencies = [ name = "bindings_wasm" version = "0.0.1" dependencies = [ + "hex", "js-sys", + "prost", + "serde", "serde-wasm-bindgen", + "tokio", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test", @@ -461,6 +465,7 @@ dependencies = [ "xmtp_cryptography", "xmtp_id", "xmtp_mls", + "xmtp_proto", ] [[package]] diff --git a/bindings_wasm/Cargo.toml b/bindings_wasm/Cargo.toml index 746b79103..2cb631a35 100644 --- a/bindings_wasm/Cargo.toml +++ b/bindings_wasm/Cargo.toml @@ -7,15 +7,22 @@ version.workspace = true crate-type = ["cdylib", "rlib"] [dependencies] +hex.workspace = true js-sys.workspace = true +prost.workspace = true serde-wasm-bindgen = "0.6.5" -wasm-bindgen.workspace = true +serde.workspace = true +tokio.workspace = true wasm-bindgen-futures.workspace = true +wasm-bindgen.workspace = true xmtp_api_http = { path = "../xmtp_api_http" } xmtp_cryptography = { path = "../xmtp_cryptography" } xmtp_id = { path = "../xmtp_id" } -xmtp_mls = { path = "../xmtp_mls", features = ["message-history"] } +xmtp_mls = { path = "../xmtp_mls", features = [ + "message-history", + "test-utils", +] } +xmtp_proto = { path = "../xmtp_proto", features = ["proto_full"] } [dev-dependencies] wasm-bindgen-test.workspace = true - diff --git a/bindings_wasm/src/consent_state.rs b/bindings_wasm/src/consent_state.rs new file mode 100644 index 000000000..0b13f3df9 --- /dev/null +++ b/bindings_wasm/src/consent_state.rs @@ -0,0 +1,65 @@ +use wasm_bindgen::prelude::wasm_bindgen; +use xmtp_mls::storage::consent_record::{ConsentState, ConsentType, StoredConsentRecord}; + +#[wasm_bindgen] +#[derive(Clone, serde::Serialize)] +pub enum WasmConsentState { + Unknown, + Allowed, + Denied, +} + +impl From for WasmConsentState { + fn from(state: ConsentState) -> Self { + match state { + ConsentState::Unknown => WasmConsentState::Unknown, + ConsentState::Allowed => WasmConsentState::Allowed, + ConsentState::Denied => WasmConsentState::Denied, + } + } +} + +impl From for ConsentState { + fn from(state: WasmConsentState) -> Self { + match state { + WasmConsentState::Unknown => ConsentState::Unknown, + WasmConsentState::Allowed => ConsentState::Allowed, + WasmConsentState::Denied => ConsentState::Denied, + } + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub enum WasmConsentEntityType { + GroupId, + InboxId, + Address, +} + +impl From for ConsentType { + fn from(entity_type: WasmConsentEntityType) -> Self { + match entity_type { + WasmConsentEntityType::GroupId => ConsentType::GroupId, + WasmConsentEntityType::InboxId => ConsentType::InboxId, + WasmConsentEntityType::Address => ConsentType::Address, + } + } +} + +#[wasm_bindgen(getter_with_clone)] +pub struct WasmConsent { + pub entity_type: WasmConsentEntityType, + pub state: WasmConsentState, + pub entity: String, +} + +impl From for StoredConsentRecord { + fn from(consent: WasmConsent) -> Self { + Self { + entity_type: consent.entity_type.into(), + state: consent.state.into(), + entity: consent.entity, + } + } +} diff --git a/bindings_wasm/src/conversations.rs b/bindings_wasm/src/conversations.rs new file mode 100644 index 000000000..415018d7f --- /dev/null +++ b/bindings_wasm/src/conversations.rs @@ -0,0 +1,176 @@ +use std::sync::Arc; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::{JsError, JsValue}; +use xmtp_mls::client::FindGroupParams; +use xmtp_mls::groups::{GroupMetadataOptions, PreconfiguredPolicies}; + +use crate::messages::WasmMessage; +use crate::permissions::WasmGroupPermissionsOptions; +use crate::{groups::WasmGroup, mls_client::RustXmtpClient}; + +#[wasm_bindgen(getter_with_clone)] +pub struct WasmListConversationsOptions { + pub created_after_ns: Option, + pub created_before_ns: Option, + pub limit: Option, +} + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct WasmCreateGroupOptions { + pub permissions: Option, + pub group_name: Option, + pub group_image_url_square: Option, + pub group_description: Option, + pub group_pinned_frame_url: Option, +} + +impl WasmCreateGroupOptions { + pub fn into_group_metadata_options(self) -> GroupMetadataOptions { + GroupMetadataOptions { + name: self.group_name, + image_url_square: self.group_image_url_square, + description: self.group_description, + pinned_frame_url: self.group_pinned_frame_url, + } + } +} + +#[wasm_bindgen] +pub struct WasmConversations { + inner_client: Arc, +} + +impl WasmConversations { + pub fn new(inner_client: Arc) -> Self { + Self { inner_client } + } +} + +#[wasm_bindgen] +impl WasmConversations { + #[wasm_bindgen] + pub async fn create_group( + &self, + account_addresses: Vec, + options: Option, + ) -> Result { + let options = match options { + Some(options) => options, + None => WasmCreateGroupOptions { + permissions: None, + group_name: None, + group_image_url_square: None, + group_description: None, + group_pinned_frame_url: None, + }, + }; + + let group_permissions = match options.permissions { + Some(WasmGroupPermissionsOptions::AllMembers) => { + Some(PreconfiguredPolicies::AllMembers.to_policy_set()) + } + Some(WasmGroupPermissionsOptions::AdminOnly) => { + Some(PreconfiguredPolicies::AdminsOnly.to_policy_set()) + } + _ => None, + }; + + let metadata_options = options.clone().into_group_metadata_options(); + + let convo = if account_addresses.is_empty() { + self + .inner_client + .create_group(group_permissions, metadata_options) + .map_err(|e| JsError::new(format!("{}", e).as_str()))? + } else { + self + .inner_client + .create_group_with_members(account_addresses, group_permissions, metadata_options) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))? + }; + + let out = WasmGroup::new( + self.inner_client.clone(), + convo.group_id, + convo.created_at_ns, + ); + + Ok(out) + } + + #[wasm_bindgen] + pub fn find_group_by_id(&self, group_id: String) -> Result { + let group_id = hex::decode(group_id).map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + let group = self + .inner_client + .group(group_id) + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + Ok(WasmGroup::new( + self.inner_client.clone(), + group.group_id, + group.created_at_ns, + )) + } + + #[wasm_bindgen] + pub fn find_message_by_id(&self, message_id: String) -> Result { + let message_id = + hex::decode(message_id).map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + let message = self + .inner_client + .message(message_id) + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + Ok(WasmMessage::from(message)) + } + + #[wasm_bindgen] + pub async fn sync(&self) -> Result<(), JsError> { + self + .inner_client + .sync_welcomes() + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + Ok(()) + } + + #[wasm_bindgen] + pub async fn list( + &self, + opts: Option, + ) -> Result { + let opts = match opts { + Some(options) => options, + None => WasmListConversationsOptions { + created_after_ns: None, + created_before_ns: None, + limit: None, + }, + }; + let convo_list: js_sys::Array = self + .inner_client + .find_groups(FindGroupParams { + created_after_ns: opts.created_after_ns, + created_before_ns: opts.created_before_ns, + limit: opts.limit, + ..FindGroupParams::default() + }) + .map_err(|e| JsError::new(format!("{}", e).as_str()))? + .into_iter() + .map(|group| { + JsValue::from(WasmGroup::new( + self.inner_client.clone(), + group.group_id, + group.created_at_ns, + )) + }) + .collect(); + + Ok(convo_list) + } +} diff --git a/bindings_wasm/src/encoded_content.rs b/bindings_wasm/src/encoded_content.rs new file mode 100644 index 000000000..87b933b5e --- /dev/null +++ b/bindings_wasm/src/encoded_content.rs @@ -0,0 +1,73 @@ +use js_sys::Uint8Array; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsValue; +use xmtp_proto::xmtp::mls::message_contents::{ContentTypeId, EncodedContent}; + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct WasmContentTypeId { + pub authority_id: String, + pub type_id: String, + pub version_major: u32, + pub version_minor: u32, +} + +impl From for WasmContentTypeId { + fn from(content_type_id: ContentTypeId) -> WasmContentTypeId { + WasmContentTypeId { + authority_id: content_type_id.authority_id, + type_id: content_type_id.type_id, + version_major: content_type_id.version_major, + version_minor: content_type_id.version_minor, + } + } +} + +impl From for ContentTypeId { + fn from(content_type_id: WasmContentTypeId) -> Self { + ContentTypeId { + authority_id: content_type_id.authority_id, + type_id: content_type_id.type_id, + version_major: content_type_id.version_major, + version_minor: content_type_id.version_minor, + } + } +} + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct WasmEncodedContent { + pub r#type: Option, + pub parameters: JsValue, + pub fallback: Option, + pub compression: Option, + pub content: Uint8Array, +} + +impl From for WasmEncodedContent { + fn from(content: EncodedContent) -> WasmEncodedContent { + let r#type = content.r#type.map(|v| v.into()); + + WasmEncodedContent { + r#type, + parameters: serde_wasm_bindgen::to_value(&content.parameters).unwrap(), + fallback: content.fallback, + compression: content.compression, + content: content.content.as_slice().into(), + } + } +} + +impl From for EncodedContent { + fn from(content: WasmEncodedContent) -> Self { + let r#type = content.r#type.map(|v| v.into()); + + EncodedContent { + r#type, + parameters: serde_wasm_bindgen::from_value(content.parameters).unwrap(), + fallback: content.fallback, + compression: content.compression, + content: content.content.to_vec(), + } + } +} diff --git a/bindings_wasm/src/groups.rs b/bindings_wasm/src/groups.rs new file mode 100644 index 000000000..91578677c --- /dev/null +++ b/bindings_wasm/src/groups.rs @@ -0,0 +1,644 @@ +use std::sync::Arc; +use wasm_bindgen::JsValue; +use wasm_bindgen::{prelude::wasm_bindgen, JsError}; + +use crate::encoded_content::WasmEncodedContent; +use crate::messages::{WasmListMessagesOptions, WasmMessage}; +use crate::mls_client::RustXmtpClient; +use crate::{consent_state::WasmConsentState, permissions::WasmGroupPermissions}; +use xmtp_cryptography::signature::ed25519_public_key_to_address; +use xmtp_mls::groups::{ + group_metadata::{ConversationType, GroupMetadata}, + members::PermissionLevel, + MlsGroup, UpdateAdminListType, +}; +use xmtp_proto::xmtp::mls::message_contents::EncodedContent; + +use prost::Message; + +#[wasm_bindgen] +pub struct WasmGroupMetadata { + inner: GroupMetadata, +} + +#[wasm_bindgen] +impl WasmGroupMetadata { + #[wasm_bindgen] + pub fn creator_inbox_id(&self) -> String { + self.inner.creator_inbox_id.clone() + } + + #[wasm_bindgen] + pub fn conversation_type(&self) -> String { + match self.inner.conversation_type { + ConversationType::Group => "group".to_string(), + ConversationType::Dm => "dm".to_string(), + ConversationType::Sync => "sync".to_string(), + } + } +} + +#[wasm_bindgen] +#[derive(Clone, serde::Serialize)] +pub enum WasmPermissionLevel { + Member, + Admin, + SuperAdmin, +} + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone, serde::Serialize)] +pub struct WasmGroupMember { + pub inbox_id: String, + pub account_addresses: Vec, + pub installation_ids: Vec, + pub permission_level: WasmPermissionLevel, + pub consent_state: WasmConsentState, +} + +#[wasm_bindgen] +pub struct WasmGroup { + inner_client: Arc, + group_id: Vec, + created_at_ns: i64, +} + +impl WasmGroup { + pub fn new(inner_client: Arc, group_id: Vec, created_at_ns: i64) -> Self { + Self { + inner_client, + group_id, + created_at_ns, + } + } +} + +#[wasm_bindgen] +impl WasmGroup { + #[wasm_bindgen] + pub fn id(&self) -> String { + hex::encode(self.group_id.clone()) + } + + #[wasm_bindgen] + pub async fn send(&self, encoded_content: WasmEncodedContent) -> Result { + let encoded_content: EncodedContent = encoded_content.into(); + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let message_id = group + .send_message( + encoded_content.encode_to_vec().as_slice(), + &self.inner_client, + ) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + Ok(hex::encode(message_id.clone())) + } + + /// send a message without immediately publishing to the delivery service. + #[wasm_bindgen] + pub fn send_optimistic(&self, encoded_content: WasmEncodedContent) -> Result { + let encoded_content: EncodedContent = encoded_content.into(); + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let id = group + .send_message_optimistic(encoded_content.encode_to_vec().as_slice()) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(hex::encode(id.clone())) + } + + /// Publish all unpublished messages + #[wasm_bindgen] + pub async fn publish_messages(&self) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + group + .publish_messages(&self.inner_client) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + Ok(()) + } + + #[wasm_bindgen] + pub async fn sync(&self) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .sync(&self.inner_client) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn find_messages( + &self, + opts: Option, + ) -> Result, JsError> { + let opts = match opts { + Some(options) => options, + None => WasmListMessagesOptions { + sent_before_ns: None, + sent_after_ns: None, + limit: None, + delivery_status: None, + }, + }; + + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let delivery_status = opts.delivery_status.map(|status| status.into()); + + let messages: Vec = group + .find_messages( + None, + opts.sent_before_ns, + opts.sent_after_ns, + delivery_status, + opts.limit, + ) + .map_err(|e| JsError::new(&format!("{e}")))? + .into_iter() + .map(|msg| msg.into()) + .collect(); + + Ok(messages) + } + + #[wasm_bindgen] + pub async fn list_members(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let members: Vec = group + .members(&self.inner_client) + .await + .map_err(|e| JsError::new(&format!("{e}")))? + .into_iter() + .map(|member| WasmGroupMember { + inbox_id: member.inbox_id, + account_addresses: member.account_addresses, + installation_ids: member + .installation_ids + .into_iter() + .map(|id| ed25519_public_key_to_address(id.as_slice())) + .collect(), + permission_level: match member.permission_level { + PermissionLevel::Member => WasmPermissionLevel::Member, + PermissionLevel::Admin => WasmPermissionLevel::Admin, + PermissionLevel::SuperAdmin => WasmPermissionLevel::SuperAdmin, + }, + consent_state: member.consent_state.into(), + }) + .collect(); + + Ok(serde_wasm_bindgen::to_value(&members)?) + } + + #[wasm_bindgen] + pub fn admin_list(&self) -> Result, JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let admin_list = group + .admin_list( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(admin_list) + } + + #[wasm_bindgen] + pub fn super_admin_list(&self) -> Result, JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let super_admin_list = group + .super_admin_list( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(super_admin_list) + } + + #[wasm_bindgen] + pub fn is_admin(&self, inbox_id: String) -> Result { + let admin_list = self.admin_list()?; + Ok(admin_list.contains(&inbox_id)) + } + + #[wasm_bindgen] + pub fn is_super_admin(&self, inbox_id: String) -> Result { + let super_admin_list = self.super_admin_list()?; + Ok(super_admin_list.contains(&inbox_id)) + } + + #[wasm_bindgen] + pub async fn add_members(&self, account_addresses: Vec) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .add_members(&self.inner_client, account_addresses) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn add_admin(&self, inbox_id: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + group + .update_admin_list(&self.inner_client, UpdateAdminListType::Add, inbox_id) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn remove_admin(&self, inbox_id: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + group + .update_admin_list(&self.inner_client, UpdateAdminListType::Remove, inbox_id) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn add_super_admin(&self, inbox_id: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + group + .update_admin_list(&self.inner_client, UpdateAdminListType::AddSuper, inbox_id) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn remove_super_admin(&self, inbox_id: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + group + .update_admin_list( + &self.inner_client, + UpdateAdminListType::RemoveSuper, + inbox_id, + ) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn group_permissions(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let permissions = group + .permissions() + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(WasmGroupPermissions::new(permissions)) + } + + #[wasm_bindgen] + pub async fn add_members_by_inbox_id(&self, inbox_ids: Vec) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .add_members_by_inbox_id(&self.inner_client, inbox_ids) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn remove_members(&self, account_addresses: Vec) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .remove_members(&self.inner_client, account_addresses) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn remove_members_by_inbox_id(&self, inbox_ids: Vec) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .remove_members_by_inbox_id(&self.inner_client, inbox_ids) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub async fn update_group_name(&self, group_name: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_name(&self.inner_client, group_name) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn group_name(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_name = group + .group_name( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(group_name) + } + + #[wasm_bindgen] + pub async fn update_group_image_url_square( + &self, + group_image_url_square: String, + ) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_image_url_square(&self.inner_client, group_image_url_square) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn group_image_url_square(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_image_url_square = group + .group_image_url_square( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(group_image_url_square) + } + + #[wasm_bindgen] + pub async fn update_group_description(&self, group_description: String) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_description(&self.inner_client, group_description) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn group_description(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_description = group + .group_description( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(group_description) + } + + #[wasm_bindgen] + pub async fn update_group_pinned_frame_url( + &self, + pinned_frame_url: String, + ) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_group_pinned_frame_url(&self.inner_client, pinned_frame_url) + .await + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } + + #[wasm_bindgen] + pub fn group_pinned_frame_url(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let group_pinned_frame_url = group + .group_pinned_frame_url( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(group_pinned_frame_url) + } + + #[wasm_bindgen] + pub fn created_at_ns(&self) -> i64 { + self.created_at_ns + } + + #[wasm_bindgen] + pub fn is_active(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .is_active( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}"))) + } + + #[wasm_bindgen] + pub fn added_by_inbox_id(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .added_by_inbox_id() + .map_err(|e| JsError::new(&format!("{e}"))) + } + + #[wasm_bindgen] + pub fn group_metadata(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let metadata = group + .metadata( + group + .mls_provider() + .map_err(|e| JsError::new(&format!("{e}")))?, + ) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(WasmGroupMetadata { inner: metadata }) + } + + #[wasm_bindgen] + pub fn consent_state(&self) -> Result { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + let state = group + .consent_state() + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(state.into()) + } + + #[wasm_bindgen] + pub fn update_consent_state(&self, state: WasmConsentState) -> Result<(), JsError> { + let group = MlsGroup::new( + self.inner_client.context().clone(), + self.group_id.clone(), + self.created_at_ns, + ); + + group + .update_consent_state(state.into()) + .map_err(|e| JsError::new(&format!("{e}")))?; + + Ok(()) + } +} diff --git a/bindings_wasm/src/inbox_id.rs b/bindings_wasm/src/inbox_id.rs new file mode 100644 index 000000000..93e5fc22b --- /dev/null +++ b/bindings_wasm/src/inbox_id.rs @@ -0,0 +1,32 @@ +use wasm_bindgen::prelude::{wasm_bindgen, JsError}; +use xmtp_api_http::XmtpHttpApiClient; +use xmtp_id::associations::generate_inbox_id as xmtp_id_generate_inbox_id; +use xmtp_mls::api::ApiClientWrapper; +use xmtp_mls::retry::Retry; + +#[wasm_bindgen(js_name = getInboxIdForAddress)] +pub async fn get_inbox_id_for_address( + host: String, + account_address: String, +) -> Result, JsError> { + let account_address = account_address.to_lowercase(); + let api_client = ApiClientWrapper::new( + XmtpHttpApiClient::new(host.clone()).unwrap(), + Retry::default(), + ); + + let results = api_client + .get_inbox_ids(vec![account_address.clone()]) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + Ok(results.get(&account_address).cloned()) +} + +#[wasm_bindgen(js_name = generateInboxId)] +pub fn generate_inbox_id(account_address: String) -> String { + let account_address = account_address.to_lowercase(); + // ensure that the nonce is always 1 for now since this will only be used for the + // create_client function above, which also has a hard-coded nonce of 1 + xmtp_id_generate_inbox_id(&account_address, &1) +} diff --git a/bindings_wasm/src/inbox_state.rs b/bindings_wasm/src/inbox_state.rs new file mode 100644 index 000000000..0bd55eece --- /dev/null +++ b/bindings_wasm/src/inbox_state.rs @@ -0,0 +1,39 @@ +use wasm_bindgen::prelude::wasm_bindgen; +use xmtp_cryptography::signature::ed25519_public_key_to_address; +use xmtp_id::associations::{AssociationState, MemberIdentifier}; + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct WasmInstallation { + pub id: String, + pub client_timestamp_ns: Option, +} + +#[wasm_bindgen(getter_with_clone)] +pub struct WasmInboxState { + pub inbox_id: String, + pub recovery_address: String, + pub installations: Vec, + pub account_addresses: Vec, +} + +impl From for WasmInboxState { + fn from(state: AssociationState) -> Self { + Self { + inbox_id: state.inbox_id().to_string(), + recovery_address: state.recovery_address().to_string(), + installations: state + .members() + .into_iter() + .filter_map(|m| match m.identifier { + MemberIdentifier::Address(_) => None, + MemberIdentifier::Installation(inst) => Some(WasmInstallation { + id: ed25519_public_key_to_address(inst.as_slice()), + client_timestamp_ns: m.client_timestamp_ns, + }), + }) + .collect(), + account_addresses: state.account_addresses(), + } + } +} diff --git a/bindings_wasm/src/lib.rs b/bindings_wasm/src/lib.rs index 9e31f7a45..7562d1e99 100644 --- a/bindings_wasm/src/lib.rs +++ b/bindings_wasm/src/lib.rs @@ -1 +1,9 @@ +pub mod consent_state; +pub mod conversations; +pub mod encoded_content; +pub mod groups; +pub mod inbox_id; +pub mod inbox_state; +pub mod messages; pub mod mls_client; +pub mod permissions; diff --git a/bindings_wasm/src/messages.rs b/bindings_wasm/src/messages.rs new file mode 100644 index 000000000..936c54ec6 --- /dev/null +++ b/bindings_wasm/src/messages.rs @@ -0,0 +1,102 @@ +use js_sys::Uint8Array; +use prost::Message; +use wasm_bindgen::prelude::wasm_bindgen; +use xmtp_mls::storage::group_message::{DeliveryStatus, GroupMessageKind, StoredGroupMessage}; +use xmtp_proto::xmtp::mls::message_contents::EncodedContent; + +use crate::encoded_content::WasmEncodedContent; + +#[wasm_bindgen] +#[derive(Clone)] +pub enum WasmGroupMessageKind { + Application, + MembershipChange, +} + +impl From for WasmGroupMessageKind { + fn from(kind: GroupMessageKind) -> Self { + match kind { + GroupMessageKind::Application => WasmGroupMessageKind::Application, + GroupMessageKind::MembershipChange => WasmGroupMessageKind::MembershipChange, + } + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub enum WasmDeliveryStatus { + Unpublished, + Published, + Failed, +} + +impl From for WasmDeliveryStatus { + fn from(status: DeliveryStatus) -> Self { + match status { + DeliveryStatus::Unpublished => WasmDeliveryStatus::Unpublished, + DeliveryStatus::Published => WasmDeliveryStatus::Published, + DeliveryStatus::Failed => WasmDeliveryStatus::Failed, + } + } +} + +impl From for DeliveryStatus { + fn from(status: WasmDeliveryStatus) -> Self { + match status { + WasmDeliveryStatus::Unpublished => DeliveryStatus::Unpublished, + WasmDeliveryStatus::Published => DeliveryStatus::Published, + WasmDeliveryStatus::Failed => DeliveryStatus::Failed, + } + } +} + +#[wasm_bindgen(getter_with_clone)] +pub struct WasmListMessagesOptions { + pub sent_before_ns: Option, + pub sent_after_ns: Option, + pub limit: Option, + pub delivery_status: Option, +} + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct WasmMessage { + pub id: String, + pub sent_at_ns: i64, + pub convo_id: String, + pub sender_inbox_id: String, + pub content: WasmEncodedContent, + pub kind: WasmGroupMessageKind, + pub delivery_status: WasmDeliveryStatus, +} + +impl From for WasmMessage { + fn from(msg: StoredGroupMessage) -> Self { + let id = hex::encode(msg.id.clone()); + let convo_id = hex::encode(msg.group_id.clone()); + let contents = msg.decrypted_message_bytes.clone(); + let content: WasmEncodedContent = match EncodedContent::decode(contents.as_slice()) { + Ok(value) => value.into(), + Err(e) => { + println!("Error decoding content: {:?}", e); + WasmEncodedContent { + r#type: None, + parameters: Default::default(), + fallback: None, + compression: None, + content: Uint8Array::new_with_length(0), + } + } + }; + + Self { + id, + sent_at_ns: msg.sent_at_ns, + convo_id, + sender_inbox_id: msg.sender_inbox_id, + content, + kind: msg.kind.into(), + delivery_status: msg.delivery_status.into(), + } + } +} diff --git a/bindings_wasm/src/mls_client.rs b/bindings_wasm/src/mls_client.rs index 6e7594e82..e396afd69 100644 --- a/bindings_wasm/src/mls_client.rs +++ b/bindings_wasm/src/mls_client.rs @@ -1,41 +1,55 @@ use js_sys::Uint8Array; use std::collections::HashMap; use std::sync::Arc; +use tokio::sync::Mutex; use wasm_bindgen::prelude::{wasm_bindgen, JsError}; use wasm_bindgen::JsValue; use xmtp_api_http::XmtpHttpApiClient; use xmtp_cryptography::signature::ed25519_public_key_to_address; -use xmtp_id::associations::{ - generate_inbox_id as xmtp_id_generate_inbox_id, unverified::UnverifiedSignature, AccountId, - MemberIdentifier, -}; -use xmtp_mls::api::ApiClientWrapper; +use xmtp_id::associations::builder::SignatureRequest; +use xmtp_id::associations::unverified::UnverifiedSignature; use xmtp_mls::builder::ClientBuilder; use xmtp_mls::identity::IdentityStrategy; -use xmtp_mls::retry::Retry; +use xmtp_mls::storage::consent_record::StoredConsentRecord; use xmtp_mls::storage::{EncryptedMessageStore, EncryptionKey, StorageOption}; use xmtp_mls::Client as MlsClient; +use crate::consent_state::{WasmConsent, WasmConsentEntityType, WasmConsentState}; +use crate::conversations::WasmConversations; +use crate::inbox_state::WasmInboxState; + pub type RustXmtpClient = MlsClient; +#[wasm_bindgen] +#[derive(Clone, Eq, Hash, PartialEq)] +pub enum WasmSignatureRequestType { + AddWallet, + CreateInbox, + RevokeWallet, + RevokeInstallations, +} + #[wasm_bindgen] pub struct WasmClient { account_address: String, inner_client: Arc, - signatures: HashMap, + signature_requests: Arc>>, } -#[wasm_bindgen] +#[wasm_bindgen(js_name = createClient)] pub async fn create_client( host: String, inbox_id: String, account_address: String, + db_path: String, encryption_key: Option, history_sync_url: Option, ) -> Result { + xmtp_mls::utils::wasm::init().await; let api_client = XmtpHttpApiClient::new(host.clone()).unwrap(); - let storage_option = StorageOption::Ephemeral; + let storage_option = StorageOption::Persistent(db_path); + let store = match encryption_key { Some(key) => { let key: Vec = key.to_vec(); @@ -78,60 +92,33 @@ pub async fn create_client( Ok(WasmClient { account_address, inner_client: Arc::new(xmtp_client), - signatures: HashMap::new(), + signature_requests: Arc::new(Mutex::new(HashMap::new())), }) } -#[wasm_bindgen] -pub async fn get_inbox_id_for_address( - host: String, - account_address: String, -) -> Result, JsError> { - let account_address = account_address.to_lowercase(); - let api_client = ApiClientWrapper::new( - XmtpHttpApiClient::new(host.clone()).unwrap(), - Retry::default(), - ); - - let results = api_client - .get_inbox_ids(vec![account_address.clone()]) - .await - .map_err(|e| JsError::new(format!("{}", e).as_str()))?; - - Ok(results.get(&account_address).cloned()) -} - -#[wasm_bindgen] -pub fn generate_inbox_id(account_address: String) -> String { - let account_address = account_address.to_lowercase(); - // ensure that the nonce is always 1 for now since this will only be used for the - // create_client function above, which also has a hard-coded nonce of 1 - xmtp_id_generate_inbox_id(&account_address, &1) -} - #[wasm_bindgen] impl WasmClient { - #[wasm_bindgen(getter)] + #[wasm_bindgen(getter, js_name = accountAddress)] pub fn account_address(&self) -> String { self.account_address.clone() } - #[wasm_bindgen(getter)] + #[wasm_bindgen(getter, js_name = inboxId)] pub fn inbox_id(&self) -> String { self.inner_client.inbox_id() } - #[wasm_bindgen(getter)] + #[wasm_bindgen(getter, js_name = isRegistered)] pub fn is_registered(&self) -> bool { - self.inner_client.identity().signature_request().is_none() + self.inner_client.identity().is_ready() } - #[wasm_bindgen(getter)] + #[wasm_bindgen(getter, js_name = installationId)] pub fn installation_id(&self) -> String { ed25519_public_key_to_address(self.inner_client.installation_public_key().as_slice()) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = canMessage)] pub async fn can_message(&self, account_addresses: Vec) -> Result { let results: HashMap = self .inner_client @@ -142,79 +129,128 @@ impl WasmClient { Ok(serde_wasm_bindgen::to_value(&results)?) } - #[wasm_bindgen] - pub fn add_ecdsa_signature(&mut self, signature_bytes: Uint8Array) -> Result<(), JsError> { + #[wasm_bindgen(js_name = registerIdentity)] + pub async fn register_identity(&self) -> Result<(), JsError> { if self.is_registered() { return Err(JsError::new( "An identity is already registered with this client", )); } - let signature = UnverifiedSignature::new_recoverable_ecdsa(signature_bytes.to_vec()); + let mut signature_requests = self.signature_requests.lock().await; - self.signatures.insert( - MemberIdentifier::Address(self.account_address.clone().to_lowercase()), - signature, - ); + let signature_request = signature_requests + .get(&WasmSignatureRequestType::CreateInbox) + .ok_or(JsError::new("No signature request found"))?; + + self + .inner_client + .register_identity(signature_request.clone()) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + signature_requests.remove(&WasmSignatureRequestType::CreateInbox); Ok(()) } - #[wasm_bindgen] - pub fn add_scw_signature( - &mut self, - signature_bytes: Uint8Array, - chain_id: u64, - account_address: String, - block_number: u64, - ) -> Result<(), JsError> { - if self.is_registered() { - return Err(JsError::new( - "An identity is already registered with this client", - )); - } + #[wasm_bindgen(js_name = createInboxSignatureText)] + pub async fn create_inbox_signature_text(&self) -> Result, JsError> { + let signature_request = match self.inner_client.identity().signature_request() { + Some(signature_req) => signature_req, + // this should never happen since we're checking for it above in is_registered + None => return Err(JsError::new("No signature request found")), + }; + let signature_text = signature_request.signature_text(); + let mut signature_requests = self.signature_requests.lock().await; - let account_id = AccountId::new_evm(chain_id, account_address.clone()); + signature_requests.insert(WasmSignatureRequestType::CreateInbox, signature_request); - let signature = UnverifiedSignature::new_smart_contract_wallet( - signature_bytes.to_vec(), - account_id, - block_number, - ); + Ok(Some(signature_text)) + } - self.signatures.insert( - MemberIdentifier::Address(account_address.clone().to_lowercase()), - signature, - ); + #[wasm_bindgen(js_name = addWalletSignatureText)] + pub async fn add_wallet_signature_text( + &self, + existing_wallet_address: String, + new_wallet_address: String, + ) -> Result { + let signature_request = self + .inner_client + .associate_wallet( + existing_wallet_address.to_lowercase(), + new_wallet_address.to_lowercase(), + ) + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + let signature_text = signature_request.signature_text(); + let mut signature_requests = self.signature_requests.lock().await; - Ok(()) + signature_requests.insert(WasmSignatureRequestType::AddWallet, signature_request); + + Ok(signature_text) } - #[wasm_bindgen] - pub async fn register_identity(&self) -> Result<(), JsError> { - if self.is_registered() { - return Err(JsError::new( - "An identity is already registered with this client", - )); - } + #[wasm_bindgen(js_name = revokeWalletSignatureText)] + pub async fn revoke_wallet_signature_text( + &self, + wallet_address: String, + ) -> Result { + let signature_request = self + .inner_client + .revoke_wallets(vec![wallet_address.to_lowercase()]) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + let signature_text = signature_request.signature_text(); + let mut signature_requests = self.signature_requests.lock().await; - if self.signatures.is_empty() { - return Err(JsError::new( - "No client signatures found, add at least 1 before registering", - )); - } + signature_requests.insert(WasmSignatureRequestType::RevokeWallet, signature_request); - let mut signature_request = match self.inner_client.identity().signature_request() { - Some(signature_req) => signature_req, - // this should never happen since we're checking for it above in is_registered - None => return Err(JsError::new("No signature request found")), - }; + Ok(signature_text) + } + + #[wasm_bindgen(js_name = revokeInstallationsSignatureText)] + pub async fn revoke_installations_signature_text(&self) -> Result { + let installation_id = self.inner_client.installation_public_key(); + let inbox_state = self + .inner_client + .inbox_state(true) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + let other_installation_ids = inbox_state + .installation_ids() + .into_iter() + .filter(|id| id != &installation_id) + .collect(); + let signature_request = self + .inner_client + .revoke_installations(other_installation_ids) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + let signature_text = signature_request.signature_text(); + let mut signature_requests = self.signature_requests.lock().await; + + signature_requests.insert( + WasmSignatureRequestType::RevokeInstallations, + signature_request, + ); + + Ok(signature_text) + } + + #[wasm_bindgen(js_name = addSignature)] + pub async fn add_signature( + &self, + signature_type: WasmSignatureRequestType, + signature_bytes: Uint8Array, + ) -> Result<(), JsError> { + let mut signature_requests = self.signature_requests.lock().await; + + if let Some(signature_request) = signature_requests.get_mut(&signature_type) { + let signature = UnverifiedSignature::new_recoverable_ecdsa(signature_bytes.to_vec()); - // apply added signatures to the signature request - for signature in self.signatures.values() { signature_request .add_signature( - signature.clone(), + signature, self .inner_client .smart_contract_signature_verifier() @@ -222,27 +258,40 @@ impl WasmClient { ) .await .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + } else { + return Err(JsError::new("Signature request not found")); } - self - .inner_client - .register_identity(signature_request) - .await - .map_err(|e| JsError::new(format!("{}", e).as_str()))?; - Ok(()) } - #[wasm_bindgen] - pub fn signature_text(&self) -> Option { - self - .inner_client - .identity() - .signature_request() - .map(|signature_req| signature_req.signature_text()) + #[wasm_bindgen(js_name = applySignatureRequests)] + pub async fn apply_signature_requests(&self) -> Result<(), JsError> { + let mut signature_requests = self.signature_requests.lock().await; + + let request_types: Vec = signature_requests.keys().cloned().collect(); + for signature_request_type in request_types { + // ignore the create inbox request since it's applied with register_identity + if signature_request_type == WasmSignatureRequestType::CreateInbox { + continue; + } + + if let Some(signature_request) = signature_requests.get(&signature_request_type) { + self + .inner_client + .apply_signature_request(signature_request.clone()) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + // remove the signature request after applying it + signature_requests.remove(&signature_request_type); + } + } + + Ok(()) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = requestHistorySync)] pub async fn request_history_sync(&self) -> Result<(), JsError> { let _ = self .inner_client @@ -253,7 +302,7 @@ impl WasmClient { Ok(()) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = findInboxIdByAddress)] pub async fn find_inbox_id_by_address(&self, address: String) -> Result, JsError> { let inbox_id = self .inner_client @@ -263,4 +312,68 @@ impl WasmClient { Ok(inbox_id) } + + /** + * Get the client's inbox state. + * + * If `refresh_from_network` is true, the client will go to the network first to refresh the state. + * Otherwise, the state will be read from the local database. + */ + #[wasm_bindgen(js_name = inboxState)] + pub async fn inbox_state(&self, refresh_from_network: bool) -> Result { + let state = self + .inner_client + .inbox_state(refresh_from_network) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + Ok(state.into()) + } + + #[wasm_bindgen(js_name = getLatestInboxState)] + pub async fn get_latest_inbox_state(&self, inbox_id: String) -> Result { + let conn = self + .inner_client + .store() + .conn() + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + let state = self + .inner_client + .get_latest_association_state(&conn, &inbox_id) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + Ok(state.into()) + } + + #[wasm_bindgen(js_name = setConsentStates)] + pub async fn set_consent_states(&self, records: Vec) -> Result<(), JsError> { + let inner = self.inner_client.as_ref(); + let stored_records: Vec = + records.into_iter().map(StoredConsentRecord::from).collect(); + + inner + .set_consent_states(stored_records) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + Ok(()) + } + + #[wasm_bindgen(js_name = getConsentState)] + pub async fn get_consent_state( + &self, + entity_type: WasmConsentEntityType, + entity: String, + ) -> Result { + let inner = self.inner_client.as_ref(); + let result = inner + .get_consent_state(entity_type.into(), entity) + .await + .map_err(|e| JsError::new(format!("{}", e).as_str()))?; + + Ok(result.into()) + } + + #[wasm_bindgen] + pub fn conversations(&self) -> WasmConversations { + WasmConversations::new(self.inner_client.clone()) + } } diff --git a/bindings_wasm/src/permissions.rs b/bindings_wasm/src/permissions.rs new file mode 100644 index 000000000..24188ce2c --- /dev/null +++ b/bindings_wasm/src/permissions.rs @@ -0,0 +1,176 @@ +use wasm_bindgen::{prelude::wasm_bindgen, JsError}; +use xmtp_mls::groups::{ + group_mutable_metadata::MetadataField, + group_permissions::{ + BasePolicies, GroupMutablePermissions, MembershipPolicies, MetadataBasePolicies, + MetadataPolicies, PermissionsBasePolicies, PermissionsPolicies, + }, + intents::{PermissionPolicyOption, PermissionUpdateType}, + PreconfiguredPolicies, +}; + +#[wasm_bindgen] +#[derive(Clone)] +pub enum WasmGroupPermissionsOptions { + AllMembers, + AdminOnly, + CustomPolicy, +} + +#[wasm_bindgen] +pub enum WasmPermissionUpdateType { + AddMember, + RemoveMember, + AddAdmin, + RemoveAdmin, + UpdateMetadata, +} + +impl From<&WasmPermissionUpdateType> for PermissionUpdateType { + fn from(update_type: &WasmPermissionUpdateType) -> Self { + match update_type { + WasmPermissionUpdateType::AddMember => PermissionUpdateType::AddMember, + WasmPermissionUpdateType::RemoveMember => PermissionUpdateType::RemoveMember, + WasmPermissionUpdateType::AddAdmin => PermissionUpdateType::AddAdmin, + WasmPermissionUpdateType::RemoveAdmin => PermissionUpdateType::RemoveAdmin, + WasmPermissionUpdateType::UpdateMetadata => PermissionUpdateType::UpdateMetadata, + } + } +} + +#[wasm_bindgen] +#[derive(Clone)] +pub enum WasmPermissionPolicy { + Allow, + Deny, + Admin, + SuperAdmin, + DoesNotExist, + Other, +} + +impl TryInto for WasmPermissionPolicy { + type Error = JsError; + + fn try_into(self) -> Result { + match self { + WasmPermissionPolicy::Allow => Ok(PermissionPolicyOption::Allow), + WasmPermissionPolicy::Deny => Ok(PermissionPolicyOption::Deny), + WasmPermissionPolicy::Admin => Ok(PermissionPolicyOption::AdminOnly), + WasmPermissionPolicy::SuperAdmin => Ok(PermissionPolicyOption::SuperAdminOnly), + _ => Err(JsError::new("InvalidPermissionPolicyOption")), + } + } +} + +impl From<&MembershipPolicies> for WasmPermissionPolicy { + fn from(policies: &MembershipPolicies) -> Self { + if let MembershipPolicies::Standard(base_policy) = policies { + match base_policy { + BasePolicies::Allow => WasmPermissionPolicy::Allow, + BasePolicies::Deny => WasmPermissionPolicy::Deny, + BasePolicies::AllowSameMember => WasmPermissionPolicy::Other, + BasePolicies::AllowIfAdminOrSuperAdmin => WasmPermissionPolicy::Admin, + BasePolicies::AllowIfSuperAdmin => WasmPermissionPolicy::SuperAdmin, + } + } else { + WasmPermissionPolicy::Other + } + } +} + +impl From<&MetadataPolicies> for WasmPermissionPolicy { + fn from(policies: &MetadataPolicies) -> Self { + if let MetadataPolicies::Standard(base_policy) = policies { + match base_policy { + MetadataBasePolicies::Allow => WasmPermissionPolicy::Allow, + MetadataBasePolicies::Deny => WasmPermissionPolicy::Deny, + MetadataBasePolicies::AllowIfActorAdminOrSuperAdmin => WasmPermissionPolicy::Admin, + MetadataBasePolicies::AllowIfActorSuperAdmin => WasmPermissionPolicy::SuperAdmin, + } + } else { + WasmPermissionPolicy::Other + } + } +} + +impl From<&PermissionsPolicies> for WasmPermissionPolicy { + fn from(policies: &PermissionsPolicies) -> Self { + if let PermissionsPolicies::Standard(base_policy) = policies { + match base_policy { + PermissionsBasePolicies::Deny => WasmPermissionPolicy::Deny, + PermissionsBasePolicies::AllowIfActorAdminOrSuperAdmin => WasmPermissionPolicy::Admin, + PermissionsBasePolicies::AllowIfActorSuperAdmin => WasmPermissionPolicy::SuperAdmin, + } + } else { + WasmPermissionPolicy::Other + } + } +} + +#[wasm_bindgen(getter_with_clone)] +pub struct WasmPermissionPolicySet { + pub add_member_policy: WasmPermissionPolicy, + pub remove_member_policy: WasmPermissionPolicy, + pub add_admin_policy: WasmPermissionPolicy, + pub remove_admin_policy: WasmPermissionPolicy, + pub update_group_name_policy: WasmPermissionPolicy, + pub update_group_description_policy: WasmPermissionPolicy, + pub update_group_image_url_square_policy: WasmPermissionPolicy, + pub update_group_pinned_frame_url_policy: WasmPermissionPolicy, +} + +impl From for WasmGroupPermissionsOptions { + fn from(policy: PreconfiguredPolicies) -> Self { + match policy { + PreconfiguredPolicies::AllMembers => WasmGroupPermissionsOptions::AllMembers, + PreconfiguredPolicies::AdminsOnly => WasmGroupPermissionsOptions::AdminOnly, + } + } +} + +#[wasm_bindgen] +pub struct WasmGroupPermissions { + inner: GroupMutablePermissions, +} + +impl WasmGroupPermissions { + pub fn new(permissions: GroupMutablePermissions) -> Self { + Self { inner: permissions } + } +} + +#[wasm_bindgen] +impl WasmGroupPermissions { + #[wasm_bindgen] + #[wasm_bindgen] + pub fn policy_type(&self) -> Result { + if let Ok(preconfigured_policy) = self.inner.preconfigured_policy() { + Ok(preconfigured_policy.into()) + } else { + Ok(WasmGroupPermissionsOptions::CustomPolicy) + } + } + + #[wasm_bindgen] + pub fn policy_set(&self) -> Result { + let policy_set = &self.inner.policies; + let metadata_policy_map = &policy_set.update_metadata_policy; + let get_policy = |field: &str| { + metadata_policy_map + .get(field) + .map(WasmPermissionPolicy::from) + .unwrap_or(WasmPermissionPolicy::DoesNotExist) + }; + Ok(WasmPermissionPolicySet { + add_member_policy: WasmPermissionPolicy::from(&policy_set.add_member_policy), + remove_member_policy: WasmPermissionPolicy::from(&policy_set.remove_member_policy), + add_admin_policy: WasmPermissionPolicy::from(&policy_set.add_admin_policy), + remove_admin_policy: WasmPermissionPolicy::from(&policy_set.remove_admin_policy), + update_group_name_policy: get_policy(MetadataField::GroupName.as_str()), + update_group_description_policy: get_policy(MetadataField::Description.as_str()), + update_group_image_url_square_policy: get_policy(MetadataField::GroupImageUrlSquare.as_str()), + update_group_pinned_frame_url_policy: get_policy(MetadataField::GroupPinnedFrameUrl.as_str()), + }) + } +} diff --git a/bindings_wasm/tests/web.rs b/bindings_wasm/tests/web.rs index fb95ee39c..ba8e66004 100644 --- a/bindings_wasm/tests/web.rs +++ b/bindings_wasm/tests/web.rs @@ -1,4 +1,4 @@ -use bindings_wasm::mls_client::{create_client, get_inbox_id_for_address}; +use bindings_wasm::{inbox_id::get_inbox_id_for_address, mls_client::create_client}; use wasm_bindgen::prelude::*; use wasm_bindgen_test::*; use xmtp_api_http::constants::ApiUrls; @@ -26,6 +26,7 @@ pub async fn test_create_client() { host.clone(), inbox_id.unwrap(), account_address.clone(), + "test".to_string(), None, None, )