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 }
273 }
274 }
275}
276
277pub(crate) fn get_predefined_underride_room_rule_id(
285 is_encrypted: IsEncrypted,
286 is_one_to_one: IsOneToOne,
287) -> PredefinedUnderrideRuleId {
288 match (is_encrypted, is_one_to_one) {
289 (IsEncrypted::Yes, IsOneToOne::Yes) => PredefinedUnderrideRuleId::EncryptedRoomOneToOne,
290 (IsEncrypted::No, IsOneToOne::Yes) => PredefinedUnderrideRuleId::RoomOneToOne,
291 (IsEncrypted::Yes, IsOneToOne::No) => PredefinedUnderrideRuleId::Encrypted,
292 (IsEncrypted::No, IsOneToOne::No) => PredefinedUnderrideRuleId::Message,
293 }
294}
295
296pub(crate) fn get_predefined_underride_poll_start_rule_id(
303 is_one_to_one: IsOneToOne,
304) -> PredefinedUnderrideRuleId {
305 match is_one_to_one {
306 IsOneToOne::Yes => PredefinedUnderrideRuleId::PollStartOneToOne,
307 IsOneToOne::No => PredefinedUnderrideRuleId::PollStart,
308 }
309}
310
311#[cfg(test)]
312pub(crate) mod tests {
313 use imbl::HashSet;
314 use matrix_sdk_test::{
315 async_test,
316 notification_settings::{build_ruleset, get_server_default_ruleset},
317 };
318 use ruma::{
319 push::{
320 Action, NewConditionalPushRule, NewPushRule, PredefinedContentRuleId,
321 PredefinedOverrideRuleId, PredefinedUnderrideRuleId, PushCondition, RuleKind,
322 },
323 OwnedRoomId, RoomId,
324 };
325
326 use super::RuleCommands;
327 use crate::{
328 error::NotificationSettingsError,
329 notification_settings::{
330 rules::{self, Rules},
331 IsEncrypted, IsOneToOne, RoomNotificationMode,
332 },
333 };
334
335 fn get_test_room_id() -> OwnedRoomId {
336 RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
337 }
338
339 #[async_test]
340 async fn test_get_custom_rules_for_room() {
341 let room_id = get_test_room_id();
342
343 let rules = Rules::new(get_server_default_ruleset());
344 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
345
346 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
348 let rules = Rules::new(ruleset);
349 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
350
351 let ruleset = build_ruleset(vec![
353 (RuleKind::Override, &room_id, false),
354 (RuleKind::Room, &room_id, false),
355 ]);
356 let rules = Rules::new(ruleset);
357 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
358 }
359
360 #[async_test]
361 async fn test_get_custom_rules_for_room_special_override_rule() {
362 let room_id = get_test_room_id();
363 let mut ruleset = get_server_default_ruleset();
364
365 let new_rule = NewConditionalPushRule::new(
368 "custom_rule_id".to_owned(),
369 vec![PushCondition::EventMatch { key: "room_id".into(), pattern: room_id.to_string() }],
370 vec![Action::Notify],
371 );
372 ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
373
374 let rules = Rules::new(ruleset);
375 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
376 }
377
378 #[async_test]
379 async fn test_get_user_defined_room_notification_mode() {
380 let room_id = get_test_room_id();
381 let rules = Rules::new(get_server_default_ruleset());
382 assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
383
384 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
386 let rules = Rules::new(ruleset);
387 assert_eq!(
388 rules.get_user_defined_room_notification_mode(&room_id),
389 Some(RoomNotificationMode::Mute)
390 );
391
392 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
394 let rules = Rules::new(ruleset);
395 assert_eq!(
396 rules.get_user_defined_room_notification_mode(&room_id),
397 Some(RoomNotificationMode::MentionsAndKeywordsOnly)
398 );
399
400 let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
402 let rules = Rules::new(ruleset);
403 assert_eq!(
404 rules.get_user_defined_room_notification_mode(&room_id),
405 Some(RoomNotificationMode::AllMessages)
406 );
407
408 let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
409 let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
410 let ruleset = build_ruleset(vec![
411 (RuleKind::Override, &room_id_a, false),
413 (RuleKind::Override, &room_id_b, true),
415 ]);
416 let rules = Rules::new(ruleset);
417 let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
418
419 assert_eq!(mode, Some(RoomNotificationMode::Mute));
422 }
423
424 #[async_test]
425 async fn test_get_predefined_underride_room_rule_id() {
426 assert_eq!(
427 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
428 PredefinedUnderrideRuleId::Message
429 );
430 assert_eq!(
431 rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
432 PredefinedUnderrideRuleId::RoomOneToOne
433 );
434 assert_eq!(
435 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
436 PredefinedUnderrideRuleId::Encrypted
437 );
438 assert_eq!(
439 rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
440 PredefinedUnderrideRuleId::EncryptedRoomOneToOne
441 );
442 }
443
444 #[async_test]
445 async fn test_get_predefined_underride_poll_start_rule_id() {
446 assert_eq!(
447 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
448 PredefinedUnderrideRuleId::PollStart
449 );
450 assert_eq!(
451 rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
452 PredefinedUnderrideRuleId::PollStartOneToOne
453 );
454 }
455
456 #[async_test]
457 async fn test_get_default_room_notification_mode_mentions_and_keywords() {
458 let mut ruleset = get_server_default_ruleset();
459 ruleset
461 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
462 .unwrap();
463
464 let rules = Rules::new(ruleset);
465 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
466 assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
468 }
469
470 #[async_test]
471 async fn test_get_default_room_notification_mode_all_messages() {
472 let mut ruleset = get_server_default_ruleset();
473 ruleset
475 .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
476 .unwrap();
477
478 let rules = Rules::new(ruleset);
479 let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
480 assert_eq!(mode, RoomNotificationMode::AllMessages);
482 }
483
484 #[async_test]
485 async fn test_is_user_mention_enabled() {
486 let mut ruleset = get_server_default_ruleset();
489 ruleset
490 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
491 .unwrap();
492 #[allow(deprecated)]
493 ruleset
494 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
495 .unwrap();
496 #[allow(deprecated)]
497 ruleset
498 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
499 .unwrap();
500
501 let rules = Rules::new(ruleset);
502 assert!(rules.is_user_mention_enabled());
503 assert!(rules
506 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
507 .unwrap());
508
509 let mut ruleset = get_server_default_ruleset();
512 ruleset
513 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
514 .unwrap();
515 #[allow(deprecated)]
516 ruleset
517 .set_actions(
518 RuleKind::Override,
519 PredefinedOverrideRuleId::ContainsDisplayName,
520 vec![Action::Notify],
521 )
522 .unwrap();
523 #[allow(deprecated)]
524 ruleset
525 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
526 .unwrap();
527 #[allow(deprecated)]
528 ruleset
529 .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
530 .unwrap();
531
532 let rules = Rules::new(ruleset);
533 assert!(!rules.is_user_mention_enabled());
534 assert!(!rules
537 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
538 .unwrap());
539 }
540
541 #[async_test]
542 async fn test_is_room_mention_enabled() {
543 let mut ruleset = get_server_default_ruleset();
546 ruleset
547 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
548 .unwrap();
549 #[allow(deprecated)]
550 ruleset
551 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
552 .unwrap();
553
554 let rules = Rules::new(ruleset);
555 assert!(rules.is_room_mention_enabled());
556 assert!(rules
559 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
560 .unwrap());
561
562 let mut ruleset = get_server_default_ruleset();
565 ruleset
566 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
567 .unwrap();
568 #[allow(deprecated)]
569 ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
570
571 let rules = Rules::new(ruleset);
572 assert!(!rules.is_room_mention_enabled());
573 assert!(!rules
576 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
577 .unwrap());
578 }
579
580 #[async_test]
581 async fn test_is_enabled_rule_not_found() {
582 let rules = Rules::new(get_server_default_ruleset());
583
584 assert_eq!(
585 rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
586 Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
587 );
588 }
589
590 #[async_test]
591 async fn test_apply_delete_command() {
592 let room_id = get_test_room_id();
593 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
595 let mut rules = Rules::new(ruleset);
596
597 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
599 rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
600
601 rules.apply(rules_commands);
602
603 assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
605 }
606
607 #[async_test]
608 async fn test_apply_set_command() {
609 let room_id = get_test_room_id();
610 let mut rules = Rules::new(get_server_default_ruleset());
611
612 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
614 rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
615
616 rules.apply(rules_commands);
617
618 assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
620 }
621
622 #[async_test]
623 async fn test_apply_set_enabled_command() {
624 let mut rules = Rules::new(get_server_default_ruleset());
625
626 rules
627 .ruleset
628 .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
629 .unwrap();
630
631 let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
633 rules_commands
634 .set_rule_enabled(
635 RuleKind::Override,
636 PredefinedOverrideRuleId::Reaction.as_str(),
637 false,
638 )
639 .unwrap();
640
641 rules.apply(rules_commands);
642
643 assert!(!rules
645 .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
646 .unwrap());
647 }
648
649 #[async_test]
650 async fn test_get_rooms_with_user_defined_rules() {
651 let rules = Rules::new(get_server_default_ruleset());
653 let room_ids = rules.get_rooms_with_user_defined_rules(None);
654 assert!(room_ids.is_empty());
655
656 let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
658 let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
659 let rules = Rules::new(ruleset);
660
661 let room_ids = rules.get_rooms_with_user_defined_rules(None);
662 assert_eq!(room_ids.len(), 1);
663
664 let ruleset = build_ruleset(vec![
666 (RuleKind::Override, &room_id, false),
667 (RuleKind::Underride, &room_id, false),
668 (RuleKind::Room, &room_id, false),
669 ]);
670 let rules = Rules::new(ruleset);
671
672 let room_ids = rules.get_rooms_with_user_defined_rules(None);
673 assert_eq!(room_ids.len(), 1);
674 assert_eq!(room_ids[0], room_id.to_string());
675
676 let ruleset = build_ruleset(vec![
678 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
679 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
680 (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
681 (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
682 (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
683 ]);
684 let rules = Rules::new(ruleset);
685
686 let room_ids = rules.get_rooms_with_user_defined_rules(None);
687 assert_eq!(room_ids.len(), 5);
688 let expected_set: HashSet<String> = vec![
689 "!room_a:matrix.org",
690 "!room_b:matrix.org",
691 "!room_c:matrix.org",
692 "!room_d:matrix.org",
693 "!room_e:matrix.org",
694 ]
695 .into_iter()
696 .collect();
697 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
698
699 let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
701 assert_eq!(room_ids.len(), 0);
702
703 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
705 assert_eq!(room_ids.len(), 5);
706
707 let mut ruleset = build_ruleset(vec![
708 (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
709 (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
710 (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
711 (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
712 ]);
713 ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
714 ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
715 let rules = Rules::new(ruleset);
716 let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
718 assert_eq!(room_ids.len(), 2);
719 let expected_set: HashSet<String> =
720 vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
721 assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
722 }
723}