use std::{ops::Deref, sync::Arc};
use indexmap::IndexSet;
use ruma::{
RoomId,
api::client::push::{
delete_pushrule, set_pushrule, set_pushrule_actions, set_pushrule_enabled,
},
events::push_rules::PushRulesEvent,
push::{Action, NewPushRule, PredefinedUnderrideRuleId, RuleKind, Ruleset, Tweak},
};
use tokio::sync::{
RwLock,
broadcast::{self, Receiver},
};
use tracing::{debug, error};
use self::{command::Command, rule_commands::RuleCommands, rules::Rules};
mod command;
mod rule_commands;
mod rules;
pub use matrix_sdk_base::notification_settings::RoomNotificationMode;
use crate::{
Client, Result, config::RequestConfig, error::NotificationSettingsError,
event_handler::EventHandlerDropGuard,
};
#[derive(Debug, Clone, Copy)]
pub enum IsEncrypted {
Yes,
No,
}
impl From<bool> for IsEncrypted {
fn from(value: bool) -> Self {
if value { Self::Yes } else { Self::No }
}
}
#[derive(Debug, Clone, Copy)]
pub enum IsOneToOne {
Yes,
No,
}
impl From<bool> for IsOneToOne {
fn from(value: bool) -> Self {
if value { Self::Yes } else { Self::No }
}
}
#[derive(Debug, Clone)]
pub struct NotificationSettings {
client: Client,
rules: Arc<RwLock<Rules>>,
_push_rules_event_handler_guard: Arc<EventHandlerDropGuard>,
changes_sender: broadcast::Sender<()>,
}
impl NotificationSettings {
pub(crate) fn new(client: Client, ruleset: Ruleset) -> Self {
let changes_sender = broadcast::Sender::new(100);
let rules = Arc::new(RwLock::new(Rules::new(ruleset)));
let push_rules_event_handler_handle = client.add_event_handler({
let changes_sender = changes_sender.clone();
let rules = rules.clone();
move |ev: PushRulesEvent| async move {
*rules.write().await = Rules::new(ev.content.global);
let _ = changes_sender.send(());
}
});
let _push_rules_event_handler_guard =
Arc::new(client.event_handler_drop_guard(push_rules_event_handler_handle));
Self { client, rules, _push_rules_event_handler_guard, changes_sender }
}
pub fn subscribe_to_changes(&self) -> Receiver<()> {
self.changes_sender.subscribe()
}
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)
}
pub async fn get_default_room_notification_mode(
&self,
is_encrypted: IsEncrypted,
is_one_to_one: IsOneToOne,
) -> RoomNotificationMode {
self.rules.read().await.get_default_room_notification_mode(is_encrypted, is_one_to_one)
}
pub async fn get_rooms_with_user_defined_rules(&self, enabled: Option<bool>) -> Vec<String> {
self.rules.read().await.get_rooms_with_user_defined_rules(enabled)
}
pub async fn contains_keyword_rules(&self) -> bool {
self.rules.read().await.contains_keyword_rules()
}
pub async fn is_push_rule_enabled(
&self,
kind: RuleKind,
rule_id: impl AsRef<str>,
) -> Result<bool, NotificationSettingsError> {
self.rules.read().await.is_enabled(kind, rule_id.as_ref())
}
pub async fn set_push_rule_enabled(
&self,
kind: RuleKind,
rule_id: impl AsRef<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.as_ref(), enabled)?;
self.run_server_commands(&rule_commands).await?;
let rules = &mut *self.rules.write().await;
rules.apply(rule_commands);
Ok(())
}
pub async fn set_default_room_notification_mode(
&self,
is_encrypted: IsEncrypted,
is_one_to_one: IsOneToOne,
mode: RoomNotificationMode,
) -> Result<(), NotificationSettingsError> {
let actions = match mode {
RoomNotificationMode::AllMessages => {
vec![Action::Notify, Action::SetTweak(Tweak::Sound("default".into()))]
}
_ => {
vec![]
}
};
let room_rule_id =
rules::get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one);
self.set_underride_push_rule_actions(room_rule_id, actions.clone()).await?;
let poll_start_rule_id = rules::get_predefined_underride_poll_start_rule_id(is_one_to_one);
if let Err(error) =
self.set_underride_push_rule_actions(poll_start_rule_id, actions.clone()).await
{
if let NotificationSettingsError::RuleNotFound(rule_id) = &error {
debug!("Unable to update poll start push rule: rule `{rule_id}` not found");
} else {
return Err(error);
}
}
Ok(())
}
pub async fn set_underride_push_rule_actions(
&self,
rule_id: PredefinedUnderrideRuleId,
actions: Vec<Action>,
) -> Result<(), NotificationSettingsError> {
let rules = self.rules.read().await.clone();
let rule_kind = RuleKind::Underride;
let mut rule_commands = RuleCommands::new(rules.clone().ruleset);
rule_commands.set_rule_actions(rule_kind.clone(), rule_id.as_str(), actions)?;
if !rules.is_enabled(rule_kind.clone(), rule_id.as_str())? {
rule_commands.set_rule_enabled(rule_kind, rule_id.as_str(), true)?
}
self.run_server_commands(&rule_commands).await?;
let rules = &mut *self.rules.write().await;
rules.apply(rule_commands);
Ok(())
}
pub async fn create_custom_conditional_push_rule(
&self,
rule_id: String,
rule_kind: RuleKind,
actions: Vec<Action>,
conditions: Vec<ruma::push::PushCondition>,
) -> Result<(), NotificationSettingsError> {
let new_conditional_rule =
ruma::push::NewConditionalPushRule::new(rule_id, conditions, actions);
let new_push_rule = match rule_kind {
RuleKind::Override => NewPushRule::Override(new_conditional_rule),
RuleKind::Underride => NewPushRule::Underride(new_conditional_rule),
_ => return Err(NotificationSettingsError::InvalidParameter("rule_kind".to_owned())),
};
let rules = self.rules.read().await.clone();
let mut rule_commands = RuleCommands::new(rules.clone().ruleset);
rule_commands.insert_custom_rule(new_push_rule)?;
self.run_server_commands(&rule_commands).await?;
let rules = &mut *self.rules.write().await;
rules.apply(rule_commands);
Ok(())
}
pub async fn set_room_notification_mode(
&self,
room_id: &RoomId,
mode: RoomNotificationMode,
) -> Result<(), NotificationSettingsError> {
let rules = self.rules.read().await.clone();
if rules.get_user_defined_room_notification_mode(room_id) == Some(mode) {
return Ok(());
}
let (new_rule_kind, notify) = match mode {
RoomNotificationMode::AllMessages => {
(RuleKind::Room, true)
}
RoomNotificationMode::MentionsAndKeywordsOnly => {
(RuleKind::Room, false)
}
RoomNotificationMode::Mute => {
(RuleKind::Override, false)
}
};
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();
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(())
}
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(())
}
pub async fn unmute_room(
&self,
room_id: &RoomId,
is_encrypted: IsEncrypted,
is_one_to_one: IsOneToOne,
) -> Result<(), NotificationSettingsError> {
let rules = self.rules.read().await.clone();
if let Some(room_mode) = rules.get_user_defined_room_notification_mode(room_id) {
if room_mode != RoomNotificationMode::Mute {
return Ok(());
}
let default_mode =
rules.get_default_room_notification_mode(is_encrypted, is_one_to_one);
if default_mode == RoomNotificationMode::Mute {
self.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await
} else {
self.delete_user_defined_room_rules(room_id).await
}
} else {
self.set_room_notification_mode(room_id, RoomNotificationMode::AllMessages).await
}
}
pub async fn enabled_keywords(&self) -> IndexSet<String> {
self.rules.read().await.enabled_keywords()
}
pub async fn add_keyword(&self, keyword: String) -> Result<(), NotificationSettingsError> {
let rules = self.rules.read().await.clone();
let mut rule_commands = RuleCommands::new(rules.clone().ruleset);
let existing_rules = rules.keyword_rules(&keyword);
if existing_rules.is_empty() {
rule_commands.insert_keyword_rule(keyword)?;
} else {
if existing_rules.iter().any(|r| r.enabled) {
return Ok(());
}
rule_commands.set_rule_enabled(RuleKind::Content, &existing_rules[0].rule_id, true)?;
}
self.run_server_commands(&rule_commands).await?;
let rules = &mut *self.rules.write().await;
rules.apply(rule_commands);
Ok(())
}
pub async fn remove_keyword(&self, keyword: &str) -> Result<(), NotificationSettingsError> {
let rules = self.rules.read().await.clone();
let mut rule_commands = RuleCommands::new(rules.clone().ruleset);
let existing_rules = rules.keyword_rules(keyword);
if existing_rules.is_empty() {
return Ok(());
}
for rule in existing_rules {
rule_commands.delete_rule(RuleKind::Content, rule.rule_id.clone())?;
}
self.run_server_commands(&rule_commands).await?;
let rules = &mut *self.rules.write().await;
rules.apply(rule_commands);
Ok(())
}
async fn run_server_commands(
&self,
rule_commands: &RuleCommands,
) -> Result<(), NotificationSettingsError> {
let request_config = Some(RequestConfig::short_retry());
for command in &rule_commands.commands {
match command {
Command::DeletePushRule { kind, rule_id } => {
let request = delete_pushrule::v3::Request::new(kind.clone(), rule_id.clone());
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to delete {kind} push rule `{rule_id}`: {error}");
NotificationSettingsError::UnableToRemovePushRule
},
)?;
}
Command::SetRoomPushRule { room_id, notify: _ } => {
let push_rule = command.to_push_rule()?;
let request = set_pushrule::v3::Request::new(push_rule);
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to set room push rule `{room_id}`: {error}");
NotificationSettingsError::UnableToAddPushRule
},
)?;
}
Command::SetOverridePushRule { rule_id, room_id: _, notify: _ } => {
let push_rule = command.to_push_rule()?;
let request = set_pushrule::v3::Request::new(push_rule);
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to set override push rule `{rule_id}`: {error}");
NotificationSettingsError::UnableToAddPushRule
},
)?;
}
Command::SetKeywordPushRule { keyword: _ } => {
let push_rule = command.to_push_rule()?;
let request = set_pushrule::v3::Request::new(push_rule);
self.client
.send(request)
.with_request_config(request_config)
.await
.map_err(|_| NotificationSettingsError::UnableToAddPushRule)?;
}
Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
let request = set_pushrule_enabled::v3::Request::new(
kind.clone(),
rule_id.clone(),
*enabled,
);
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to set {kind} push rule `{rule_id}` enabled: {error}");
NotificationSettingsError::UnableToUpdatePushRule
},
)?;
}
Command::SetPushRuleActions { kind, rule_id, actions } => {
let request = set_pushrule_actions::v3::Request::new(
kind.clone(),
rule_id.clone(),
actions.clone(),
);
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to set {kind} push rule `{rule_id}` actions: {error}");
NotificationSettingsError::UnableToUpdatePushRule
},
)?;
}
Command::SetCustomPushRule { rule } => {
let request = set_pushrule::v3::Request::new(rule.clone());
self.client.send(request).with_request_config(request_config).await.map_err(
|error| {
error!("Unable to set custom push rule `{rule:#?}`: {error}");
NotificationSettingsError::UnableToAddPushRule
},
)?;
}
}
}
Ok(())
}
pub async fn ruleset(&self) -> Ruleset {
self.rules.read().await.ruleset.clone()
}
pub(crate) async fn rules(&self) -> impl Deref<Target = Rules> + '_ {
self.rules.read().await
}
}
#[cfg(all(test, not(target_family = "wasm")))]
mod tests {
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use assert_matches::assert_matches;
use matrix_sdk_test::{
TestResult, async_test,
event_factory::EventFactory,
notification_settings::{build_ruleset, get_server_default_ruleset},
};
use ruma::{
OwnedRoomId, RoomId, owned_room_id,
push::{
Action, AnyPushRuleRef, NewPatternedPushRule, NewPushRule, PredefinedContentRuleId,
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, RuleKind, Ruleset,
},
};
use stream_assert::{assert_next_eq, assert_pending};
use tokio_stream::wrappers::BroadcastStream;
use wiremock::{
Mock, MockServer, ResponseTemplate,
matchers::{method, path, path_regex},
};
use crate::{
Client,
error::NotificationSettingsError,
notification_settings::{
IsEncrypted, IsOneToOne, NotificationSettings, RoomNotificationMode,
},
test_utils::{logged_in_client, mocks::MatrixMockServer},
};
fn get_test_room_id() -> OwnedRoomId {
owned_room_id!("!AAAaAAAAAaaAAaaaaa:matrix.org")
}
fn from_insert_rules(
client: &Client,
rules: Vec<(RuleKind, &RoomId, bool)>,
) -> NotificationSettings {
let ruleset = build_ruleset(rules);
NotificationSettings::new(client.to_owned(), ruleset)
}
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_subscribe_to_changes() -> TestResult {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let settings = client.notification_settings().await;
let subscriber = settings.subscribe_to_changes();
let mut stream = BroadcastStream::new(subscriber);
assert_pending!(stream);
server
.mock_sync()
.ok_and_run(&client, |sync_response_builder| {
let f = EventFactory::new();
sync_response_builder.add_global_account_data(
f.push_rules(Ruleset::server_default(client.user_id().unwrap())),
);
})
.await;
assert_next_eq!(stream, Ok(()));
assert_pending!(stream);
Ok(())
}
#[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();
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();
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();
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() -> TestResult {
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],
)?;
let settings = NotificationSettings::new(client, ruleset);
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::AllMessages
);
Ok(())
}
#[async_test]
async fn test_get_default_room_notification_mode_mentions_and_keywords() -> TestResult {
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![],
)?;
let settings = NotificationSettings::new(client.to_owned(), ruleset.to_owned());
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::MentionsAndKeywordsOnly
);
ruleset.set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)?;
let settings = NotificationSettings::new(client, ruleset);
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::MentionsAndKeywordsOnly
);
Ok(())
}
#[async_test]
async fn test_contains_keyword_rules() -> TestResult {
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());
let contains_keywords_rules = settings.contains_keyword_rules().await;
assert!(!contains_keywords_rules);
let rule = NewPatternedPushRule::new(
"keyword_rule_id".into(),
"keyword".into(),
vec![Action::Notify],
);
ruleset.insert(NewPushRule::Content(rule), None, None)?;
let settings = NotificationSettings::new(client, ruleset);
let contains_keywords_rules = settings.contains_keyword_rules().await;
assert!(contains_keywords_rules);
Ok(())
}
#[async_test]
async fn test_is_push_rule_enabled() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = get_server_default_ruleset();
ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false)?;
let settings = NotificationSettings::new(client.clone(), ruleset);
let enabled = settings
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction)
.await?;
assert!(!enabled);
let mut ruleset = get_server_default_ruleset();
ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)?;
let settings = NotificationSettings::new(client, ruleset);
let enabled = settings
.is_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction)
.await?;
assert!(enabled);
Ok(())
}
#[async_test]
async fn test_set_push_rule_enabled() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = client.account().push_rules().await?;
ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, false)?;
let settings = NotificationSettings::new(client, ruleset);
Mock::given(method("PUT"))
.and(path("/_matrix/client/r0/pushrules/global/override/.m.rule.reaction/enabled"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
settings
.set_push_rule_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
.await?;
let rules = settings.rules.read().await;
let rule =
rules.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::Reaction).unwrap();
assert!(rule.enabled());
server.verify().await;
Ok(())
}
#[async_test]
async fn test_set_push_rule_enabled_api_error() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = client.account().push_rules().await?;
ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)?;
let settings = NotificationSettings::new(client, ruleset);
Mock::given(method("PUT")).respond_with(ResponseTemplate::new(500)).mount(&server).await;
assert_eq!(
settings
.set_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsUserMention,
true,
)
.await,
Err(NotificationSettingsError::UnableToUpdatePushRule)
);
let rules = settings.rules.read().await;
let rule =
rules.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention).unwrap();
assert!(!rule.enabled());
Ok(())
}
#[async_test]
async fn test_set_room_notification_mode() -> TestResult {
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).await?;
assert_eq!(
new_mode,
settings.get_user_defined_room_notification_mode(&room_id).await.unwrap()
);
}
Ok(())
}
#[async_test]
async fn test_set_room_notification_mode_requests_order() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let put_was_called = Arc::new(AtomicBool::default());
Mock::given(method("PUT"))
.and(path_regex(r"_matrix/client/r0/pushrules/global/override/.*"))
.and({
let put_was_called = put_was_called.clone();
move |_: &wiremock::Request| {
put_was_called.store(true, Ordering::SeqCst);
true
}
})
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path_regex(r"_matrix/client/r0/pushrules/global/room/.*"))
.and(move |_: &wiremock::Request| {
let put_was_called = put_was_called.load(Ordering::SeqCst);
assert!(
put_was_called,
"The PUT /pushrules/global/override/ method should have been called before the \
DELETE method"
);
true
})
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let room_id = get_test_room_id();
let settings = from_insert_rules(&client, vec![(RuleKind::Room, &room_id, true)]);
settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await?;
assert_eq!(
RoomNotificationMode::Mute,
settings.get_user_defined_room_notification_mode(&room_id).await.unwrap()
);
server.verify().await;
Ok(())
}
#[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;
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();
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
);
assert_eq!(
settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await,
Err(NotificationSettingsError::UnableToAddPushRule)
);
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;
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();
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
);
assert_eq!(
settings.set_room_notification_mode(&room_id, RoomNotificationMode::Mute).await,
Err(NotificationSettingsError::UnableToRemovePushRule)
);
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() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let room_id_a = owned_room_id!("!AAAaAAAAAaaAAaaaaa:matrix.org");
let room_id_b = owned_room_id!("!BBBbBBBBBbbBBbbbbb:matrix.org");
Mock::given(method("DELETE")).respond_with(ResponseTemplate::new(200)).mount(&server).await;
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),
],
);
settings.delete_user_defined_room_rules(&room_id_a).await?;
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());
Ok(())
}
#[async_test]
async fn test_unmute_room_not_muted() -> TestResult {
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, false)]);
assert_eq!(
settings.get_user_defined_room_notification_mode(&room_id).await.unwrap(),
RoomNotificationMode::MentionsAndKeywordsOnly
);
settings.unmute_room(&room_id, IsEncrypted::Yes, IsOneToOne::Yes).await?;
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());
}
);
Ok(())
}
#[async_test]
async fn test_unmute_room() -> TestResult {
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();
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)
);
settings.unmute_room(&room_id, IsEncrypted::No, IsOneToOne::Yes).await?;
assert!(settings.get_user_defined_room_notification_mode(&room_id).await.is_none());
Ok(())
}
#[async_test]
async fn test_unmute_room_default_mode() -> TestResult {
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;
settings.unmute_room(&room_id, IsEncrypted::No, IsOneToOne::Yes).await?;
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());
}
);
Ok(())
}
#[async_test]
async fn test_set_default_room_notification_mode() -> TestResult {
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 mut ruleset = get_server_default_ruleset();
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::Message,
vec![Action::Notify],
)?;
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::PollStart,
vec![Action::Notify],
)?;
let settings = NotificationSettings::new(client, ruleset);
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::No).await,
RoomNotificationMode::AllMessages
);
settings
.set_default_room_notification_mode(
IsEncrypted::No,
IsOneToOne::No,
RoomNotificationMode::MentionsAndKeywordsOnly,
)
.await?;
assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Underride, PredefinedUnderrideRuleId::Message),
Some(AnyPushRuleRef::Underride(rule)) => {
assert!(rule.actions.is_empty());
}
);
assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Underride, PredefinedUnderrideRuleId::PollStart),
Some(AnyPushRuleRef::Underride(rule)) => {
assert!(rule.actions.is_empty());
}
);
assert_matches!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::No).await,
RoomNotificationMode::MentionsAndKeywordsOnly
);
Ok(())
}
#[async_test]
async fn test_set_default_room_notification_mode_one_to_one() -> TestResult {
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 mut ruleset = get_server_default_ruleset();
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::RoomOneToOne,
vec![Action::Notify],
)?;
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::PollStartOneToOne,
vec![Action::Notify],
)?;
let settings = NotificationSettings::new(client, ruleset);
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::AllMessages
);
settings
.set_default_room_notification_mode(
IsEncrypted::No,
IsOneToOne::Yes,
RoomNotificationMode::MentionsAndKeywordsOnly,
)
.await?;
assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne),
Some(AnyPushRuleRef::Underride(rule)) => {
assert!(rule.actions.is_empty());
}
);
assert_matches!(settings.rules.read().await.ruleset.get(RuleKind::Underride, PredefinedUnderrideRuleId::PollStartOneToOne),
Some(AnyPushRuleRef::Underride(rule)) => {
assert!(rule.actions.is_empty());
}
);
assert_matches!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::MentionsAndKeywordsOnly
);
Ok(())
}
#[async_test]
async fn test_set_default_room_notification_mode_enables_rules() -> TestResult {
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 mut ruleset = get_server_default_ruleset();
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::RoomOneToOne,
vec![],
)?;
ruleset.set_actions(
RuleKind::Underride,
PredefinedUnderrideRuleId::PollStartOneToOne,
vec![],
)?;
ruleset.set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)?;
let settings = NotificationSettings::new(client, ruleset);
settings
.set_default_room_notification_mode(
IsEncrypted::No,
IsOneToOne::Yes,
RoomNotificationMode::AllMessages,
)
.await?;
assert_matches!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes).await,
RoomNotificationMode::AllMessages
);
Ok(())
}
#[async_test]
async fn test_list_keywords() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let ruleset = get_server_default_ruleset();
let settings = NotificationSettings::new(client.clone(), ruleset);
let keywords = settings.enabled_keywords().await;
assert!(keywords.is_empty());
let mut ruleset = get_server_default_ruleset();
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new("a".to_owned(), "a".to_owned(), vec![])),
None,
None,
)?;
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"a_bis".to_owned(),
"a".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new("b".to_owned(), "b".to_owned(), vec![])),
None,
None,
)?;
let settings = NotificationSettings::new(client, ruleset);
let keywords = settings.enabled_keywords().await;
assert_eq!(keywords.len(), 2);
assert!(keywords.get("a").is_some());
assert!(keywords.get("b").is_some());
Ok(())
}
#[async_test]
async fn test_add_keyword_missing() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let settings = client.notification_settings().await;
Mock::given(method("PUT"))
.and(path("/_matrix/client/r0/pushrules/global/content/banana"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
settings.add_keyword("banana".to_owned()).await?;
let keywords = settings.enabled_keywords().await;
assert_eq!(keywords.len(), 1);
assert!(keywords.get("banana").is_some());
let rule_enabled = settings.is_push_rule_enabled(RuleKind::Content, "banana").await?;
assert!(rule_enabled);
Ok(())
}
#[async_test]
async fn test_add_keyword_disabled() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = get_server_default_ruleset();
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_two".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.set_enabled(RuleKind::Content, "banana_two", false)?;
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_one".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.set_enabled(RuleKind::Content, "banana_one", false)?;
let settings = NotificationSettings::new(client, ruleset);
Mock::given(method("PUT"))
.and(path("/_matrix/client/r0/pushrules/global/content/banana_one/enabled"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
settings.add_keyword("banana".to_owned()).await?;
let keywords = settings.enabled_keywords().await;
assert_eq!(keywords.len(), 1);
assert!(keywords.get("banana").is_some());
let first_rule_enabled =
settings.is_push_rule_enabled(RuleKind::Content, "banana_one").await?;
assert!(first_rule_enabled);
let second_rule_enabled =
settings.is_push_rule_enabled(RuleKind::Content, "banana_two").await?;
assert!(!second_rule_enabled);
Ok(())
}
#[async_test]
async fn test_add_keyword_noop() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = get_server_default_ruleset();
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_two".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_one".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.set_enabled(RuleKind::Content, "banana_one", false)?;
let settings = NotificationSettings::new(client, ruleset);
settings.add_keyword("banana".to_owned()).await?;
let keywords = settings.enabled_keywords().await;
assert_eq!(keywords.len(), 1);
assert!(keywords.get("banana").is_some());
let first_rule_enabled =
settings.is_push_rule_enabled(RuleKind::Content, "banana_one").await?;
assert!(!first_rule_enabled);
let second_rule_enabled =
settings.is_push_rule_enabled(RuleKind::Content, "banana_two").await?;
assert!(second_rule_enabled);
Ok(())
}
#[async_test]
async fn test_remove_keyword_all() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let mut ruleset = get_server_default_ruleset();
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_two".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.insert(
NewPushRule::Content(NewPatternedPushRule::new(
"banana_one".to_owned(),
"banana".to_owned(),
vec![],
)),
None,
None,
)?;
ruleset.set_enabled(RuleKind::Content, "banana_one", false)?;
let settings = NotificationSettings::new(client, ruleset);
Mock::given(method("DELETE"))
.and(path("/_matrix/client/r0/pushrules/global/content/banana_one"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/_matrix/client/r0/pushrules/global/content/banana_two"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
settings.remove_keyword("banana").await?;
let keywords = settings.enabled_keywords().await;
assert!(keywords.is_empty());
let first_rule_error =
settings.is_push_rule_enabled(RuleKind::Content, "banana_one").await.unwrap_err();
assert_matches!(first_rule_error, NotificationSettingsError::RuleNotFound(_));
let second_rule_error =
settings.is_push_rule_enabled(RuleKind::Content, "banana_two").await.unwrap_err();
assert_matches!(second_rule_error, NotificationSettingsError::RuleNotFound(_));
Ok(())
}
#[async_test]
async fn test_remove_keyword_noop() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let settings = client.notification_settings().await;
settings.remove_keyword("banana").await?;
Ok(())
}
#[async_test]
async fn test_set_default_room_notification_mode_missing_poll_start() -> TestResult {
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 mut ruleset = get_server_default_ruleset();
ruleset.underride.swap_remove(PredefinedUnderrideRuleId::PollStart.as_str());
let settings = NotificationSettings::new(client, ruleset);
assert_eq!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::No).await,
RoomNotificationMode::AllMessages
);
settings
.set_default_room_notification_mode(
IsEncrypted::No,
IsOneToOne::No,
RoomNotificationMode::MentionsAndKeywordsOnly,
)
.await?;
assert_matches!(
settings.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::No).await,
RoomNotificationMode::MentionsAndKeywordsOnly
);
Ok(())
}
#[async_test]
async fn test_create_custom_conditional_push_rule() -> TestResult {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let settings = client.notification_settings().await;
Mock::given(method("PUT"))
.and(path("/_matrix/client/r0/pushrules/global/override/custom_rule"))
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&server)
.await;
let actions = vec![Action::Notify];
let conditions = vec![ruma::push::PushCondition::EventMatch {
key: "content.body".to_owned(),
pattern: "hello".to_owned(),
}];
settings
.create_custom_conditional_push_rule(
"custom_rule".to_owned(),
RuleKind::Override,
actions.clone(),
conditions.clone(),
)
.await?;
let rules = settings.rules.read().await;
let rule = rules.ruleset.get(RuleKind::Override, "custom_rule").unwrap();
assert_eq!(rule.rule_id(), "custom_rule");
assert!(rule.enabled());
Ok(())
}
#[async_test]
async fn test_create_custom_conditional_push_rule_invalid_kind() {
let server = MockServer::start().await;
let client = logged_in_client(Some(server.uri())).await;
let settings = client.notification_settings().await;
let actions = vec![Action::Notify];
let conditions = vec![ruma::push::PushCondition::EventMatch {
key: "content.body".to_owned(),
pattern: "hello".to_owned(),
}];
let result = settings
.create_custom_conditional_push_rule(
"custom_rule".to_owned(),
RuleKind::Room,
actions,
conditions,
)
.await;
assert_matches!(result, Err(NotificationSettingsError::InvalidParameter(_)));
}
#[async_test]
#[allow(deprecated)]
async fn test_enable_mention_ignore_missing_legacy_push_rules() -> TestResult {
let server = MatrixMockServer::new().await;
let client = server.client_builder().build().await;
let mut ruleset = get_server_default_ruleset();
if let Some(idx) = ruleset
.override_
.iter()
.position(|rule| rule.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
{
ruleset.override_.shift_remove_index(idx);
}
if let Some(idx) = ruleset
.override_
.iter()
.position(|rule| rule.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref())
{
ruleset.override_.shift_remove_index(idx);
}
if let Some(idx) = ruleset
.content
.iter()
.position(|rule| rule.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref())
{
ruleset.content.shift_remove_index(idx);
}
assert_matches!(
ruleset.iter().find(|rule| {
rule.rule_id() == PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
|| rule.rule_id() == PredefinedOverrideRuleId::RoomNotif.as_ref()
|| rule.rule_id() == PredefinedContentRuleId::ContainsUserName.as_ref()
}),
None,
"ruleset must not have legacy mention push rules"
);
let settings = NotificationSettings::new(client, ruleset);
server
.mock_enable_push_rule(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
.ok()
.mock_once()
.named("is_user_mention")
.mount()
.await;
settings
.set_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsUserMention,
false,
)
.await?;
server
.mock_enable_push_rule(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
.ok()
.mock_once()
.named("is_room_mention")
.mount()
.await;
settings
.set_push_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::IsRoomMention,
false,
)
.await?;
Ok(())
}
}