diff --git a/crates/matrix-sdk/src/client/mod.rs b/crates/matrix-sdk/src/client/mod.rs index 2b80e3ed9..7706dad3e 100644 --- a/crates/matrix-sdk/src/client/mod.rs +++ b/crates/matrix-sdk/src/client/mod.rs @@ -62,8 +62,10 @@ use ruma::{ error::FromHttpResponseError, MatrixVersion, OutgoingRequest, }, - assign, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, - RoomOrAliasId, ServerName, UInt, UserId, + assign, + push::Ruleset, + DeviceId, OwnedDeviceId, OwnedRoomId, OwnedServerName, RoomAliasId, RoomId, RoomOrAliasId, + ServerName, UInt, UserId, }; use serde::de::DeserializeOwned; use tokio::sync::{broadcast, Mutex, OnceCell, RwLock, RwLockReadGuard}; @@ -81,6 +83,7 @@ use crate::{ }, http_client::HttpClient, matrix_auth::MatrixAuth, + notification_settings::NotificationSettings, room, sync::{RoomUpdate, SyncResponse}, Account, AuthApi, AuthSession, Error, Media, RefreshTokenError, Result, TransmissionProgress, @@ -1926,6 +1929,12 @@ impl Client { let request = get_profile::v3::Request::new(user_id.to_owned()); Ok(self.send(request, Some(RequestConfig::short_retry())).await?) } + + /// Get the notification settings of the current owner of the client. + pub async fn notification_settings(&self) -> NotificationSettings { + let ruleset = self.account().push_rules().await.unwrap_or_else(|_| Ruleset::new()); + NotificationSettings::new(self.clone(), ruleset) + } } // The http mocking library is not supported for wasm32 diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 5afe9ad52..6092f2257 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -30,7 +30,7 @@ use ruma::{ error::{FromHttpResponseError, IntoHttpError}, }, events::tag::InvalidUserTagName, - push::{InsertPushRuleError, RuleNotFoundError}, + push::{InsertPushRuleError, RemovePushRuleError, RuleNotFoundError}, IdParseError, }; use serde_json::Error as JsonError; @@ -430,12 +430,21 @@ pub enum NotificationSettingsError { /// Invalid parameter. #[error("Invalid parameter `{0}`")] InvalidParameter(String), - /// Rule not found - #[error("Rule not found")] - RuleNotFound, /// Unable to add push rule. #[error("Unable to add push rule")] UnableToAddPushRule, + /// Unable to remove push rule. + #[error("Unable to remove push rule")] + UnableToRemovePushRule, + /// Unable to update push rule. + #[error("Unable to update push rule")] + UnableToUpdatePushRule, + /// Rule not found + #[error("Rule not found")] + RuleNotFound, + /// Unable to save the push rules + #[error("Unable to save push rules")] + UnableToSavePushRules, } impl From for NotificationSettingsError { @@ -444,6 +453,12 @@ impl From for NotificationSettingsError { } } +impl From for NotificationSettingsError { + fn from(_: RemovePushRuleError) -> Self { + Self::UnableToRemovePushRule + } +} + impl From for NotificationSettingsError { fn from(_: RuleNotFoundError) -> Self { Self::RuleNotFound diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 02fccc89b..2c44d2923 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -56,7 +56,10 @@ pub use authentication::{AuthApi, AuthSession}; pub use client::{Client, ClientBuildError, ClientBuilder, LoopCtrl, SendRequest, UnknownToken}; #[cfg(feature = "image-proc")] pub use error::ImageError; -pub use error::{Error, HttpError, HttpResult, RefreshTokenError, Result, RumaApiError}; +pub use error::{ + Error, HttpError, HttpResult, NotificationSettingsError, RefreshTokenError, Result, + RumaApiError, +}; pub use http_client::TransmissionProgress; pub use media::Media; pub use ruma::{IdParseError, OwnedServerName, ServerName}; diff --git a/crates/matrix-sdk/src/notification_settings/command.rs b/crates/matrix-sdk/src/notification_settings/command.rs new file mode 100644 index 000000000..1cbef1dd5 --- /dev/null +++ b/crates/matrix-sdk/src/notification_settings/command.rs @@ -0,0 +1,65 @@ +use std::fmt::Debug; + +use ruma::{ + api::client::push::RuleScope, + push::{ + Action, NewConditionalPushRule, NewPushRule, NewSimplePushRule, PushCondition, RuleKind, + Tweak, + }, + OwnedRoomId, +}; + +use crate::NotificationSettingsError; + +/// Enum describing the commands required to modify the owner's account data. +#[derive(Clone, Debug)] +pub(crate) enum Command { + /// Set a new `Room` push rule + SetRoomPushRule { scope: RuleScope, room_id: OwnedRoomId, notify: bool }, + /// Set a new `Override` push rule matching a `RoomId` + SetOverridePushRule { scope: RuleScope, rule_id: String, room_id: OwnedRoomId, notify: bool }, + /// Set whether a push rule is enabled + SetPushRuleEnabled { scope: RuleScope, kind: RuleKind, rule_id: String, enabled: bool }, + /// Delete a push rule + DeletePushRule { scope: RuleScope, kind: RuleKind, rule_id: String }, +} + +fn get_notify_actions(notify: bool) -> Vec { + if notify { + vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))] + } else { + vec![] + } +} + +impl Command { + /// Tries to create a push rule corresponding to this command + pub(crate) fn to_push_rule(&self) -> Result { + match self { + Self::SetRoomPushRule { scope: _, room_id, notify } => { + // `Room` push rule for this `room_id` + let new_rule = NewSimplePushRule::new(room_id.clone(), get_notify_actions(*notify)); + Ok(NewPushRule::Room(new_rule)) + } + + Self::SetOverridePushRule { scope: _, rule_id, room_id, notify } => { + // `Override` push rule matching this `room_id` + let new_rule = NewConditionalPushRule::new( + rule_id.clone(), + vec![PushCondition::EventMatch { + key: "room_id".to_owned(), + pattern: room_id.to_string(), + }], + get_notify_actions(*notify), + ); + Ok(NewPushRule::Override(new_rule)) + } + + Self::SetPushRuleEnabled { .. } | Self::DeletePushRule { .. } => { + Err(NotificationSettingsError::InvalidParameter( + "cannot create a push rule from this command.".to_owned(), + )) + } + } + } +} diff --git a/crates/matrix-sdk/src/notification_settings/mod.rs b/crates/matrix-sdk/src/notification_settings/mod.rs index cd835dc58..0eccf4e48 100644 --- a/crates/matrix-sdk/src/notification_settings/mod.rs +++ b/crates/matrix-sdk/src/notification_settings/mod.rs @@ -1,7 +1,23 @@ //! High-level push notification settings API +use std::sync::Arc; + +use ruma::{ + api::client::push::{delete_pushrule, set_pushrule, set_pushrule_enabled}, + events::push_rules::PushRulesEvent, + push::{RuleKind, Ruleset}, + RoomId, +}; +use tokio::sync::RwLock; + +use self::{command::Command, rule_commands::RuleCommands, rules::Rules}; + +mod command; +mod rule_commands; mod rules; +use crate::{error::NotificationSettingsError, event_handler::EventHandlerHandle, Client, Result}; + /// Enum representing the push notification modes for a room. #[derive(Debug, Clone, PartialEq)] pub enum RoomNotificationMode { @@ -12,3 +28,804 @@ pub enum RoomNotificationMode { /// Do not receive any notifications. Mute, } + +/// A high-level API to manage the client owner's push notification settings. +#[derive(Debug, Clone)] +pub struct NotificationSettings { + /// The underlying HTTP client. + client: Client, + /// Owner's account push rules. They will be updated on sync. + rules: Arc>, + /// Event handler for push rules event + push_rules_event_handler: EventHandlerHandle, +} + +impl Drop for NotificationSettings { + fn drop(&mut self) { + self.client.remove_event_handler(self.push_rules_event_handler.clone()); + } +} + +impl NotificationSettings { + /// Build a new `NotificationSettings`` + /// + /// # Arguments + /// + /// * `client` - A `Client` used to perform API calls + /// * `ruleset` - A `Ruleset` containing account's owner push rules + pub fn new(client: Client, ruleset: Ruleset) -> Self { + let rules = Arc::new(RwLock::new(Rules::new(ruleset))); + + // Listen for PushRulesEvent + let rules_clone = rules.clone(); + let push_rules_event_handler = client.add_event_handler(move |ev: PushRulesEvent| { + let rules = rules_clone.to_owned(); + async move { + *rules.write().await = Rules::new(ev.content.global); + } + }); + + Self { client, rules, push_rules_event_handler } + } + + /// Gets the user defined notification mode for a room. + pub async fn get_user_defined_room_notification_mode( + &self, + room_id: &RoomId, + ) -> Option { + self.rules.read().await.get_user_defined_room_notification_mode(room_id) + } + + /// Gets the default notification mode for a room. + /// + /// # Arguments + /// + /// * `is_encrypted` - `true` if the room is encrypted + /// * `members_count` - the room members count + pub async fn get_default_room_notification_mode( + &self, + is_encrypted: bool, + members_count: u64, + ) -> RoomNotificationMode { + self.rules.read().await.get_default_room_notification_mode(is_encrypted, members_count) + } + + /// Get whether the given ruleset contains some enabled keywords rules. + pub async fn contains_keyword_rules(&self) -> bool { + self.rules.read().await.contains_keyword_rules() + } + + /// Get whether a push rule is enabled. + pub async fn is_push_rule_enabled( + &self, + kind: RuleKind, + rule_id: &str, + ) -> Result { + self.rules.read().await.is_enabled(kind, rule_id) + } + + /// Set whether a push rule is enabled. + pub async fn set_push_rule_enabled( + &self, + kind: RuleKind, + rule_id: &str, + enabled: bool, + ) -> Result<(), NotificationSettingsError> { + let rules = self.rules.read().await.clone(); + + let mut rule_commands = RuleCommands::new(rules.ruleset); + rule_commands.set_rule_enabled(kind, rule_id, enabled)?; + + self.run_server_commands(&rule_commands).await?; + + let rules = &mut *self.rules.write().await; + rules.apply(rule_commands); + + Ok(()) + } + + /// Sets the notification mode for a room. + pub async fn set_room_notification_mode( + &self, + room_id: &RoomId, + mode: RoomNotificationMode, + ) -> Result<(), NotificationSettingsError> { + let rules = self.rules.read().await.clone(); + + // Check that the current mode is not already the target mode. + if rules.get_user_defined_room_notification_mode(room_id) == Some(mode.clone()) { + return Ok(()); + } + + // Build the command list to set the new mode + let (new_rule_kind, notify) = match mode { + RoomNotificationMode::AllMessages => { + // insert a `Room` rule which notifies + (RuleKind::Room, true) + } + RoomNotificationMode::MentionsAndKeywordsOnly => { + // insert a `Room` rule which doesn't notify + (RuleKind::Room, false) + } + RoomNotificationMode::Mute => { + // insert an `Override` rule which doesn't notify + (RuleKind::Override, false) + } + }; + + // Extract all the custom rules except the one we just created. + let new_rule_id = room_id.as_str(); + let custom_rules: Vec<(RuleKind, String)> = rules + .get_custom_rules_for_room(room_id) + .into_iter() + .filter(|(kind, rule_id)| kind != &new_rule_kind || rule_id != new_rule_id) + .collect(); + + // Build the command list to delete all other custom rules, with the exception + // of the newly inserted rule. + let mut rule_commands = RuleCommands::new(rules.ruleset); + rule_commands.insert_rule(new_rule_kind.clone(), room_id, notify)?; + for (kind, rule_id) in custom_rules { + rule_commands.delete_rule(kind, rule_id)?; + } + + self.run_server_commands(&rule_commands).await?; + + let rules = &mut *self.rules.write().await; + rules.apply(rule_commands); + + Ok(()) + } + + /// Delete all user defined rules for a room. + pub async fn delete_user_defined_room_rules( + &self, + room_id: &RoomId, + ) -> Result<(), NotificationSettingsError> { + let rules = self.rules.read().await.clone(); + + let custom_rules = rules.get_custom_rules_for_room(room_id); + if custom_rules.is_empty() { + return Ok(()); + } + + let mut rule_commands = RuleCommands::new(rules.ruleset); + for (kind, rule_id) in custom_rules { + rule_commands.delete_rule(kind, rule_id)?; + } + + self.run_server_commands(&rule_commands).await?; + + let rules = &mut *self.rules.write().await; + rules.apply(rule_commands); + + Ok(()) + } + + /// Unmute a room. + pub async fn unmute_room( + &self, + room_id: &RoomId, + is_encrypted: bool, + members_count: u64, + ) -> Result<(), NotificationSettingsError> { + let rules = self.rules.read().await.clone(); + + // Check if there is a user defined mode + if let Some(room_mode) = rules.get_user_defined_room_notification_mode(room_id) { + if room_mode != RoomNotificationMode::Mute { + // Already unmuted + return Ok(()); + } + + // Get default mode for this room + let default_mode = + rules.get_default_room_notification_mode(is_encrypted, members_count); + + // If the default mode is `Mute`, set it to `AllMessages` + if default_mode == RoomNotificationMode::Mute { + self.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await + } else { + // Otherwise, delete user defined rules to use the default mode + self.delete_user_defined_room_rules(room_id).await + } + } else { + // This is the default mode, create a custom rule to unmute this room by setting + // the mode to `AllMessages` + self.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await + } + } + + /// Convert commands into requests to the server, and run them. + async fn run_server_commands( + &self, + rule_commands: &RuleCommands, + ) -> Result<(), NotificationSettingsError> { + for command in &rule_commands.commands { + match command { + Command::DeletePushRule { scope, kind, rule_id } => { + let request = delete_pushrule::v3::Request::new( + scope.to_owned(), + kind.to_owned(), + rule_id.to_owned(), + ); + self.client + .send(request, None) + .await + .map_err(|_| NotificationSettingsError::UnableToRemovePushRule)?; + } + Command::SetRoomPushRule { scope, room_id: _, notify: _ } => { + let push_rule = command.to_push_rule()?; + let request = set_pushrule::v3::Request::new(scope.to_owned(), push_rule); + self.client + .send(request, None) + .await + .map_err(|_| NotificationSettingsError::UnableToAddPushRule)?; + } + Command::SetOverridePushRule { scope, rule_id: _, room_id: _, notify: _ } => { + let push_rule = command.to_push_rule()?; + let request = set_pushrule::v3::Request::new(scope.to_owned(), push_rule); + self.client + .send(request, None) + .await + .map_err(|_| NotificationSettingsError::UnableToAddPushRule)?; + } + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + let request = set_pushrule_enabled::v3::Request::new( + scope.to_owned(), + kind.to_owned(), + rule_id.to_owned(), + *enabled, + ); + self.client + .send(request, None) + .await + .map_err(|_| NotificationSettingsError::UnableToUpdatePushRule)?; + } + } + } + Ok(()) + } +} + +// The http mocking library is not supported for wasm32 +#[cfg(all(test, not(target_arch = "wasm32")))] +mod tests { + use assert_matches::assert_matches; + use matrix_sdk_test::async_test; + use ruma::{ + push::{ + Action, AnyPushRuleRef, NewPatternedPushRule, NewPushRule, PredefinedOverrideRuleId, + PredefinedUnderrideRuleId, RuleKind, Ruleset, + }, + OwnedRoomId, RoomId, UserId, + }; + use wiremock::{http::Method, matchers::method, Mock, MockServer, ResponseTemplate}; + + use super::rule_commands::RuleCommands; + use crate::{ + error::NotificationSettingsError, + notification_settings::{NotificationSettings, RoomNotificationMode}, + test_utils::logged_in_client, + Client, + }; + + fn get_test_room_id() -> OwnedRoomId { + RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap() + } + + fn get_server_default_ruleset() -> Ruleset { + let user_id = UserId::parse("@user:matrix.org").unwrap(); + Ruleset::server_default(&user_id) + } + + fn from_insert_rules( + client: &Client, + rules: Vec<(RuleKind, &RoomId, bool)>, + ) -> NotificationSettings { + let ruleset = get_server_default_ruleset(); + // XXX would be slightly better to only use `Ruleset` here too, and no + // `RuleCommands` so we're testing things more in isolation. + let mut rule_commands = RuleCommands::new(ruleset); + for (kind, room_id, notify) in rules { + rule_commands.insert_rule(kind, room_id, notify).unwrap(); + } + NotificationSettings::new(client.to_owned(), rule_commands.rules) + } + + async fn get_custom_rules_for_room( + settings: &NotificationSettings, + room_id: &RoomId, + ) -> Vec<(RuleKind, String)> { + settings.rules.read().await.get_custom_rules_for_room(room_id) + } + + #[async_test] + async fn test_get_custom_rules_for_room() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]); + + let custom_rules = get_custom_rules_for_room(&settings, &room_id).await; + assert_eq!(custom_rules.len(), 1); + assert_eq!(custom_rules[0], (RuleKind::Room, room_id.to_string())); + + let settings = from_insert_rules( + &client, + vec![(RuleKind::Room, &room_id, true), (RuleKind::Override, &room_id, true)], + ); + let custom_rules = get_custom_rules_for_room(&settings, &room_id).await; + assert_eq!(custom_rules.len(), 2); + assert_eq!(custom_rules[0], (RuleKind::Override, room_id.to_string())); + assert_eq!(custom_rules[1], (RuleKind::Room, room_id.to_string())); + } + + #[async_test] + async fn test_get_user_defined_room_notification_mode_none() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + let settings = client.notification_settings().await; + assert!(settings.get_user_defined_room_notification_mode(&room_id).await.is_none()); + } + + #[async_test] + async fn test_get_user_defined_room_notification_mode_all_messages() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + // Initialize with a notifying `Room` rule to be in `AllMessages` + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]); + + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::AllMessages + ); + } + + #[async_test] + async fn test_get_user_defined_room_notification_mode_mentions_and_keywords() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + // Initialize with a muted `Room` rule to be in `MentionsAndKeywordsOnly` + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, false)]); + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::MentionsAndKeywordsOnly + ); + } + + #[async_test] + async fn test_get_user_defined_room_notification_mode_mute() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + // Initialize with a muted `Override` rule to be in `Mute` + let settings = from_insert_rules(&client, vec![(RuleKind::Override, &room_id, false)]); + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::Mute + ); + } + + #[async_test] + async fn test_get_default_room_notification_mode_all_messages() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let mut ruleset = get_server_default_ruleset(); + ruleset + .set_actions( + RuleKind::Underride, + PredefinedUnderrideRuleId::RoomOneToOne, + vec![Action::Notify], + ) + .unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + assert_eq!( + settings.get_default_room_notification_mode(false, 2).await, + RoomNotificationMode::AllMessages + ); + } + + #[async_test] + async fn test_get_default_room_notification_mode_mentions_and_keywords() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + // The default mode must be `MentionsAndKeywords` if the corresponding Underride + // rule doesn't notify + let mut ruleset = get_server_default_ruleset(); + ruleset + .set_actions(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, vec![]) + .unwrap(); + + let settings = NotificationSettings::new(client.to_owned(), ruleset.to_owned()); + assert_eq!( + settings.get_default_room_notification_mode(false, 2).await, + RoomNotificationMode::MentionsAndKeywordsOnly + ); + + // The default mode must be `MentionsAndKeywords` if the corresponding Underride + // rule is disabled + ruleset + .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false) + .unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + assert_eq!( + settings.get_default_room_notification_mode(false, 2).await, + RoomNotificationMode::MentionsAndKeywordsOnly + ); + } + + #[async_test] + async fn test_contains_keyword_rules() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + let mut ruleset = get_server_default_ruleset(); + let settings = NotificationSettings::new(client.to_owned(), ruleset.to_owned()); + + // By default, no keywords rules should be present + let contains_keywords_rules = settings.contains_keyword_rules().await; + assert!(!contains_keywords_rules); + + // Initialize with a keyword rule + let rule = NewPatternedPushRule::new( + "keyword_rule_id".into(), + "keyword".into(), + vec![Action::Notify], + ); + ruleset.insert(NewPushRule::Content(rule), None, None).unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + let contains_keywords_rules = settings.contains_keyword_rules().await; + assert!(contains_keywords_rules); + } + + #[async_test] + async fn test_is_push_rule_enabled() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + // Initial state: Reaction disabled + let mut ruleset = get_server_default_ruleset(); + ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false).unwrap(); + + let settings = NotificationSettings::new(client.clone(), ruleset); + + let enabled = settings + .is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str()) + .await + .unwrap(); + + assert!(!enabled); + + // Initial state: Reaction enabled + let mut ruleset = get_server_default_ruleset(); + ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true).unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + + let enabled = settings + .is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str()) + .await + .unwrap(); + + assert!(enabled); + } + + #[async_test] + async fn test_set_push_rule_enabled() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let mut ruleset = client.account().push_rules().await.unwrap(); + // Initial state + ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false).unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + + settings + .set_push_rule_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::Reaction.as_str(), + true, + ) + .await + .unwrap(); + + // Test the request sent + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 1); + assert_eq!(requests[0].method, Method::Put); + assert_eq!( + requests[0].url.path(), + "/_matrix/client/r0/pushrules/global/override/.m.rule.reaction/enabled" + ); + + // The ruleset must have been updated + let rules = settings.rules.read().await; + let rule = + rules.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::Reaction).unwrap(); + assert!(rule.enabled()); + } + + #[async_test] + async fn test_set_push_rule_enabled_api_error() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let mut ruleset = client.account().push_rules().await.unwrap(); + // Initial state + ruleset + .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false) + .unwrap(); + + let settings = NotificationSettings::new(client, ruleset); + + // If the server returns an error + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(500)).mount(&server).await; + + // When enabling the push rule + assert_eq!( + settings + .set_push_rule_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::IsUserMention.as_str(), + true, + ) + .await, + Err(NotificationSettingsError::UnableToUpdatePushRule) + ); + + // The ruleset must not have been updated + let rules = settings.rules.read().await; + let rule = + rules.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention).unwrap(); + assert!(!rule.enabled()); + } + + #[async_test] + async fn test_set_room_notification_mode() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + + let settings = client.notification_settings().await; + let room_id = get_test_room_id(); + + let mode = settings.get_user_defined_room_notification_mode(&room_id).await; + assert!(mode.is_none()); + + let new_modes = &[ + RoomNotificationMode::AllMessages, + RoomNotificationMode::MentionsAndKeywordsOnly, + RoomNotificationMode::Mute, + ]; + for new_mode in new_modes { + settings.set_room_notification_mode(&room_id, new_mode.clone()).await.unwrap(); + + assert_eq!( + new_mode.clone(), + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap() + ); + } + } + + #[async_test] + async fn test_set_room_notification_mode_requests_order() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + + let room_id = get_test_room_id(); + + // Set the initial state to `AllMessages` by setting a `Room` rule that notifies + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]); + + // Set the new mode to `Mute`, this will add a new `Override` rule without + // action and remove the `Room` rule. + settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await.unwrap(); + + assert_eq!( + RoomNotificationMode::Mute, + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap() + ); + + // Test that the PUT is executed before the DELETE, so that the following sync + // results will give the following transitions: `AllMessages` -> + // `AllMessages` -> `Mute` by sending the DELETE before the PUT, we + // would have `AllMessages` -> `Default` -> `Mute` + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 2); + assert_eq!(requests[0].method, Method::Put); + assert_eq!(requests[1].method, Method::Delete); + } + + #[async_test] + async fn test_set_room_notification_mode_put_api_error() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + // If the server returns an error + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(500)).mount(&server).await; + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + + let room_id = get_test_room_id(); + + // Set the initial state to `AllMessages` by setting a `Room` rule that notifies + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]); + + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::AllMessages + ); + + // Setting the new mode should fail + assert_eq!( + settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await, + Err(NotificationSettingsError::UnableToAddPushRule) + ); + + // The ruleset must not have been updated + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::AllMessages + ); + } + + #[async_test] + async fn test_set_room_notification_mode_delete_api_error() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + + // If the server returns an error + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(500)).mount(&server).await; + + let room_id = get_test_room_id(); + + // Set the initial state to `AllMessages` by setting a `Room` rule that notifies + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]); + + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::AllMessages + ); + + // Setting the new mode should fail + assert_eq!( + settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await, + Err(NotificationSettingsError::UnableToRemovePushRule) + ); + + // The ruleset must not have been updated + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::AllMessages + ); + } + + #[async_test] + async fn test_delete_user_defined_room_rules() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap(); + let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap(); + + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + + // Initialize with some of custom rules + let settings = from_insert_rules( + &client, + vec![ + (RuleKind::Room, &room_id_a, true), + (RuleKind::Room, &room_id_b, true), + (RuleKind::Override, &room_id_b, true), + ], + ); + + // Delete all user defined rules for room_id_a + settings.delete_user_defined_room_rules(&room_id_a).await.unwrap(); + + // Only the rules for room_id_b should remain + let updated_rules = settings.rules.read().await; + assert_eq!(updated_rules.get_custom_rules_for_room(&room_id_b).len(), 2); + assert!(updated_rules.get_custom_rules_for_room(&room_id_a).is_empty()); + } + + #[async_test] + async fn test_unmute_room_not_muted() { + let server = MockServer::start().await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + // Initialize with a `MentionsAndKeywordsOnly` mode + let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, false)]); + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::MentionsAndKeywordsOnly + ); + + // Unmute the room + settings.unmute_room(&room_id, true, 2).await.unwrap(); + + // The ruleset must not be modified + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(), + RoomNotificationMode::MentionsAndKeywordsOnly + ); + + let room_rules = get_custom_rules_for_room(&settings, &room_id).await; + assert_eq!(room_rules.len(), 1); + assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Room, &room_id), + Some(AnyPushRuleRef::Room(rule)) => { + assert_eq!(rule.rule_id, room_id); + assert!(rule.actions.is_empty()); + } + ); + } + + #[async_test] + async fn test_unmute_room() { + let server = MockServer::start().await; + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + + // Start with the room muted + let settings = from_insert_rules(&client, vec![(RuleKind::Override, &room_id, false)]); + assert_eq!( + settings.get_user_defined_room_notification_mode(&room_id).await, + Some(RoomNotificationMode::Mute) + ); + + // Unmute the room + settings.unmute_room(&room_id, false, 2).await.unwrap(); + + // The user defined mode must have been removed + assert!(settings.get_user_defined_room_notification_mode(&room_id).await.is_none()); + } + + #[async_test] + async fn test_unmute_room_default_mode() { + let server = MockServer::start().await; + Mock::given(method("PUT")).respond_with(ResponseTemplate::new(200)).mount(&server).await; + let client = logged_in_client(Some(server.uri())).await; + let room_id = get_test_room_id(); + let settings = client.notification_settings().await; + + // Unmute the room + settings.unmute_room(&room_id, false, 2).await.unwrap(); + + // The new mode must be `AllMessages` + assert_eq!( + Some(RoomNotificationMode::AllMessages), + settings.get_user_defined_room_notification_mode(&room_id).await + ); + + let room_rules = get_custom_rules_for_room(&settings, &room_id).await; + assert_eq!(room_rules.len(), 1); + assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Room, &room_id), + Some(AnyPushRuleRef::Room(rule)) => { + assert_eq!(rule.rule_id, room_id); + assert!(!rule.actions.is_empty()); + } + ); + } +} diff --git a/crates/matrix-sdk/src/notification_settings/rule_commands.rs b/crates/matrix-sdk/src/notification_settings/rule_commands.rs new file mode 100644 index 000000000..bbd1a551c --- /dev/null +++ b/crates/matrix-sdk/src/notification_settings/rule_commands.rs @@ -0,0 +1,485 @@ +use ruma::{ + api::client::push::RuleScope, + push::{ + PredefinedContentRuleId, PredefinedOverrideRuleId, RemovePushRuleError, RuleKind, Ruleset, + }, + RoomId, +}; + +use super::command::Command; +use crate::NotificationSettingsError; + +/// A `RuleCommand` allows to generate a list of `Command` needed to modify a +/// `Ruleset` +#[derive(Clone, Debug)] +pub(crate) struct RuleCommands { + pub(crate) commands: Vec, + pub(crate) rules: Ruleset, +} + +impl RuleCommands { + pub(crate) fn new(rules: Ruleset) -> Self { + RuleCommands { commands: vec![], rules } + } + + /// Insert a new rule + pub(crate) fn insert_rule( + &mut self, + kind: RuleKind, + room_id: &RoomId, + notify: bool, + ) -> Result<(), NotificationSettingsError> { + let command = match kind { + RuleKind::Room => Command::SetRoomPushRule { + scope: RuleScope::Global, + room_id: room_id.to_owned(), + notify, + }, + RuleKind::Override => Command::SetOverridePushRule { + scope: RuleScope::Global, + rule_id: room_id.to_string(), + room_id: room_id.to_owned(), + notify, + }, + _ => { + return Err(NotificationSettingsError::InvalidParameter( + "cannot insert a rule for this kind.".to_owned(), + )) + } + }; + + self.rules.insert(command.to_push_rule()?, None, None)?; + self.commands.push(command); + + Ok(()) + } + + /// Delete a rule + pub(crate) fn delete_rule( + &mut self, + kind: RuleKind, + rule_id: String, + ) -> Result<(), RemovePushRuleError> { + self.rules.remove(kind.clone(), &rule_id)?; + self.commands.push(Command::DeletePushRule { scope: RuleScope::Global, kind, rule_id }); + + Ok(()) + } + + fn set_enabled_internal( + &mut self, + kind: RuleKind, + rule_id: &str, + enabled: bool, + ) -> Result<(), NotificationSettingsError> { + self.rules.set_enabled(kind.clone(), rule_id, enabled)?; + self.commands.push(Command::SetPushRuleEnabled { + scope: RuleScope::Global, + kind, + rule_id: rule_id.to_owned(), + enabled, + }); + Ok(()) + } + + /// Set whether a rule is enabled + pub(crate) fn set_rule_enabled( + &mut self, + kind: RuleKind, + rule_id: &str, + enabled: bool, + ) -> Result<(), NotificationSettingsError> { + if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() { + // Handle specific case for `PredefinedOverrideRuleId::IsRoomMention` + self.set_room_mention_enabled(enabled) + } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() { + // Handle specific case for `PredefinedOverrideRuleId::IsUserMention` + self.set_user_mention_enabled(enabled) + } else { + self.set_enabled_internal(kind, rule_id, enabled) + } + } + + /// Set whether `IsUserMention` is enabled + fn set_user_mention_enabled(&mut self, enabled: bool) -> Result<(), NotificationSettingsError> { + // Add a command for the `IsUserMention` `Override` rule (MSC3952). + // This is a new push rule that may not yet be present. + self.set_enabled_internal( + RuleKind::Override, + PredefinedOverrideRuleId::IsUserMention.as_str(), + enabled, + )?; + + // For compatibility purpose, we still need to add commands for + // `ContainsUserName` and `ContainsDisplayName` (deprecated rules). + #[allow(deprecated)] + { + // `ContainsUserName` + self.set_enabled_internal( + RuleKind::Content, + PredefinedContentRuleId::ContainsUserName.as_str(), + enabled, + )?; + + // `ContainsDisplayName` + self.set_enabled_internal( + RuleKind::Override, + PredefinedOverrideRuleId::ContainsDisplayName.as_str(), + enabled, + )?; + } + + Ok(()) + } + + /// Set whether `IsRoomMention` is enabled + fn set_room_mention_enabled(&mut self, enabled: bool) -> Result<(), NotificationSettingsError> { + // Sets the `IsRoomMention` `Override` rule (MSC3952). + // This is a new push rule that may not yet be present. + self.set_enabled_internal( + RuleKind::Override, + PredefinedOverrideRuleId::IsRoomMention.as_str(), + enabled, + )?; + + // For compatibility purpose, we still need to set `RoomNotif` (deprecated + // rule). + #[allow(deprecated)] + self.set_enabled_internal( + RuleKind::Override, + PredefinedOverrideRuleId::RoomNotif.as_str(), + enabled, + )?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use matrix_sdk_test::async_test; + use ruma::{ + api::client::push::RuleScope, + push::{ + NewPushRule, NewSimplePushRule, PredefinedContentRuleId, PredefinedOverrideRuleId, + RemovePushRuleError, RuleKind, Ruleset, + }, + OwnedRoomId, RoomId, UserId, + }; + + use super::RuleCommands; + use crate::{error::NotificationSettingsError, notification_settings::command::Command}; + + fn get_server_default_ruleset() -> Ruleset { + let user_id = UserId::parse("@user:matrix.org").unwrap(); + Ruleset::server_default(&user_id) + } + + fn get_test_room_id() -> OwnedRoomId { + RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap() + } + + #[async_test] + async fn test_insert_rule_room() { + let room_id = get_test_room_id(); + let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); + rule_commands.insert_rule(RuleKind::Room, &room_id, true).unwrap(); + + // A rule must have been inserted in the ruleset. + assert!(rule_commands.rules.get(RuleKind::Room, &room_id).is_some()); + + // Exactly one command must have been created. + assert_eq!(rule_commands.commands.len(), 1); + assert_matches!(&rule_commands.commands[0], + Command::SetRoomPushRule { scope, room_id: command_room_id, notify } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(command_room_id, &room_id); + assert!(notify); + } + ); + } + + #[async_test] + async fn test_insert_rule_override() { + let room_id = get_test_room_id(); + let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); + rule_commands.insert_rule(RuleKind::Override, &room_id, true).unwrap(); + + // A rule must have been inserted in the ruleset. + assert!(rule_commands.rules.get(RuleKind::Override, &room_id).is_some()); + + // Exactly one command must have been created. + assert_eq!(rule_commands.commands.len(), 1); + assert_matches!(&rule_commands.commands[0], + Command::SetOverridePushRule { scope, room_id: command_room_id, rule_id, notify } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(command_room_id, &room_id); + assert_eq!(rule_id, room_id.as_str()); + assert!(notify); + } + ); + } + + #[async_test] + async fn test_insert_rule_unsupported() { + let room_id = get_test_room_id(); + let mut rule_commands = RuleCommands::new(get_server_default_ruleset()); + + assert_matches!( + rule_commands.insert_rule(RuleKind::Underride, &room_id, true), + Err(NotificationSettingsError::InvalidParameter(_)) => {} + ); + + assert_matches!( + rule_commands.insert_rule(RuleKind::Content, &room_id, true), + Err(NotificationSettingsError::InvalidParameter(_)) => {} + ); + + assert_matches!( + rule_commands.insert_rule(RuleKind::Sender, &room_id, true), + Err(NotificationSettingsError::InvalidParameter(_)) => {} + ); + } + + #[async_test] + async fn test_delete_rule() { + let room_id = get_test_room_id(); + let mut ruleset = get_server_default_ruleset(); + + let new_rule = NewSimplePushRule::new(room_id.to_owned(), vec![]); + ruleset.insert(NewPushRule::Room(new_rule), None, None).unwrap(); + + let mut rule_commands = RuleCommands::new(ruleset); + + // Delete must succeed. + rule_commands.delete_rule(RuleKind::Room, room_id.to_string()).unwrap(); + + // The ruleset must have been updated. + assert!(rule_commands.rules.get(RuleKind::Room, &room_id).is_none()); + + // Exactly one command must have been created. + assert_eq!(rule_commands.commands.len(), 1); + assert_matches!(&rule_commands.commands[0], + Command::DeletePushRule { scope, kind, rule_id } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Room); + assert_eq!(rule_id, room_id.as_str()); + } + ); + } + + #[async_test] + async fn test_delete_rule_errors() { + let room_id = get_test_room_id(); + let ruleset = get_server_default_ruleset(); + + let mut rule_commands = RuleCommands::new(ruleset); + + // Deletion should fail if an attempt is made to delete a rule that does not + // exist. + assert_matches!( + rule_commands.delete_rule(RuleKind::Room, room_id.to_string()), + Err(RemovePushRuleError::NotFound) => {} + ); + + // Deletion should fail if an attempt is made to delete a default server rule. + assert_matches!( + rule_commands.delete_rule(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.to_string()), + Err(RemovePushRuleError::ServerDefault) => {} + ); + + assert!(rule_commands.commands.is_empty()); + } + + #[async_test] + async fn test_set_rule_enabled() { + let mut ruleset = get_server_default_ruleset(); + + // Initialize with `Reaction` rule disabled. + ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false).unwrap(); + + let mut rule_commands = RuleCommands::new(ruleset); + rule_commands + .set_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str(), true) + .unwrap(); + + // The ruleset must have been updated + let rule = rule_commands + .rules + .get(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str()) + .unwrap(); + assert!(rule.enabled()); + + // Exactly one command must have been created. + assert_eq!(rule_commands.commands.len(), 1); + assert_matches!(&rule_commands.commands[0], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Override); + assert_eq!(rule_id, PredefinedOverrideRuleId::Reaction.as_str()); + assert!(enabled); + } + ); + } + + #[async_test] + async fn test_set_rule_enabled_not_found() { + let ruleset = get_server_default_ruleset(); + let mut rule_commands = RuleCommands::new(ruleset); + assert_eq!( + rule_commands.set_rule_enabled(RuleKind::Room, "unknown_rule_id", true), + Err(NotificationSettingsError::RuleNotFound) + ); + } + + #[async_test] + async fn test_set_rule_enabled_user_mention() { + let mut ruleset = get_server_default_ruleset(); + let mut rule_commands = RuleCommands::new(ruleset.clone()); + + ruleset + .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false) + .unwrap(); + + #[allow(deprecated)] + { + ruleset + .set_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::ContainsDisplayName, + false, + ) + .unwrap(); + ruleset + .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false) + .unwrap(); + } + + // Enable the user mention rule. + rule_commands + .set_rule_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::IsUserMention.as_str(), + true, + ) + .unwrap(); + + // The ruleset must have been updated. + assert!(rule_commands + .rules + .get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention) + .unwrap() + .enabled()); + #[allow(deprecated)] + { + assert!(rule_commands + .rules + .get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName) + .unwrap() + .enabled()); + assert!(rule_commands + .rules + .get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName) + .unwrap() + .enabled()); + } + + // Three commands are expected. + assert_eq!(rule_commands.commands.len(), 3); + + assert_matches!(&rule_commands.commands[0], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Override); + assert_eq!(rule_id, PredefinedOverrideRuleId::IsUserMention.as_str()); + assert!(enabled); + } + ); + + #[allow(deprecated)] + { + assert_matches!(&rule_commands.commands[1], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Content); + assert_eq!(rule_id, PredefinedContentRuleId::ContainsUserName.as_str()); + assert!(enabled); + } + ); + + assert_matches!(&rule_commands.commands[2], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Override); + assert_eq!(rule_id, PredefinedOverrideRuleId::ContainsDisplayName.as_str()); + assert!(enabled); + } + ); + } + } + + #[async_test] + async fn test_set_rule_enabled_room_mention() { + let mut ruleset = get_server_default_ruleset(); + let mut rule_commands = RuleCommands::new(ruleset.clone()); + + ruleset + .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false) + .unwrap(); + + #[allow(deprecated)] + { + ruleset + .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false) + .unwrap(); + } + + rule_commands + .set_rule_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::IsRoomMention.as_str(), + true, + ) + .unwrap(); + + // The ruleset must have been updated. + assert!(rule_commands + .rules + .get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention) + .unwrap() + .enabled()); + #[allow(deprecated)] + { + assert!(rule_commands + .rules + .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif) + .unwrap() + .enabled()); + } + + // Two commands are expected. + assert_eq!(rule_commands.commands.len(), 2); + + assert_matches!(&rule_commands.commands[0], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Override); + assert_eq!(rule_id, PredefinedOverrideRuleId::IsRoomMention.as_str()); + assert!(enabled); + } + ); + + #[allow(deprecated)] + { + assert_matches!(&rule_commands.commands[1], + Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { + assert_eq!(scope, &RuleScope::Global); + assert_eq!(kind, &RuleKind::Override); + assert_eq!(rule_id, PredefinedOverrideRuleId::RoomNotif.as_str()); + assert!(enabled); + } + ); + } + } +} diff --git a/crates/matrix-sdk/src/notification_settings/rules.rs b/crates/matrix-sdk/src/notification_settings/rules.rs index 7f13574aa..d231f9c72 100644 --- a/crates/matrix-sdk/src/notification_settings/rules.rs +++ b/crates/matrix-sdk/src/notification_settings/rules.rs @@ -1,35 +1,21 @@ //! Ruleset utility struct use ruma::{ - api::client::push::RuleScope, push::{ - Action, NewConditionalPushRule, NewPushRule, NewSimplePushRule, PredefinedContentRuleId, - PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RemovePushRuleError, - RuleKind, Ruleset, Tweak, + PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedUnderrideRuleId, + PushCondition, RuleKind, Ruleset, }, RoomId, }; -use super::RoomNotificationMode; +use super::{command::Command, rule_commands::RuleCommands, RoomNotificationMode}; use crate::error::NotificationSettingsError; -/// enum describing the commands required to modify the owner's account data. #[derive(Clone, Debug)] -#[allow(dead_code)] -pub(crate) enum Command { - /// Set a new push rule - SetPushRule { scope: RuleScope, rule: NewPushRule }, - /// Set whether a push rule is enabled - SetPushRuleEnabled { scope: RuleScope, kind: RuleKind, rule_id: String, enabled: bool }, - /// Delete a push rule - DeletePushRule { scope: RuleScope, kind: RuleKind, rule_id: String }, -} - pub(crate) struct Rules { pub ruleset: Ruleset, } -#[allow(dead_code)] impl Rules { pub(crate) fn new(ruleset: Ruleset) -> Self { Rules { ruleset } @@ -81,6 +67,7 @@ impl Rules { // enabled x.enabled && // with a condition of type `EventMatch` for this `room_id` + // XXX why not checking against x.rule_id here? x.conditions.iter().any(|x| matches!( x, PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id @@ -132,7 +119,7 @@ impl Rules { } /// Get whether the `IsUserMention` rule is enabled. - pub(crate) fn is_user_mention_enabled(&self) -> bool { + fn is_user_mention_enabled(&self) -> bool { // Search for an enabled `Override` rule `IsUserMention` (MSC3952). // This is a new push rule that may not yet be present. if let Some(rule) = @@ -166,7 +153,7 @@ impl Rules { } /// Get whether the `IsRoomMention` rule is enabled. - pub(crate) fn is_room_mention_enabled(&self) -> bool { + fn is_room_mention_enabled(&self) -> bool { // Search for an enabled `Override` rule `IsRoomMention` (MSC3952). // This is a new push rule that may not yet be present. if let Some(rule) = @@ -210,197 +197,26 @@ impl Rules { } } - /// Insert a new `Room` push rule for a given `room_id` and return an - /// optional `Command` describing the action to be performed on the - /// user's account data. - pub(crate) fn insert_room_rule( - &mut self, - kind: RuleKind, - room_id: &RoomId, - notify: bool, - ) -> Result, NotificationSettingsError> { - let command; - let actions = if notify { - vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))] - } else { - vec![] - }; - - match kind { - RuleKind::Override => { - // Insert a new push rule matching this `room_id` - let new_rule = NewConditionalPushRule::new( - room_id.to_string(), - vec![PushCondition::EventMatch { - key: "room_id".into(), - pattern: room_id.to_string(), - }], - actions, - ); - let new_rule = NewPushRule::Override(new_rule); - self.ruleset.insert(new_rule.clone(), None, None)?; - command = Some(Command::SetPushRule { scope: RuleScope::Global, rule: new_rule }); - } - RuleKind::Room => { - // Insert a new `Room` push rule for this `room_id` - let new_rule = NewSimplePushRule::new(room_id.to_owned(), actions); - let new_rule = NewPushRule::Room(new_rule); - self.ruleset.insert(new_rule.clone(), None, None)?; - command = Some(Command::SetPushRule { scope: RuleScope::Global, rule: new_rule }); - } - _ => { - return Err(NotificationSettingsError::InvalidParameter( - "kind must be either Override or Room.".to_owned(), - )) - } - } - - Ok(command) - } - - /// Deletes a list of rules and returns a list of `Command` describing the - /// actions to be performed on the user's account data. + /// Apply a group of commands to the managed ruleset. /// - /// # Arguments - /// - /// * `rules` - A list of rules to delete - /// * `exceptions` - A list of rules to ignore - pub(crate) fn delete_rules( - &mut self, - rules: &[(RuleKind, String)], - exceptions: &[(RuleKind, String)], - ) -> Result, RemovePushRuleError> { - let mut commands = vec![]; - - for (rule_kind, rule_id) in rules { - // Ignore rules present in exceptions - if exceptions.iter().any(|e| e.0 == *rule_kind && e.1 == *rule_id) { - continue; + /// The command may silently fail because the ruleset may have changed + /// between the time the command was created and the time it is applied. + pub(crate) fn apply(&mut self, commands: RuleCommands) { + for command in commands.commands { + match command { + Command::DeletePushRule { scope: _, kind, rule_id } => { + _ = self.ruleset.remove(kind, rule_id); + } + Command::SetRoomPushRule { .. } | Command::SetOverridePushRule { .. } => { + if let Ok(push_rule) = command.to_push_rule() { + _ = self.ruleset.insert(push_rule, None, None); + } + } + Command::SetPushRuleEnabled { scope: _, kind, rule_id, enabled } => { + _ = self.ruleset.set_enabled(kind, rule_id, enabled); + } } - - self.ruleset.remove(rule_kind.clone(), rule_id)?; - commands.push(Command::DeletePushRule { - scope: RuleScope::Global, - kind: rule_kind.clone(), - rule_id: rule_id.clone(), - }) } - Ok(commands) - } - - /// Sets whether a rule is enabled and returns a list of `Command` - /// describing the actions to be performed on the user's account data. - pub(crate) fn set_enabled( - &mut self, - kind: RuleKind, - rule_id: &str, - enabled: bool, - ) -> Result, NotificationSettingsError> { - if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() { - // Handle specific case for `PredefinedOverrideRuleId::IsRoomMention` - self.set_room_mention_enabled(enabled) - } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() { - // Handle specific case for `PredefinedOverrideRuleId::IsUserMention` - self.set_user_mention_enabled(enabled) - } else { - let mut commands = vec![]; - self.set_rule_enabled(kind, rule_id, enabled, &mut commands)?; - Ok(commands) - } - } - - /// Helper function to enable or disable a rule and update a list of - /// commands to be performed on the user's account data to reflect the - /// change. - fn set_rule_enabled( - &mut self, - kind: RuleKind, - rule_id: &str, - enabled: bool, - commands: &mut Vec, - ) -> Result<(), NotificationSettingsError> { - self.ruleset.set_enabled(kind.clone(), rule_id, enabled)?; - commands.push(Command::SetPushRuleEnabled { - scope: RuleScope::Global, - kind, - rule_id: rule_id.to_owned(), - enabled, - }); - - Ok(()) - } - - /// Set whether the `IsUserMention` rule is enabled and returns a list of - /// `Command` describing the actions to be performed on the user's account - /// data. - fn set_user_mention_enabled( - &mut self, - enabled: bool, - ) -> Result, NotificationSettingsError> { - let mut commands = vec![]; - - // Sets the `IsUserMention` `Override` rule (MSC3952). - // This is a new push rule that may not yet be present. - self.set_rule_enabled( - RuleKind::Override, - PredefinedOverrideRuleId::IsUserMention.as_str(), - enabled, - &mut commands, - )?; - - // For compatibility purpose, we still need to set `ContainsUserName` and - // `ContainsDisplayName` (deprecated rules). - #[allow(deprecated)] - { - // `ContainsUserName` - self.set_rule_enabled( - RuleKind::Content, - PredefinedContentRuleId::ContainsUserName.as_str(), - enabled, - &mut commands, - )?; - - // `ContainsDisplayName` - self.set_rule_enabled( - RuleKind::Override, - PredefinedOverrideRuleId::ContainsDisplayName.as_str(), - enabled, - &mut commands, - )?; - } - - Ok(commands) - } - - /// Set whether the `IsRoomMention` rule is enabled and returns a list of - /// `Command` describing the actions to be performed on the user's account - /// data. - fn set_room_mention_enabled( - &mut self, - enabled: bool, - ) -> Result, NotificationSettingsError> { - let mut commands = vec![]; - - // Sets the `IsRoomMention` `Override` rule (MSC3952). - // This is a new push rule that may not yet be present. - self.set_rule_enabled( - RuleKind::Override, - PredefinedOverrideRuleId::IsRoomMention.as_str(), - enabled, - &mut commands, - )?; - - // For compatibility purpose, we still need to set `RoomNotif` (deprecated - // rule). - #[allow(deprecated)] - self.set_rule_enabled( - RuleKind::Override, - PredefinedOverrideRuleId::RoomNotif.as_str(), - enabled, - &mut commands, - )?; - - Ok(commands) } } @@ -425,19 +241,16 @@ fn get_predefined_underride_room_rule_id( #[cfg(test)] pub(crate) mod tests { - use assert_matches::assert_matches; use matrix_sdk_test::async_test; use ruma::{ - api::client::push::RuleScope, push::{ - Action, AnyPushRule, NewConditionalPushRule, NewPushRule, NewSimplePushRule, - PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedUnderrideRuleId, - PushCondition, RuleKind, Ruleset, + Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId, + PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset, }, OwnedRoomId, RoomId, UserId, }; - use super::Command; + use super::RuleCommands; use crate::{ error::NotificationSettingsError, notification_settings::{ @@ -455,19 +268,39 @@ pub(crate) mod tests { RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap() } + fn build_rules(rule_list: Vec<(RuleKind, &RoomId, bool)>) -> Rules { + let mut rules = Rules::new(get_server_default_ruleset()); + let mut commands = RuleCommands::new(rules.ruleset.clone()); + for (kind, room_id, notify) in rule_list { + commands.insert_rule(kind, room_id, notify).unwrap(); + } + // XXX this should not make use of `apply()`, see other comment. Such a helper + // should return a `Ruleset`, and not have to do anything with `Rules`. + rules.apply(commands); + rules + } + #[async_test] async fn test_get_custom_rules_for_room() { let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + let rules = Rules::new(get_server_default_ruleset()); assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0); - // Insert an Override rule - rules.insert_room_rule(ruma::push::RuleKind::Override, &room_id, false).unwrap(); + // Initialize with one rule. + // XXX this is not testing things in isolation: `build_rules` makes use of + // `apply`, and then we use `get_custom_rules_for_room`. Instead, this + // test should create a `Ruleset` by hand, then test single functions in + // isolation in it. `build_rules` should not use `Rules` method, since + // we're testing `Rules` methods here! + let rules = build_rules(vec![(RuleKind::Override, &room_id, false)]); assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1); // Insert a Room rule - rules.insert_room_rule(ruma::push::RuleKind::Room, &room_id, false).unwrap(); + let rules = build_rules(vec![ + (RuleKind::Override, &room_id, false), + (RuleKind::Room, &room_id, false), + ]); assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2); } @@ -490,58 +323,40 @@ pub(crate) mod tests { } #[async_test] - async fn test_get_user_defined_room_notification_mode_none() { + async fn test_get_user_defined_room_notification_mode() { let room_id = get_test_room_id(); let rules = Rules::new(get_server_default_ruleset()); assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None); - } - #[async_test] - async fn test_get_user_defined_room_notification_mode_mute() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + // Initialize with an `Override` rule that doesn't notify + let rules = build_rules(vec![(RuleKind::Override, &room_id, false)]); + assert_eq!( + rules.get_user_defined_room_notification_mode(&room_id), + Some(RoomNotificationMode::Mute) + ); - // Insert an Override rule that doesn't notify - rules.insert_room_rule(ruma::push::RuleKind::Override, &room_id, false).unwrap(); - let mode = rules.get_user_defined_room_notification_mode(&room_id); - assert_eq!(mode, Some(RoomNotificationMode::Mute)); - } + // Initialize with a `Room` rule that doesn't notify + let rules = build_rules(vec![(RuleKind::Room, &room_id, false)]); + assert_eq!( + rules.get_user_defined_room_notification_mode(&room_id), + Some(RoomNotificationMode::MentionsAndKeywordsOnly) + ); - #[async_test] - async fn test_get_user_defined_room_notification_mode_mentions_and_keywords() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + // Initialize with a `Room` rule that doesn't notify + let rules = build_rules(vec![(RuleKind::Room, &room_id, true)]); + assert_eq!( + rules.get_user_defined_room_notification_mode(&room_id), + Some(RoomNotificationMode::AllMessages) + ); - // Insert a Room rule that doesn't notify - rules.insert_room_rule(ruma::push::RuleKind::Room, &room_id, false).unwrap(); - let mode = rules.get_user_defined_room_notification_mode(&room_id); - assert_eq!(mode, Some(RoomNotificationMode::MentionsAndKeywordsOnly)); - } - - #[async_test] - async fn test_get_user_defined_room_notification_mode_all_messages() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); - - // Insert a Room rule that notifies - rules.insert_room_rule(ruma::push::RuleKind::Room, &room_id, true).unwrap(); - let mode = rules.get_user_defined_room_notification_mode(&room_id); - assert_eq!(mode, Some(RoomNotificationMode::AllMessages)); - } - - #[async_test] - async fn test_get_user_defined_room_notification_mode_multiple_override_rules() { let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap(); let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap(); - - let mut rules = Rules::new(get_server_default_ruleset()); - - // Insert the muting rule - rules.insert_room_rule(ruma::push::RuleKind::Override, &room_id_a, false).unwrap(); - // Insert another muting rule for another room (it will be inserted before the - // previous one) - rules.insert_room_rule(ruma::push::RuleKind::Override, &room_id_b, true).unwrap(); - + let rules = build_rules(vec![ + // A mute rule for room_id_a + (RuleKind::Override, &room_id_a, false), + // A notifying rule for room_id_b + (RuleKind::Override, &room_id_b, true), + ]); let mode = rules.get_user_defined_room_notification_mode(&room_id_a); // The mode should be Mute as there is an Override rule that doesn't notify, @@ -605,6 +420,7 @@ pub(crate) mod tests { ruleset .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true) .unwrap(); + let rules = Rules::new(ruleset); assert!(rules.is_user_mention_enabled()); // is_enabled() should also return `true` for @@ -676,6 +492,7 @@ pub(crate) mod tests { ruleset .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true) .unwrap(); + let rules = Rules::new(ruleset); assert!(rules.is_room_mention_enabled()); // is_enabled() should also return `true` for @@ -700,6 +517,7 @@ pub(crate) mod tests { vec![Action::Notify], ) .unwrap(); + let rules = Rules::new(ruleset); assert!(rules.is_room_mention_enabled()); // is_enabled() should also return `true` for @@ -739,351 +557,60 @@ pub(crate) mod tests { } #[async_test] - async fn test_insert_room_rule_override() { + async fn test_apply_delete_command() { let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + // Initialize with a custom rule + let mut rules = build_rules(vec![(RuleKind::Override, &room_id, false)]); - let command = rules.insert_room_rule(RuleKind::Override, &room_id, true).unwrap(); + // Build a `RuleCommands` deleting this rule + let mut rules_commands = RuleCommands::new(rules.ruleset.clone()); + rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap(); - // The ruleset should contains the new rule - let new_rule = rules.ruleset.get(RuleKind::Override, &room_id).unwrap(); + rules.apply(rules_commands); - assert_matches!( - new_rule.to_owned(), - AnyPushRule::Override(rule) => { - assert_eq!(rule.rule_id, room_id); - assert!(rule.conditions.iter().any(|x| matches!( - x, - PushCondition::EventMatch { key, pattern } if key == "room_id" && *pattern == room_id - ))) - } - ); - - // The command list should contains only a SetPushRule command - assert_matches!( - command, - Some(Command::SetPushRule { scope, rule }) => { - assert_eq!(scope, RuleScope::Global); - assert_matches!( - rule, - NewPushRule::Override(rule) => { - assert_eq!(rule.rule_id, room_id); - assert!(rule.conditions.iter().any(|x| matches!( - x, - PushCondition::EventMatch { key, pattern } if key == "room_id" && *pattern == room_id - ))) - } - ) - } - ); + // The rule must have been removed from the updated rules + assert!(rules.get_custom_rules_for_room(&room_id).is_empty()); } #[async_test] - async fn test_insert_room_rule_room() { + async fn test_apply_set_command() { let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + let mut rules = build_rules(vec![]); - let command = rules.insert_room_rule(RuleKind::Room, &room_id, true).unwrap(); + // Build a `RuleCommands` inserting a rule + let mut rules_commands = RuleCommands::new(rules.ruleset.clone()); + rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap(); - // The ruleset should contains the new rule - let new_rule = - rules.ruleset.get(RuleKind::Room, &room_id).expect("a new Room rule is expected."); + rules.apply(rules_commands); - assert_matches!( - new_rule.to_owned(), - AnyPushRule::Room(rule) => { - assert_eq!(rule.rule_id, room_id); - } - ); - - // The command list should contains only a SetPushRule command - assert_matches!( - command, - Some(Command::SetPushRule { scope, rule }) => { - assert_eq!(scope, RuleScope::Global); - assert_matches!( - rule, - NewPushRule::Room(rule) => { - assert_eq!(rule.rule_id, room_id); - } - ) - } - ); + // The rule must have been removed from the updated rules + assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1); } #[async_test] - async fn test_insert_room_rule_invalid_kind() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); + async fn test_apply_set_enabled_command() { + let mut rules = build_rules(vec![]); - rules - .insert_room_rule(RuleKind::Content, &room_id, true) - .expect_err("An InvalidParameter error is expected"); - } - - #[async_test] - async fn test_delete_rules() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); - - rules.delete_rules(&[(RuleKind::Room, room_id.to_string())], &[]).expect_err( - "A RemovePushRuleError is expected while trying to delete an unknown rule.", - ); - - let new_rule = NewSimplePushRule::new(room_id.to_owned(), vec![]); - let new_rule = NewPushRule::Room(new_rule); - rules.ruleset.insert(new_rule, None, None).unwrap(); - - let commands = rules.delete_rules(&[(RuleKind::Room, room_id.to_string())], &[]).unwrap(); - // The command list should contains only a SetPushRule command - assert_eq!(commands.len(), 1); - - assert_matches!( - &commands[0], - Command::DeletePushRule { scope, kind, rule_id } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Room); - assert_eq!(rule_id, room_id.as_str()); - } - ); - } - - #[async_test] - async fn test_delete_rules_with_exceptions() { - let room_id = get_test_room_id(); - let mut rules = Rules::new(get_server_default_ruleset()); - - let new_rule = NewSimplePushRule::new(room_id.to_owned(), vec![]); - let new_rule = NewPushRule::Room(new_rule); - rules.ruleset.insert(new_rule, None, None).unwrap(); - - let rules_to_delete = vec![(RuleKind::Room, room_id.to_string())]; - let commands = rules.delete_rules(&rules_to_delete, &rules_to_delete).unwrap(); - // The command list should be empty as the rule to delete is also in the - // exceptions. - assert_eq!(commands.len(), 0); - } - - #[async_test] - async fn test_set_enabled() { - let mut rules = Rules::new(get_server_default_ruleset()); - - // Initialize the PredefinedOverrideRuleId::Reaction rule to enabled rules .ruleset .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true) .unwrap(); - // Ensure the initial state is `true` - let initial_state = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::Reaction) - .unwrap() - .enabled(); - assert!(initial_state); - // Disable the PredefinedOverrideRuleId::Reaction rule - let commands = rules - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str(), false) - .unwrap(); - let new_enabled_state = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::Reaction) - .unwrap() - .enabled(); - // The new enabled state should be `false` - assert!(!new_enabled_state); - // The command list should contains only a SetPushRuleEnabled command - assert_eq!(commands.len(), 1); - - assert_matches!( - &commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Override); - assert_eq!(rule_id, &PredefinedOverrideRuleId::Reaction.to_string()); - assert!(!enabled); - } - ); - } - - #[async_test] - async fn test_set_is_room_mention_enabled() { - let mut rules = Rules::new(get_server_default_ruleset()); - - // Initialize PredefinedOverrideRuleId::IsRoomMention and - // PredefinedOverrideRuleId::RoomNotif rules to disabled - rules - .ruleset - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false) - .unwrap(); - #[allow(deprecated)] - rules - .ruleset - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false) + // Build a `RuleCommands` disabling the rule + let mut rules_commands = RuleCommands::new(rules.ruleset.clone()); + rules_commands + .set_rule_enabled( + RuleKind::Override, + PredefinedOverrideRuleId::Reaction.as_str(), + false, + ) .unwrap(); - // Ensure the initial state is `false` - let is_room_mention_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention) - .unwrap() - .enabled(); - assert!(!is_room_mention_enabled); - #[allow(deprecated)] - let room_notif_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif) - .unwrap() - .enabled(); - assert!(!room_notif_enabled); + rules.apply(rules_commands); - // Enable the PredefinedOverrideRuleId::IsRoomMention rule - let commands = rules - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str(), true) - .unwrap(); - - // Ensure the new state is `true` for both rules - let is_room_mention_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention) - .unwrap() - .enabled(); - assert!(is_room_mention_enabled); - #[allow(deprecated)] - let room_notif_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif) - .unwrap() - .enabled(); - assert!(room_notif_enabled); - - // The command list should contains two SetPushRuleEnabled - assert_eq!(commands.len(), 2); - assert_matches!( - &commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Override); - assert_eq!(rule_id, &PredefinedOverrideRuleId::IsRoomMention.to_string()); - assert!(enabled); - } - ); - - assert_matches!( - &commands[1], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Override); - #[allow(deprecated)] - let expected_rule_id = PredefinedOverrideRuleId::RoomNotif; - assert_eq!(rule_id, &expected_rule_id.to_string()); - assert!(enabled); - } - ); - } - - #[async_test] - async fn test_set_is_user_mention_enabled() { - let mut rules = Rules::new(get_server_default_ruleset()); - - // Initialize PredefinedOverrideRuleId::IsRoomMention and - // PredefinedOverrideRuleId::RoomNotif rules to disabled - rules - .ruleset - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false) - .unwrap(); - #[allow(deprecated)] - rules - .ruleset - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false) - .unwrap(); - #[allow(deprecated)] - rules - .ruleset - .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false) - .unwrap(); - - // Ensure the initial state is `false` - let is_user_mention_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention) - .unwrap() - .enabled(); - assert!(!is_user_mention_enabled); - #[allow(deprecated)] - let contains_display_name_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName) - .unwrap() - .enabled(); - assert!(!contains_display_name_enabled); - #[allow(deprecated)] - let contains_user_name_enabled = rules - .ruleset - .get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName) - .unwrap() - .enabled(); - assert!(!contains_user_name_enabled); - - // Enable the PredefinedOverrideRuleId::IsUserMention rule - let commands = rules - .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str(), true) - .unwrap(); - - // Ensure the new state is `true` for all corresponding rules - let is_user_mention_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention) - .unwrap() - .enabled(); - assert!(is_user_mention_enabled); - #[allow(deprecated)] - let contains_display_name_enabled = rules - .ruleset - .get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName) - .unwrap() - .enabled(); - assert!(contains_display_name_enabled); - #[allow(deprecated)] - let contains_user_name_enabled = rules - .ruleset - .get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName) - .unwrap() - .enabled(); - assert!(contains_user_name_enabled); - - // The command list should contains 3 SetPushRuleEnabled - assert_eq!(commands.len(), 3); - assert_matches!( - &commands[0], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Override); - assert_eq!(rule_id, &PredefinedOverrideRuleId::IsUserMention.to_string()); - assert!(enabled); - } - ); - assert_matches!( - &commands[1], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Content); - #[allow(deprecated)] - let expected_rule_id = PredefinedContentRuleId::ContainsUserName; - assert_eq!(rule_id, &expected_rule_id.to_string()); - assert!(enabled); - } - ); - assert_matches!( - &commands[2], - Command::SetPushRuleEnabled { scope, kind, rule_id, enabled } => { - assert_eq!(scope, &RuleScope::Global); - assert_eq!(kind, &RuleKind::Override); - #[allow(deprecated)] - let expected_rule_id = PredefinedOverrideRuleId::ContainsDisplayName; - assert_eq!(rule_id, expected_rule_id.as_str()); - assert!(enabled); - } - ); + // The rule must have been disabled in the updated rules + assert!(!rules + .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str()) + .unwrap()); } }