diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 0d42d61a5..0256cdc01 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -6,6 +6,7 @@ use std::{ use anyhow::{anyhow, Context as _}; use async_compat::get_runtime_handle; +use futures_util::StreamExt; use matrix_sdk::{ authentication::oauth::{ AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession, @@ -45,8 +46,13 @@ use mime::Mime; use ruma::{ api::client::{alias::get_alias, error::ErrorKind, uiaa::UserIdentifier}, events::{ + direct::DirectEventContent, + fully_read::FullyReadEventContent, + identity_server::IdentityServerEventContent, ignored_user_list::IgnoredUserListEventContent, key::verification::request::ToDeviceKeyVerificationRequestEvent, + marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent}, + push_rules::PushRulesEventContent, room::{ history_visibility::RoomHistoryVisibilityEventContent, join_rules::{ @@ -55,7 +61,13 @@ use ruma::{ message::OriginalSyncRoomMessageEvent, power_levels::RoomPowerLevelsEventContent, }, - GlobalAccountDataEventType, + secret_storage::{ + default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent, + }, + tag::TagEventContent, + GlobalAccountDataEvent as RumaGlobalAccountDataEvent, + GlobalAccountDataEventType as RumaGlobalAccountDataEventType, + RoomAccountDataEvent as RumaRoomAccountDataEvent, }, push::{HttpPusherData as RumaHttpPusherData, PushFormat as RumaPushFormat}, OwnedServerName, RoomAliasId, RoomOrAliasId, ServerName, @@ -76,7 +88,10 @@ use crate::{ room::RoomHistoryVisibility, room_directory_search::RoomDirectorySearch, room_preview::RoomPreview, - ruma::{AuthData, MediaSource}, + ruma::{ + AccountDataEvent, AccountDataEventType, AuthData, MediaSource, RoomAccountDataEvent, + RoomAccountDataEventType, + }, sync_service::{SyncService, SyncServiceBuilder}, task_handle::TaskHandle, utils::AsyncRuntimeDropped, @@ -168,6 +183,20 @@ pub trait SendQueueRoomErrorListener: Sync + Send { fn on_error(&self, room_id: String, error: ClientError); } +/// A listener for changes of global account data events. +#[matrix_sdk_ffi_macros::export(callback_interface)] +pub trait AccountDataListener: Sync + Send { + /// Called when a global account data event has changed. + fn on_change(&self, event: AccountDataEvent); +} + +/// A listener for changes of room account data events. +#[matrix_sdk_ffi_macros::export(callback_interface)] +pub trait RoomAccountDataListener: Sync + Send { + /// Called when a room account data event was changed. + fn on_change(&self, event: RoomAccountDataEvent, room_id: String); +} + #[derive(Clone, Copy, uniffi::Record)] pub struct TransmissionProgress { pub current: u64, @@ -545,6 +574,198 @@ impl Client { }))) } + /// Subscribe to updates of global account data events. + /// + /// Be careful that only the most recent value can be observed. Subscribers + /// are notified when a new value is sent, but there is no guarantee that + /// they will see all values. + pub fn observe_account_data_event( + &self, + event_type: AccountDataEventType, + listener: Box, + ) -> Arc { + match event_type { + AccountDataEventType::Direct => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new( + self.inner + .observe_events::, ()>(), + ); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into()); + } + } + }))) + } + AccountDataEventType::IdentityServer => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_events::, ()>()); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into()); + } + } + }))) + } + AccountDataEventType::IgnoredUserList => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_events::, ()>()); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into()); + } + } + }))) + } + AccountDataEventType::PushRules => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new( + self.inner + .observe_events::, ()>(), + ); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + if let Ok(event) = next.0.try_into() { + listener.on_change(event); + } + } + } + }))) + } + AccountDataEventType::SecretStorageDefaultKey => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_events::, ()>()); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into()); + } + } + }))) + } + AccountDataEventType::SecretStorageKey(key_id) => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_events::, ()>()); + + Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + if next.0.content.key_id != key_id { + continue; + } + if let Ok(event) = next.0.try_into() { + listener.on_change(event); + } + } + } + }))) + } + } + } + + /// Subscribe to updates of room account data events. + /// + /// Be careful that only the most recent value can be observed. Subscribers + /// are notified when a new value is sent, but there is no guarantee that + /// they will see all values. + pub fn observe_room_account_data_event( + &self, + room_id: String, + event_type: RoomAccountDataEventType, + listener: Box, + ) -> Result, ClientError> { + match event_type { + RoomAccountDataEventType::FullyRead => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new( + self.inner + .observe_room_events::, ()>( + &RoomId::parse(&room_id)?, + ), + ); + + Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into(), room_id.clone()); + } + } + })))) + } + RoomAccountDataEventType::MarkedUnread => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_room_events::, ()>(&RoomId::parse( + &room_id, + )?)); + + Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into(), room_id.clone()); + } + } + })))) + } + RoomAccountDataEventType::Tag => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new( + self.inner + .observe_room_events::, ()>( + &RoomId::parse(&room_id)?, + ), + ); + + Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + if let Ok(event) = next.0.try_into() { + listener.on_change(event, room_id.clone()); + } + } + } + })))) + } + RoomAccountDataEventType::UnstableMarkedUnread => { + // Using an Arc here is mandatory or else the subscriber will never trigger + let observer = Arc::new(self.inner.observe_room_events::, ()>(&RoomId::parse( + &room_id, + )?)); + + Ok(Arc::new(TaskHandle::new(get_runtime_handle().spawn(async move { + let mut subscriber = observer.subscribe(); + loop { + if let Some(next) = subscriber.next().await { + listener.on_change(next.0.into(), room_id.clone()); + } + } + })))) + } + } + } + /// Allows generic GET requests to be made through the SDKs internal HTTP /// client pub async fn get_url(&self, url: String) -> Result { @@ -953,7 +1174,7 @@ impl Client { if let Some(raw_content) = self .inner .account() - .fetch_account_data(GlobalAccountDataEventType::IgnoredUserList) + .fetch_account_data(RumaGlobalAccountDataEventType::IgnoredUserList) .await? { let content = raw_content.deserialize_as::()?; diff --git a/bindings/matrix-sdk-ffi/src/notification_settings.rs b/bindings/matrix-sdk-ffi/src/notification_settings.rs index cb2dbff9c..977127c2a 100644 --- a/bindings/matrix-sdk-ffi/src/notification_settings.rs +++ b/bindings/matrix-sdk-ffi/src/notification_settings.rs @@ -161,7 +161,7 @@ pub enum PushCondition { } impl TryFrom for PushCondition { - type Error = (); + type Error = String; fn try_from(value: SdkPushCondition) -> Result { Ok(match value { @@ -179,7 +179,7 @@ impl TryFrom for PushCondition { SdkPushCondition::EventPropertyContains { key, value } => { Self::EventPropertyContains { key, value: value.into() } } - _ => return Err(()), + _ => return Err("Unsupported condition type".to_owned()), }) } } diff --git a/bindings/matrix-sdk-ffi/src/ruma.rs b/bindings/matrix-sdk-ffi/src/ruma.rs index 37ed3d1c4..3e9b0dc44 100644 --- a/bindings/matrix-sdk-ffi/src/ruma.rs +++ b/bindings/matrix-sdk-ffi/src/ruma.rs @@ -12,7 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::BTreeSet, sync::Arc, time::Duration}; +use std::{ + collections::{BTreeSet, HashMap}, + sync::Arc, + time::Duration, +}; use extension_trait::extension_trait; use matrix_sdk::attachment::{BaseAudioInfo, BaseFileInfo, BaseImageInfo, BaseVideoInfo}; @@ -20,8 +24,14 @@ use ruma::{ assign, events::{ call::notify::NotifyType as RumaNotifyType, + direct::DirectEventContent, + fully_read::FullyReadEventContent, + identity_server::IdentityServerEventContent, + ignored_user_list::{IgnoredUser as RumaIgnoredUser, IgnoredUserListEventContent}, location::AssetType as RumaAssetType, + marked_unread::{MarkedUnreadEventContent, UnstableMarkedUnreadEventContent}, poll::start::PollKind as RumaPollKind, + push_rules::PushRulesEventContent, room::{ message::{ AudioInfo as RumaAudioInfo, @@ -43,16 +53,39 @@ use ruma::{ ImageInfo as RumaImageInfo, MediaSource as RumaMediaSource, ThumbnailInfo as RumaThumbnailInfo, }, + secret_storage::{ + default_key::SecretStorageDefaultKeyEventContent, + key::{ + PassPhrase as RumaPassPhrase, + SecretStorageEncryptionAlgorithm as RumaSecretStorageEncryptionAlgorithm, + SecretStorageKeyEventContent, + SecretStorageV1AesHmacSha2Properties as RumaSecretStorageV1AesHmacSha2Properties, + }, + }, + tag::{ + TagEventContent, TagInfo as RumaTagInfo, TagName as RumaTagName, + UserTagName as RumaUserTagName, + }, + GlobalAccountDataEvent as RumaGlobalAccountDataEvent, + GlobalAccountDataEventType as RumaGlobalAccountDataEventType, + RoomAccountDataEvent as RumaRoomAccountDataEvent, + RoomAccountDataEventType as RumaRoomAccountDataEventType, }, matrix_uri::MatrixId as RumaMatrixId, + push::{ + ConditionalPushRule as RumaConditionalPushRule, PatternedPushRule as RumaPatternedPushRule, + Ruleset as RumaRuleset, SimplePushRule as RumaSimplePushRule, + }, serde::JsonObject, - MatrixToUri, MatrixUri as RumaMatrixUri, OwnedUserId, UInt, UserId, + KeyDerivationAlgorithm as RumaKeyDerivationAlgorithm, MatrixToUri, MatrixUri as RumaMatrixUri, + OwnedRoomId, OwnedUserId, UInt, UserId, }; use tracing::info; use crate::{ error::{ClientError, MediaInfoError}, helpers::unwrap_or_clone_arc, + notification_settings::{Action, PushCondition}, timeline::MessageContent, utils::u64_to_uint, }; @@ -985,3 +1018,591 @@ pub fn content_without_relation_from_message( let msg_type = message.msg_type.try_into()?; Ok(Arc::new(RoomMessageEventContentWithoutRelation::new(msg_type))) } + +/// Types of global account data events. +#[derive(Clone, uniffi::Enum)] +pub enum AccountDataEventType { + /// m.direct + Direct, + /// m.identity_server + IdentityServer, + /// m.ignored_user_list + IgnoredUserList, + /// m.push_rules + PushRules, + /// m.secret_storage.default_key + SecretStorageDefaultKey, + /// m.secret_storage.key.* + SecretStorageKey(String), +} + +impl TryFrom for AccountDataEventType { + type Error = String; + + fn try_from(value: RumaGlobalAccountDataEventType) -> Result { + match value { + RumaGlobalAccountDataEventType::Direct => Ok(Self::Direct), + RumaGlobalAccountDataEventType::IdentityServer => Ok(Self::IdentityServer), + RumaGlobalAccountDataEventType::IgnoredUserList => Ok(Self::IgnoredUserList), + RumaGlobalAccountDataEventType::PushRules => Ok(Self::PushRules), + RumaGlobalAccountDataEventType::SecretStorageDefaultKey => { + Ok(Self::SecretStorageDefaultKey) + } + RumaGlobalAccountDataEventType::SecretStorageKey(key_id) => { + Ok(Self::SecretStorageKey(key_id)) + } + _ => Err("Unsupported account data event type".to_owned()), + } + } +} + +/// Global account data events. +#[derive(Clone, uniffi::Enum)] +pub enum AccountDataEvent { + /// m.direct + Direct { + /// The mapping of user ID to a list of room IDs of the ‘direct’ rooms + /// for that user ID. + map: HashMap>, + }, + /// m.identity_server + IdentityServer { + /// The base URL for the identity server for client-server connections. + base_url: Option, + }, + /// m.ignored_user_list + IgnoredUserList { + /// The map of users to ignore. This is a mapping of user ID to empty + /// object. + ignored_users: HashMap, + }, + /// m.push_rules + PushRules { + /// The global ruleset. + global: Ruleset, + }, + /// m.secret_storage.default_key + SecretStorageDefaultKey { + /// The ID of the default key. + key_id: String, + }, + /// m.secret_storage.key.* + SecretStorageKey { + /// The ID of the key. + key_id: String, + + /// The name of the key. + name: Option, + + /// The encryption algorithm used for this key. + /// + /// Currently, only `m.secret_storage.v1.aes-hmac-sha2` is supported. + algorithm: SecretStorageEncryptionAlgorithm, + + /// The passphrase from which to generate the key. + passphrase: Option, + }, +} + +/// Details about an ignored user. +/// +/// This is currently empty. +#[derive(Clone, uniffi::Record)] +pub struct IgnoredUser {} + +impl From for IgnoredUser { + fn from(_value: RumaIgnoredUser) -> Self { + IgnoredUser {} + } +} + +/// A push ruleset scopes a set of rules according to some criteria. +#[derive(Clone, uniffi::Record)] +pub struct Ruleset { + /// These rules configure behavior for (unencrypted) messages that match + /// certain patterns. + pub content: Vec, + + /// These user-configured rules are given the highest priority. + /// + /// This field is named `override_` instead of `override` because the latter + /// is a reserved keyword in Rust. + pub override_: Vec, + + /// These rules change the behavior of all messages for a given room. + pub room: Vec, + + /// These rules configure notification behavior for messages from a specific + /// Matrix user ID. + pub sender: Vec, + + /// These rules are identical to override rules, but have a lower priority + /// than `content`, `room` and `sender` rules. + pub underride: Vec, +} + +impl TryFrom for Ruleset { + type Error = String; + + fn try_from(value: RumaRuleset) -> Result { + Ok(Self { + content: value + .content + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + override_: value + .override_ + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + room: value.room.into_iter().map(TryInto::try_into).collect::, _>>()?, + sender: value + .sender + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + underride: value + .underride + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +/// Like [`SimplePushRule`], but with an additional `pattern`` field. +#[derive(Clone, uniffi::Record)] +pub struct PatternedPushRule { + /// Actions to determine if and how a notification is delivered for events + /// matching this rule. + pub actions: Vec, + + /// Whether this is a default rule, or has been set explicitly. + pub default: bool, + + /// Whether the push rule is enabled or not. + pub enabled: bool, + + /// The ID of this rule. + pub rule_id: String, + + /// The glob-style pattern to match against. + pub pattern: String, +} + +impl TryFrom for PatternedPushRule { + type Error = String; + + fn try_from(value: RumaPatternedPushRule) -> Result { + Ok(Self { + actions: value + .actions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + default: value.default, + enabled: value.enabled, + rule_id: value.rule_id, + pattern: value.pattern, + }) + } +} + +/// Like [`SimplePushRule`], but with an additional `conditions` field. +#[derive(Clone, uniffi::Record)] +pub struct ConditionalPushRule { + /// Actions to determine if and how a notification is delivered for events + /// matching this rule. + pub actions: Vec, + + /// Whether this is a default rule, or has been set explicitly. + pub default: bool, + + /// Whether the push rule is enabled or not. + pub enabled: bool, + + /// The ID of this rule. + pub rule_id: String, + + /// The conditions that must hold true for an event in order for a rule to + /// be applied to an event. + /// + /// A rule with no conditions always matches. + pub conditions: Vec, +} + +impl TryFrom for ConditionalPushRule { + type Error = String; + + fn try_from(value: RumaConditionalPushRule) -> Result { + Ok(Self { + actions: value + .actions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + default: value.default, + enabled: value.enabled, + rule_id: value.rule_id, + conditions: value + .conditions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + }) + } +} + +/// A push rule is a single rule that states under what conditions an event +/// should be passed onto a push gateway and how the notification should be +/// presented. +#[derive(Clone, uniffi::Record)] +pub struct SimplePushRule { + /// Actions to determine if and how a notification is delivered for events + /// matching this rule. + pub actions: Vec, + + /// Whether this is a default rule, or has been set explicitly. + pub default: bool, + + /// Whether the push rule is enabled or not. + pub enabled: bool, + + /// The ID of this rule. + /// + /// This is generally the Matrix ID of the entity that it applies to. + pub rule_id: String, +} + +impl TryFrom> for SimplePushRule { + type Error = String; + + fn try_from(value: RumaSimplePushRule) -> Result { + Ok(Self { + actions: value + .actions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + default: value.default, + enabled: value.enabled, + rule_id: value.rule_id.into(), + }) + } +} + +impl TryFrom> for SimplePushRule { + type Error = String; + + fn try_from(value: RumaSimplePushRule) -> Result { + Ok(Self { + actions: value + .actions + .into_iter() + .map(TryInto::try_into) + .collect::, _>>()?, + default: value.default, + enabled: value.enabled, + rule_id: value.rule_id.into(), + }) + } +} + +/// An algorithm and its properties, used to encrypt a secret. +#[derive(Clone, uniffi::Enum)] +pub enum SecretStorageEncryptionAlgorithm { + /// Encrypted using the `m.secret_storage.v1.aes-hmac-sha2` algorithm. + /// + /// Secrets using this method are encrypted using AES-CTR-256 and + /// authenticated using HMAC-SHA-256. + V1AesHmacSha2(SecretStorageV1AesHmacSha2Properties), +} + +impl TryFrom for SecretStorageEncryptionAlgorithm { + type Error = String; + + fn try_from(value: RumaSecretStorageEncryptionAlgorithm) -> Result { + match value { + RumaSecretStorageEncryptionAlgorithm::V1AesHmacSha2(properties) => { + Ok(Self::V1AesHmacSha2(properties.into())) + } + _ => Err("Unsupported encryption algorithm".to_owned()), + } + } +} + +/// The key properties for the `m.secret_storage.v1.aes-hmac-sha2`` algorithm. +#[derive(Clone, uniffi::Record)] +pub struct SecretStorageV1AesHmacSha2Properties { + /// The 16-byte initialization vector, encoded as base64. + pub iv: Option, + + /// The MAC, encoded as base64. + pub mac: Option, +} + +impl From for SecretStorageV1AesHmacSha2Properties { + fn from(value: RumaSecretStorageV1AesHmacSha2Properties) -> Self { + Self { + iv: value.iv.map(|base64| base64.encode()), + mac: value.mac.map(|base64| base64.encode()), + } + } +} + +/// A passphrase from which a key is to be derived. +#[derive(Clone, uniffi::Record)] +pub struct PassPhrase { + /// The algorithm to use to generate the key from the passphrase. + /// + /// Must be `m.pbkdf2`. + pub algorithm: KeyDerivationAlgorithm, + + /// The salt used in PBKDF2. + pub salt: String, + + /// The number of iterations to use in PBKDF2. + pub iterations: u64, + + /// The number of bits to generate for the key. + /// + /// Defaults to 256 + pub bits: u64, +} + +impl TryFrom for PassPhrase { + type Error = String; + + fn try_from(value: RumaPassPhrase) -> Result { + Ok(PassPhrase { + algorithm: value.algorithm.try_into()?, + salt: value.salt, + iterations: value.iterations.into(), + bits: value.bits.into(), + }) + } +} + +/// A key algorithm to be used to generate a key from a passphrase. +#[derive(Clone, uniffi::Enum)] +pub enum KeyDerivationAlgorithm { + /// PBKDF2 + Pbkfd2, +} + +impl TryFrom for KeyDerivationAlgorithm { + type Error = String; + + fn try_from(value: RumaKeyDerivationAlgorithm) -> Result { + match value { + RumaKeyDerivationAlgorithm::Pbkfd2 => Ok(Self::Pbkfd2), + _ => Err("Unsupported key derivation algorithm".to_owned()), + } + } +} + +impl From> for AccountDataEvent { + fn from(value: RumaGlobalAccountDataEvent) -> Self { + Self::Direct { + map: value + .content + .0 + .into_iter() + .map(|(user_id, room_ids)| { + (user_id.to_string(), room_ids.iter().map(ToString::to_string).collect()) + }) + .collect(), + } + } +} + +impl From> for AccountDataEvent { + fn from(value: RumaGlobalAccountDataEvent) -> Self { + Self::IdentityServer { base_url: value.content.base_url.into_option() } + } +} + +impl From> for AccountDataEvent { + fn from(value: RumaGlobalAccountDataEvent) -> Self { + Self::IgnoredUserList { + ignored_users: value + .content + .ignored_users + .into_iter() + .map(|(user_id, ignored_user)| { + (user_id.to_string(), IgnoredUser::from(ignored_user)) + }) + .collect(), + } + } +} + +impl TryFrom> for AccountDataEvent { + type Error = String; + + fn try_from( + value: RumaGlobalAccountDataEvent, + ) -> Result { + Ok(Self::PushRules { global: value.content.global.try_into()? }) + } +} + +impl From> for AccountDataEvent { + fn from(value: RumaGlobalAccountDataEvent) -> Self { + Self::SecretStorageDefaultKey { key_id: value.content.key_id } + } +} + +impl TryFrom> for AccountDataEvent { + type Error = String; + + fn try_from( + value: RumaGlobalAccountDataEvent, + ) -> Result { + Ok(Self::SecretStorageKey { + key_id: value.content.key_id, + name: value.content.name, + algorithm: value.content.algorithm.try_into()?, + passphrase: value.content.passphrase.map(TryInto::try_into).transpose()?, + }) + } +} + +/// Types of room account data events. +#[derive(Clone, uniffi::Enum)] +pub enum RoomAccountDataEventType { + /// m.fully_read + FullyRead, + /// m.marked_unread + MarkedUnread, + /// m.tag + Tag, + /// com.famedly.marked_unread + UnstableMarkedUnread, +} + +impl TryFrom for RoomAccountDataEventType { + type Error = String; + + fn try_from(value: RumaRoomAccountDataEventType) -> Result { + match value { + RumaRoomAccountDataEventType::FullyRead => Ok(Self::FullyRead), + RumaRoomAccountDataEventType::MarkedUnread => Ok(Self::MarkedUnread), + RumaRoomAccountDataEventType::Tag => Ok(Self::Tag), + RumaRoomAccountDataEventType::UnstableMarkedUnread => Ok(Self::UnstableMarkedUnread), + _ => Err("Unsupported account data event type".to_owned()), + } + } +} + +/// Room account data events. +#[derive(Clone, uniffi::Enum)] +pub enum RoomAccountDataEvent { + /// m.fully_read + FullyReadEvent { + /// The event the user's read marker is located at in the room. + event_id: String, + }, + /// m.marked_unread + MarkedUnread { + /// The current unread state. + unread: bool, + }, + /// m.tag + Tag { tags: HashMap }, + /// com.famedly.marked_unread + UnstableMarkedUnread { + /// The current unread state. + unread: bool, + }, +} + +/// The name of a tag. +#[derive(Clone, PartialEq, Eq, Hash, uniffi::Enum)] +pub enum TagName { + /// `m.favourite`: The user's favorite rooms. + Favorite, + + /// `m.lowpriority`: These should be shown with lower precedence than + /// others. + LowPriority, + + /// `m.server_notice`: Used to identify + ServerNotice, + + /// `u.*`: User-defined tag + User(UserTagName), +} + +impl TryFrom for TagName { + type Error = String; + + fn try_from(value: RumaTagName) -> Result { + match value { + RumaTagName::Favorite => Ok(Self::Favorite), + RumaTagName::LowPriority => Ok(Self::LowPriority), + RumaTagName::ServerNotice => Ok(Self::ServerNotice), + RumaTagName::User(name) => Ok(Self::User(name.into())), + _ => Err("Unsupported tag name".to_owned()), + } + } +} + +/// A user-defined tag name. +#[derive(Clone, PartialEq, Eq, Hash, uniffi::Record)] +pub struct UserTagName { + name: String, +} + +impl From for UserTagName { + fn from(value: RumaUserTagName) -> Self { + Self { name: value.as_ref().to_owned() } + } +} + +/// Information about a tag. +#[derive(Clone, uniffi::Record)] +pub struct TagInfo { + /// Value to use for lexicographically ordering rooms with this tag. + pub order: Option, +} + +impl From for TagInfo { + fn from(value: RumaTagInfo) -> Self { + Self { order: value.order } + } +} + +impl From> for RoomAccountDataEvent { + fn from(value: RumaRoomAccountDataEvent) -> Self { + Self::FullyReadEvent { event_id: value.content.event_id.into() } + } +} + +impl From> for RoomAccountDataEvent { + fn from(value: RumaRoomAccountDataEvent) -> Self { + Self::MarkedUnread { unread: value.content.unread } + } +} + +impl TryFrom> for RoomAccountDataEvent { + type Error = String; + + fn try_from(value: RumaRoomAccountDataEvent) -> Result { + Ok(Self::Tag { + tags: value + .content + .tags + .into_iter() + .map(|(name, info)| name.try_into().map(|name| (name, info.into()))) + .collect::, _>>()?, + }) + } +} + +impl From> for RoomAccountDataEvent { + fn from(value: RumaRoomAccountDataEvent) -> Self { + Self::UnstableMarkedUnread { unread: value.content.unread } + } +}