sdk: Allow to update notification settings (#2135)

* sdk: Allow to update notification settings

* Add an event listener for push rule events

* Fix: simplify insertion and deletion of rules

* Fix: Limit rules cloning

* Fix: Unit tests

* Fix: set_room_notification_mode

* Fix: potential race condition when updating the local ruleset

* Refactor RuleCommands

* RuleCommands Unit Tests

* Fix: limit lock usage

* nit: use expression assignment by default

* nit: pass Ruleset by ownership in RuleCommands' ctor

* a few nits: use to_owned() for &str -> String; use free function for notify actions; use clone() where it's more obvious

and use explicit variants so we know we have to consider this match if we were to add a new variant later

* nit: rename RuleCommands::set_enabled to set_enabled_internal

* nit: test modules don't need to be publicized

* tidy tests

* nit: pass an owned RuleCommands in Rules::apply

* add comments/questions in Rules tests

* nit: no need to publicize the tests module

* tweak NotificationSettings tests too

* rustfmt

---------

Co-authored-by: Benjamin Bouvier <public@benj.me>
This commit is contained in:
Nicolas Mauri
2023-07-04 17:29:57 +02:00
committed by GitHub
parent 7bcc886429
commit 3db72101f3
7 changed files with 1511 additions and 590 deletions

View File

@@ -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

View File

@@ -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<InsertPushRuleError> for NotificationSettingsError {
@@ -444,6 +453,12 @@ impl From<InsertPushRuleError> for NotificationSettingsError {
}
}
impl From<RemovePushRuleError> for NotificationSettingsError {
fn from(_: RemovePushRuleError) -> Self {
Self::UnableToRemovePushRule
}
}
impl From<RuleNotFoundError> for NotificationSettingsError {
fn from(_: RuleNotFoundError) -> Self {
Self::RuleNotFound

View File

@@ -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};

View File

@@ -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<Action> {
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<NewPushRule, NotificationSettingsError> {
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(),
))
}
}
}
}

View File

@@ -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<RwLock<Rules>>,
/// 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<RoomNotificationMode> {
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<bool, NotificationSettingsError> {
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());
}
);
}
}

View File

@@ -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<Command>,
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);
}
);
}
}
}

View File

@@ -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<Option<Command>, 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<Vec<Command>, 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<Vec<Command>, 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<Command>,
) -> 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<Vec<Command>, 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<Vec<Command>, 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());
}
}