1use imbl::HashSet;
4use indexmap::IndexSet;
5use ruma::{
6 push::{
7 AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
8 PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
9 },
10 RoomId,
11};
12
13use super::{command::Command, rule_commands::RuleCommands, RoomNotificationMode};
14use crate::{
15 error::NotificationSettingsError,
16 notification_settings::{IsEncrypted, IsOneToOne},
17};
18
19#[derive(Clone, Debug)]
20pub(crate) struct Rules {
21 pub ruleset: Ruleset,
22}
23
24impl Rules {
25 pub(crate) fn new(ruleset: Ruleset) -> Self {
26 Rules { ruleset }
27 }
28
29 pub(crate) fn get_custom_rules_for_room(&self, room_id: &RoomId) -> Vec<(RuleKind, String)> {
31 let mut custom_rules = vec![];
32
33 for rule in &self.ruleset.override_ {
35 if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
37 x,
38 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
39 )) {
40 custom_rules.push((RuleKind::Override, rule.rule_id.clone()));
42 }
43 }
44
45 if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
47 custom_rules.push((RuleKind::Room, rule.rule_id().to_owned()));
48 }
49
50 for rule in &self.ruleset.underride {
52 if &rule.rule_id == room_id || rule.conditions.iter().any(|x| matches!(
54 x,
55 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
56 )) {
57 custom_rules.push((RuleKind::Underride, rule.rule_id.clone()));
59 }
60 }
61
62 custom_rules
63 }
64
65 pub(crate) fn get_user_defined_room_notification_mode(
67 &self,
68 room_id: &RoomId,
69 ) -> Option<RoomNotificationMode> {
70 if self.ruleset.override_.iter().any(|x| {
72 x.enabled &&
74 x.conditions.iter().any(|x| matches!(
77 x,
78 PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
79 )) &&
80 !x.actions.iter().any(|x| x.should_notify())
82 }) {
83 return Some(RoomNotificationMode::Mute);
84 }
85
86 if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
88 if rule.triggers_notification() {
90 return Some(RoomNotificationMode::AllMessages);
91 }
92 return Some(RoomNotificationMode::MentionsAndKeywordsOnly);
93 }
94
95 None
97 }
98
99 pub(crate) fn get_default_room_notification_mode(
107 &self,
108 is_encrypted: IsEncrypted,
109 is_one_to_one: IsOneToOne,
110 ) -> RoomNotificationMode {
111 let predefined_rule_id = get_predefined_underride_room_rule_id(is_encrypted, is_one_to_one);
113 let rule_id = predefined_rule_id.as_str();
114
115 if self
118 .ruleset
119 .get(RuleKind::Underride, rule_id)
120 .is_some_and(|r| r.enabled() && r.triggers_notification())
121 {
122 RoomNotificationMode::AllMessages
123 } else {
124 RoomNotificationMode::MentionsAndKeywordsOnly
126 }
127 }
128
129 pub(crate) fn get_rooms_with_user_defined_rules(&self, enabled: Option<bool>) -> Vec<String> {
131 let test_if_enabled = enabled.is_some();
132 let must_be_enabled = enabled.unwrap_or(false);
133
134 let mut room_ids = HashSet::new();
135 for rule in &self.ruleset {
136 if rule.is_server_default() {
137 continue;
138 }
139 if test_if_enabled && rule.enabled() != must_be_enabled {
140 continue;
141 }
142 match rule {
143 AnyPushRuleRef::Override(r) | AnyPushRuleRef::Underride(r) => {
144 for condition in &r.conditions {
145 if let PushCondition::EventMatch { key, pattern } = condition {
146 if key == "room_id" {
147 room_ids.insert(pattern.clone());
148 break;
149 }
150 }
151 }
152 }
153 AnyPushRuleRef::Room(r) => {
154 room_ids.insert(r.rule_id.to_string());
155 }
156 _ => {}
157 }
158 }
159 Vec::from_iter(room_ids)
160 }
161
162 fn is_user_mention_enabled(&self) -> bool {
164 if let Some(rule) =
167 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
168 {
169 return rule.enabled();
170 }
171
172 #[allow(deprecated)]
174 if let Some(rule) =
175 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName)
176 {
177 if rule.enabled() && rule.triggers_notification() {
178 return true;
179 }
180 }
181
182 #[allow(deprecated)]
183 if let Some(rule) =
184 self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
185 {
186 if rule.enabled() && rule.triggers_notification() {
187 return true;
188 }
189 }
190
191 false
192 }
193
194 fn is_room_mention_enabled(&self) -> bool {
196 if let Some(rule) =
199 self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
200 {
201 return rule.enabled();
202 }
203
204 #[allow(deprecated)]
206 self.ruleset
207 .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
208 .is_some_and(|r| r.enabled() && r.triggers_notification())
209 }
210
211 pub(crate) fn contains_keyword_rules(&self) -> bool {
213 self.ruleset.content.iter().any(|r| !r.default && r.enabled)
215 }
216
217 pub(crate) fn enabled_keywords(&self) -> IndexSet<String> {
219 self.ruleset
220 .content
221 .iter()
222 .filter(|r| !r.default && r.enabled)
223 .map(|r| r.pattern.clone())
224 .collect()
225 }
226
227 pub(crate) fn keyword_rules(&self, keyword: &str) -> Vec<&PatternedPushRule> {
229 self.ruleset.content.iter().filter(|r| !r.default && r.pattern == keyword).collect()
230 }
231
232 pub(crate) fn is_enabled(
234 &self,
235 kind: RuleKind,
236 rule_id: &str,
237 ) -> Result<bool, NotificationSettingsError> {
238 if rule_id == PredefinedOverrideRuleId::IsRoomMention.as_str() {
239 Ok(self.is_room_mention_enabled())
240 } else if rule_id == PredefinedOverrideRuleId::IsUserMention.as_str() {
241 Ok(self.is_user_mention_enabled())
242 } else if let Some(rule) = self.ruleset.get(kind, rule_id) {
243 Ok(rule.enabled())
244 } else {
245 Err(NotificationSettingsError::RuleNotFound(rule_id.to_owned()))
246 }
247 }
248
249 pub(crate) fn apply(&mut self, commands: RuleCommands) {
254 for command in commands.commands {
255 match command {
256 Command::DeletePushRule { kind, rule_id } => {
257 _ = self.ruleset.remove(kind, rule_id);
258 }
259 Command::SetRoomPushRule { .. }
260 | Command::SetOverridePushRule { .. }
261 | Command::SetKeywordPushRule { .. } => {
262 if let Ok(push_rule) = command.to_push_rule() {
263 _ = self.ruleset.insert(push_rule, None, None);
264 }
265 }
266 Command::SetPushRuleEnabled { kind, rule_id, enabled } => {
267 _ = self.ruleset.set_enabled(kind, rule_id, enabled);
268 }
269 Command::SetPushRuleActions { kind, rule_id, actions } => {
270 _ = self.ruleset.set_actions(kind, rule_id, actions);
271 }
272 Command::SetCustomPushRule { rule } => {
273 _ = self.ruleset.insert(rule, None, None);
274 }
275 }
276 }
277 }
278}
279
280pub(crate) fn get_predefined_underride_room_rule_id(
288 is_encrypted: IsEncrypted,
289 is_one_to_one: IsOneToOne,
290) -> PredefinedUnderrideRuleId {
291 match (is_encrypted, is_one_to_one) {
292 (IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
293 (IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
294 (IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
295 (IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
296 }
297}
298
299pub(crate) fn get_predefined_underride_poll_start_rule_id(
306 is_one_to_one: IsOneToOne,
307) -> PredefinedUnderrideRuleId {
308 match is_one_to_one {
309 IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
310 IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
311 }
312}
313
314#[cfg(test)]
315pub(crate) mod tests {
316 use imbl::HashSet;
317 use matrix_sdk_test::{
318 async_test,
319 notification_settings::{build_ruleset, get_server_default_ruleset},
320 };
321 use ruma::{
322 push::{
323 Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
324 PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
325 },
326 OwnedRoomId, RoomId,
327 };
328
329 use super::RuleCommands;
330 use crate::{
331 error::NotificationSettingsError,
332 notification_settings::{
333 rules::{self, Rules},
334 IsEncrypted, IsOneToOne, RoomNotificationMode,
335 },
336 };
337
338 fn get_test_room_id() -> OwnedRoomId {
339 RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
340 }
341
342 #[async_test]
343 async fn test_get_custom_rules_for_room() {
344 let room_id = get_test_room_id();
345
346 let rules = Rules::new(get_server_default_ruleset());
347 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
348
349 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
351 let rules = Rules::new(ruleset);
352 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
353
354 let ruleset = build_ruleset(vec![
356 (RuleKind::Override, &room_id, false),
357 (RuleKind::Room, &room_id, false),
358 ]);
359 let rules = Rules::new(ruleset);
360 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
361 }
362
363 #[async_test]
364 async fn test_get_custom_rules_for_room_special_override_rule() {
365 let room_id = get_test_room_id();
366 let mut ruleset = get_server_default_ruleset();
367
368 let new_rule = NewConditionalPushRule::new(
371 "custom_rule_id".to_owned(),
372 vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
373 vec![Action::Notify],
374 );
375 ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
376
377 let rules = Rules::new(ruleset);
378 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
379 }
380
381 #[async_test]
382 async fn test_get_user_defined_room_notification_mode() {
383 let room_id = get_test_room_id();
384 let rules = Rules::new(get_server_default_ruleset());
385 assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
386
387 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
389 let rules = Rules::new(ruleset);
390 assert_eq!(
391 rules.get_user_defined_room_notification_mode(&room_id),
392 Some(RoomNotificationMode::Mute)
393 );
394
395 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
397 let rules = Rules::new(ruleset);
398 assert_eq!(
399 rules.get_user_defined_room_notification_mode(&room_id),
400 Some(RoomNotificationMode::MentionsAndKeywordsOnly)
401 );
402
403 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
405 let rules = Rules::new(ruleset);
406 assert_eq!(
407 rules.get_user_defined_room_notification_mode(&room_id),
408 Some(RoomNotificationMode::AllMessages)
409 );
410
411 let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
412 let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
413 let ruleset = build_ruleset(vec![
414 (RuleKind::Override, &room_id_a, false),
416 (RuleKind::Override, &room_id_b, true),
418 ]);
419 let rules = Rules::new(ruleset);
420 let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
421
422 assert_eq!(mode, Some(RoomNotificationMode::Mute));
425 }
426
427 #[async_test]
428 async fn test_get_predefined_underride_room_rule_id() {
429 assert_eq!(
430 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
431 PredefinedUnderrideRuleId::Message
432 );
433 assert_eq!(
434 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
435 PredefinedUnderrideRuleId::RoomOneToOne
436 );
437 assert_eq!(
438 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
439 PredefinedUnderrideRuleId::Encrypted
440 );
441 assert_eq!(
442 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
443 PredefinedUnderrideRuleId::EncryptedRoomOneToOne
444 );
445 }
446
447 #[async_test]
448 async fn test_get_predefined_underride_poll_start_rule_id() {
449 assert_eq!(
450 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
451 PredefinedUnderrideRuleId::PollStart
452 );
453 assert_eq!(
454 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
455 PredefinedUnderrideRuleId::PollStartOneToOne
456 );
457 }
458
459 #[async_test]
460 async fn test_get_default_room_notification_mode_mentions_and_keywords() {
461 let mut ruleset = get_server_default_ruleset();
462 ruleset
464 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
465 .unwrap();
466
467 let rules = Rules::new(ruleset);
468 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
469 assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
471 }
472
473 #[async_test]
474 async fn test_get_default_room_notification_mode_all_messages() {
475 let mut ruleset = get_server_default_ruleset();
476 ruleset
478 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
479 .unwrap();
480
481 let rules = Rules::new(ruleset);
482 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
483 assert_eq!(mode, RoomNotificationMode::AllMessages);
485 }
486
487 #[async_test]
488 async fn test_is_user_mention_enabled() {
489 let mut ruleset = get_server_default_ruleset();
492 ruleset
493 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
494 .unwrap();
495 #[allow(deprecated)]
496 ruleset
497 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
498 .unwrap();
499 #[allow(deprecated)]
500 ruleset
501 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
502 .unwrap();
503
504 let rules = Rules::new(ruleset);
505 assert!(rules.is_user_mention_enabled());
506 assert!(rules
509 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
510 .unwrap());
511
512 let mut ruleset = get_server_default_ruleset();
515 ruleset
516 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
517 .unwrap();
518 #[allow(deprecated)]
519 ruleset
520 .set_actions(
521 RuleKind::Override,
522 PredefinedOverrideRuleId::ContainsDisplayName,
523 vec![Action::Notify],
524 )
525 .unwrap();
526 #[allow(deprecated)]
527 ruleset
528 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
529 .unwrap();
530 #[allow(deprecated)]
531 ruleset
532 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
533 .unwrap();
534
535 let rules = Rules::new(ruleset);
536 assert!(!rules.is_user_mention_enabled());
537 assert!(!rules
540 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
541 .unwrap());
542 }
543
544 #[async_test]
545 async fn test_is_room_mention_enabled() {
546 let mut ruleset = get_server_default_ruleset();
549 ruleset
550 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
551 .unwrap();
552 #[allow(deprecated)]
553 ruleset
554 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
555 .unwrap();
556
557 let rules = Rules::new(ruleset);
558 assert!(rules.is_room_mention_enabled());
559 assert!(rules
562 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
563 .unwrap());
564
565 let mut ruleset = get_server_default_ruleset();
568 ruleset
569 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
570 .unwrap();
571 #[allow(deprecated)]
572 ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
573
574 let rules = Rules::new(ruleset);
575 assert!(!rules.is_room_mention_enabled());
576 assert!(!rules
579 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
580 .unwrap());
581 }
582
583 #[async_test]
584 async fn test_is_enabled_rule_not_found() {
585 let rules = Rules::new(get_server_default_ruleset());
586
587 assert_eq!(
588 rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
589 Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
590 );
591 }
592
593 #[async_test]
594 async fn test_apply_delete_command() {
595 let room_id = get_test_room_id();
596 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
598 let mut rules = Rules::new(ruleset);
599
600 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
602 rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
603
604 rules.apply(rules_commands);
605
606 assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
608 }
609
610 #[async_test]
611 async fn test_apply_set_command() {
612 let room_id = get_test_room_id();
613 let mut rules = Rules::new(get_server_default_ruleset());
614
615 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
617 rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
618
619 rules.apply(rules_commands);
620
621 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
623 }
624
625 #[async_test]
626 async fn test_apply_set_enabled_command() {
627 let mut rules = Rules::new(get_server_default_ruleset());
628
629 rules
630 .ruleset
631 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
632 .unwrap();
633
634 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
636 rules_commands
637 .set_rule_enabled(
638 RuleKind::Override,
639 PredefinedOverrideRuleId::Reaction.as_str(),
640 false,
641 )
642 .unwrap();
643
644 rules.apply(rules_commands);
645
646 assert!(!rules
648 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
649 .unwrap());
650 }
651
652 #[async_test]
653 async fn test_get_rooms_with_user_defined_rules() {
654 let rules = Rules::new(get_server_default_ruleset());
656 let room_ids = rules.get_rooms_with_user_defined_rules(None);
657 assert!(room_ids.is_empty());
658
659 let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
661 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
662 let rules = Rules::new(ruleset);
663
664 let room_ids = rules.get_rooms_with_user_defined_rules(None);
665 assert_eq!(room_ids.len(), 1);
666
667 let ruleset = build_ruleset(vec![
669 (RuleKind::Override, &room_id, false),
670 (RuleKind::Underride, &room_id, false),
671 (RuleKind::Room, &room_id, false),
672 ]);
673 let rules = Rules::new(ruleset);
674
675 let room_ids = rules.get_rooms_with_user_defined_rules(None);
676 assert_eq!(room_ids.len(), 1);
677 assert_eq!(room_ids[0], room_id.to_string());
678
679 let ruleset = build_ruleset(vec![
681 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
682 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
683 (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
684 (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
685 (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
686 ]);
687 let rules = Rules::new(ruleset);
688
689 let room_ids = rules.get_rooms_with_user_defined_rules(None);
690 assert_eq!(room_ids.len(), 5);
691 let expected_set: HashSet<String> = vec![
692 "!room_a:matrix.org",
693 "!room_b:matrix.org",
694 "!room_c:matrix.org",
695 "!room_d:matrix.org",
696 "!room_e:matrix.org",
697 ]
698 .into_iter()
699 .collect();
700 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
701
702 let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
704 assert_eq!(room_ids.len(), 0);
705
706 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
708 assert_eq!(room_ids.len(), 5);
709
710 let mut ruleset = build_ruleset(vec![
711 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
712 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
713 (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
714 (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
715 ]);
716 ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
717 ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
718 let rules = Rules::new(ruleset);
719 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
721 assert_eq!(room_ids.len(), 2);
722 let expected_set: HashSet<String> =
723 vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
724 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
725 }
726}