use imbl::HashSet;
use indexmap::IndexSet;
use ruma::{
push::{
AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
},
RoomId,
};
use super::{command::Command, rule_commands::RuleCommands, RoomNotificationMode};
use crate::{
error::NotificationSettingsError,
notification_settings::{IsEncrypted, IsOneToOne},
};
#[derive(Clone, Debug)]
pub(crate) struct Rules {
pub ruleset: Ruleset,
}
impl Rules {
pub(crate) fn new(ruleset: Ruleset) -> Self {
Rules { ruleset }
}
pub(crate) fn get_custom_rules_for_room(&self, room_id: &RoomId) -> Vec<(RuleKind, String)> {
let mut custom_rules = vec![];
for rule in &self.ruleset.override_ {
if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
x,
PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
)) {
custom_rules.push((RuleKind::Override, rule.rule_id.clone()));
}
}
if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
custom_rules.push((RuleKind::Room, rule.rule_id().to_owned()));
}
for rule in &self.ruleset.underride {
if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
x,
PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
)) {
custom_rules.push((RuleKind::Underride, rule.rule_id.clone()));
}
}
custom_rules
}
pub(crate) fn get_user_defined_room_notification_mode(
&self,
room_id: &RoomId,
) -> Option<RoomNotificationMode> {
if self.ruleset.override_.iter().any(|x| {
x.enabled &&
x.conditions.iter().any(|x| matches!(
x,
PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
)) &&
!x.actions.iter().any(|x| x.should_notify())
}) {
return Some(RoomNotificationMode::Mute);
}
if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
if rule.triggers_notification() {
return Some(RoomNotificationMode::AllMessages);
}
return Some(RoomNotificationMode::MentionsAndKeywordsOnly);
}
None
}
pub(crate) fn get_default_room_notification_mode(
&self,
is_encrypted: IsEncrypted,
is_one_to_one: IsOneToOne,
) -> RoomNotificationMode {
let predefined_rule_id = get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one);
let rule_id = predefined_rule_id.as_str();
if self
.ruleset
.get(RuleKind::Underride, rule_id)
.is_some_and(|r| r.enabled() && r.triggers_notification())
{
RoomNotificationMode::AllMessages
} else {
RoomNotificationMode::MentionsAndKeywordsOnly
}
}
pub(crate) fn get_rooms_with_user_defined_rules(&self, enabled: Option<bool>) -> Vec<String> {
let test_if_enabled = enabled.is_some();
let must_be_enabled = enabled.unwrap_or(false);
let mut room_ids = HashSet::new();
for rule in &self.ruleset {
if rule.is_server_default() {
continue;
}
if test_if_enabled && rule.enabled() != must_be_enabled {
continue;
}
match rule {
AnyPushRuleRef::Override(r) | AnyPushRuleRef::Underride(r) => {
for condition in &r.conditions {
if let PushCondition::EventMatch { key, pattern } = condition {
if key == "room_id" {
room_ids.insert(pattern.clone());
break;
}
}
}
}
AnyPushRuleRef::Room(r) => {
room_ids.insert(r.rule_id.to_string());
}
_ => {}
}
}
Vec::from_iter(room_ids)
}
fn is_user_mention_enabled(&self) -> bool {
if let Some(rule) =
self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
{
return rule.enabled();
}
#[allow(deprecated)]
if let Some(rule) =
self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName)
{
if rule.enabled() && rule.triggers_notification() {
return true;
}
}
#[allow(deprecated)]
if let Some(rule) =
self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
{
if rule.enabled() && rule.triggers_notification() {
return true;
}
}
false
}
fn is_room_mention_enabled(&self) -> bool {
if let Some(rule) =
self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
{
return rule.enabled();
}
#[allow(deprecated)]
self.ruleset
.get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
.is_some_and(|r| r.enabled() && r.triggers_notification())
}
pub(crate) fn contains_keyword_rules(&self) -> bool {
self.ruleset.content.iter().any(|r| !r.default && r.enabled)
}
pub(crate) fn enabled_keywords(&self) -> IndexSet<String> {
self.ruleset
.content
.iter()
.filter(|r| !r.default && r.enabled)
.map(|r| r.pattern.clone())
.collect()
}
pub(crate) fn keyword_rules(&self, keyword: &str) -> Vec<&PatternedPushRule> {
self.ruleset.content.iter().filter(|r| !r.default && r.pattern == keyword).collect()
}
pub(crate) fn is_enabled(
&self,
kind: RuleKind,
rule_id: &str,
) -> Result<bool, NotificationSettingsError> {
if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() {
Ok(self.is_room_mention_enabled())
} else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() {
Ok(self.is_user_mention_enabled())
} else if let Some(rule) = self.ruleset.get(kind, rule_id) {
Ok(rule.enabled())
} else {
Err(NotificationSettingsError::RuleNotFound(rule_id.to_owned()))
}
}
pub(crate) fn apply(&mut self, commands: RuleCommands) {
for command in commands.commands {
match command {
Command::DeletePushRule { kind, rule_id } => {
_ = self.ruleset.remove(kind, rule_id);
}
Command::SetRoomPushRule { .. }
| Command::SetOverridePushRule { .. }
| Command::SetKeywordPushRule { .. } => {
if let Ok(push_rule) = command.to_push_rule() {
_ = self.ruleset.insert(push_rule, None, None);
}
}
Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
_ = self.ruleset.set_enabled(kind, rule_id, enabled);
}
Command::SetPushRuleActions { kind, rule_id, actions } => {
_ = self.ruleset.set_actions(kind, rule_id, actions);
}
}
}
}
}
pub(crate) fn get_predefined_underride_room_rule_id(
is_encrypted: IsEncrypted,
is_one_to_one: IsOneToOne,
) -> PredefinedUnderrideRuleId {
match (is_encrypted, is_one_to_one) {
(IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
(IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
(IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
(IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
}
}
pub(crate) fn get_predefined_underride_poll_start_rule_id(
is_one_to_one: IsOneToOne,
) -> PredefinedUnderrideRuleId {
match is_one_to_one {
IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
}
}
#[cfg(test)]
pub(crate) mod tests {
use imbl::HashSet;
use matrix_sdk_test::{
async_test,
notification_settings::{build_ruleset, get_server_default_ruleset},
};
use ruma::{
push::{
Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
},
OwnedRoomId, RoomId,
};
use super::RuleCommands;
use crate::{
error::NotificationSettingsError,
notification_settings::{
rules::{self, Rules},
IsEncrypted, IsOneToOne, RoomNotificationMode,
},
};
fn get_test_room_id() -> OwnedRoomId {
RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
}
#[async_test]
async fn test_get_custom_rules_for_room() {
let room_id = get_test_room_id();
let rules = Rules::new(get_server_default_ruleset());
assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
let rules = Rules::new(ruleset);
assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
let ruleset = build_ruleset(vec![
(RuleKind::Override, &room_id, false),
(RuleKind::Room, &room_id, false),
]);
let rules = Rules::new(ruleset);
assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
}
#[async_test]
async fn test_get_custom_rules_for_room_special_override_rule() {
let room_id = get_test_room_id();
let mut ruleset = get_server_default_ruleset();
let new_rule = NewConditionalPushRule::new(
"custom_rule_id".to_owned(),
vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
vec![Action::Notify],
);
ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
let rules = Rules::new(ruleset);
assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
}
#[async_test]
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);
let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
let rules = Rules::new(ruleset);
assert_eq!(
rules.get_user_defined_room_notification_mode(&room_id),
Some(RoomNotificationMode::Mute)
);
let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
let rules = Rules::new(ruleset);
assert_eq!(
rules.get_user_defined_room_notification_mode(&room_id),
Some(RoomNotificationMode::MentionsAndKeywordsOnly)
);
let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
let rules = Rules::new(ruleset);
assert_eq!(
rules.get_user_defined_room_notification_mode(&room_id),
Some(RoomNotificationMode::AllMessages)
);
let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
let ruleset = build_ruleset(vec![
(RuleKind::Override, &room_id_a, false),
(RuleKind::Override, &room_id_b, true),
]);
let rules = Rules::new(ruleset);
let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
assert_eq!(mode, Some(RoomNotificationMode::Mute));
}
#[async_test]
async fn test_get_predefined_underride_room_rule_id() {
assert_eq!(
rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
PredefinedUnderrideRuleId::Message
);
assert_eq!(
rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
PredefinedUnderrideRuleId::RoomOneToOne
);
assert_eq!(
rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
PredefinedUnderrideRuleId::Encrypted
);
assert_eq!(
rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
PredefinedUnderrideRuleId::EncryptedRoomOneToOne
);
}
#[async_test]
async fn test_get_predefined_underride_poll_start_rule_id() {
assert_eq!(
rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
PredefinedUnderrideRuleId::PollStart
);
assert_eq!(
rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
PredefinedUnderrideRuleId::PollStartOneToOne
);
}
#[async_test]
async fn test_get_default_room_notification_mode_mentions_and_keywords() {
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
.unwrap();
let rules = Rules::new(ruleset);
let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
}
#[async_test]
async fn test_get_default_room_notification_mode_all_messages() {
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
.unwrap();
let rules = Rules::new(ruleset);
let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
assert_eq!(mode, RoomNotificationMode::AllMessages);
}
#[async_test]
async fn test_is_user_mention_enabled() {
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
.unwrap();
#[allow(deprecated)]
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
.unwrap();
#[allow(deprecated)]
ruleset
.set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
.unwrap();
let rules = Rules::new(ruleset);
assert!(rules.is_user_mention_enabled());
assert!(rules
.is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
.unwrap());
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
.unwrap();
#[allow(deprecated)]
ruleset
.set_actions(
RuleKind::Override,
PredefinedOverrideRuleId::ContainsDisplayName,
vec![Action::Notify],
)
.unwrap();
#[allow(deprecated)]
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
.unwrap();
#[allow(deprecated)]
ruleset
.set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
.unwrap();
let rules = Rules::new(ruleset);
assert!(!rules.is_user_mention_enabled());
assert!(!rules
.is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
.unwrap());
}
#[async_test]
async fn test_is_room_mention_enabled() {
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
.unwrap();
#[allow(deprecated)]
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
.unwrap();
let rules = Rules::new(ruleset);
assert!(rules.is_room_mention_enabled());
assert!(rules
.is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
.unwrap());
let mut ruleset = get_server_default_ruleset();
ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
.unwrap();
#[allow(deprecated)]
ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
let rules = Rules::new(ruleset);
assert!(!rules.is_room_mention_enabled());
assert!(!rules
.is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
.unwrap());
}
#[async_test]
async fn test_is_enabled_rule_not_found() {
let rules = Rules::new(get_server_default_ruleset());
assert_eq!(
rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
);
}
#[async_test]
async fn test_apply_delete_command() {
let room_id = get_test_room_id();
let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
let mut rules = Rules::new(ruleset);
let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
rules.apply(rules_commands);
assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
}
#[async_test]
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_commands = RuleCommands::new(rules.ruleset.clone());
rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
rules.apply(rules_commands);
assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
}
#[async_test]
async fn test_apply_set_enabled_command() {
let mut rules = Rules::new(get_server_default_ruleset());
rules
.ruleset
.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
.unwrap();
let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
rules_commands
.set_rule_enabled(
RuleKind::Override,
PredefinedOverrideRuleId::Reaction.as_str(),
false,
)
.unwrap();
rules.apply(rules_commands);
assert!(!rules
.is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
.unwrap());
}
#[async_test]
async fn test_get_rooms_with_user_defined_rules() {
let rules = Rules::new(get_server_default_ruleset());
let room_ids = rules.get_rooms_with_user_defined_rules(None);
assert!(room_ids.is_empty());
let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
let rules = Rules::new(ruleset);
let room_ids = rules.get_rooms_with_user_defined_rules(None);
assert_eq!(room_ids.len(), 1);
let ruleset = build_ruleset(vec![
(RuleKind::Override, &room_id, false),
(RuleKind::Underride, &room_id, false),
(RuleKind::Room, &room_id, false),
]);
let rules = Rules::new(ruleset);
let room_ids = rules.get_rooms_with_user_defined_rules(None);
assert_eq!(room_ids.len(), 1);
assert_eq!(room_ids[0], room_id.to_string());
let ruleset = build_ruleset(vec![
(RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
(RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
(RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
(RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
(RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
]);
let rules = Rules::new(ruleset);
let room_ids = rules.get_rooms_with_user_defined_rules(None);
assert_eq!(room_ids.len(), 5);
let expected_set: HashSet<String> = vec![
"!room_a:matrix.org",
"!room_b:matrix.org",
"!room_c:matrix.org",
"!room_d:matrix.org",
"!room_e:matrix.org",
]
.into_iter()
.collect();
assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
assert_eq!(room_ids.len(), 0);
let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
assert_eq!(room_ids.len(), 5);
let mut ruleset = build_ruleset(vec![
(RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
(RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
(RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
(RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
]);
ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
let rules = Rules::new(ruleset);
let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
assert_eq!(room_ids.len(), 2);
let expected_set: HashSet<String> =
vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
}
}