matrix_sdk/notification_settings/
rules.rs

1//! Ruleset utility struct
2
3use 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    /// Gets all user defined rules matching a given `room_id`.
30    pub(crate) fn get_custom_rules_for_room(&self, room_id: &RoomId) -> Vec<(RuleKind, String)> {
31        let mut custom_rules = vec![];
32
33        // add any `Override` rules matching this `room_id`
34        for rule in &self.ruleset.override_ {
35            // if the rule_id is the room_id
36            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                // the rule contains a condition matching this `room_id`
41                custom_rules.push((RuleKind::Override, rule.rule_id.clone()));
42            }
43        }
44
45        // add any `Room` rules matching this `room_id`
46        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        // add any `Underride` rules matching this `room_id`
51        for rule in &self.ruleset.underride {
52            // if the rule_id is the room_id
53            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                // the rule contains a condition matching this `room_id`
58                custom_rules.push((RuleKind::Underride, rule.rule_id.clone()));
59            }
60        }
61
62        custom_rules
63    }
64
65    /// Gets the user defined notification mode for a room.
66    pub(crate) fn get_user_defined_room_notification_mode(
67        &self,
68        room_id: &RoomId,
69    ) -> Option<RoomNotificationMode> {
70        // Search for an enabled `Override` rule
71        if self.ruleset.override_.iter().any(|x| {
72            // enabled
73            x.enabled &&
74            // with a condition of type `EventMatch` for this `room_id`
75            // (checking on x.rule_id is not sufficient here as more than one override rule may have a condition matching on `room_id`)
76            x.conditions.iter().any(|x| matches!(
77                x,
78                PushCondition::EventMatch { key, pattern } if key == "room_id" && pattern == room_id
79            )) &&
80            // and without a Notify action
81            !x.actions.iter().any(|x| x.should_notify())
82        }) {
83            return Some(RoomNotificationMode::Mute);
84        }
85
86        // Search for an enabled `Room` rule where `rule_id` is the `room_id`
87        if let Some(rule) = self.ruleset.get(RuleKind::Room, room_id) {
88            // if this rule contains a `Notify` action
89            if rule.triggers_notification() {
90                return Some(RoomNotificationMode::AllMessages);
91            }
92            return Some(RoomNotificationMode::MentionsAndKeywordsOnly);
93        }
94
95        // There is no custom rule matching this `room_id`
96        None
97    }
98
99    /// Gets the default notification mode for a room.
100    ///
101    /// # Arguments
102    ///
103    /// * `is_encrypted` - `Yes` if the room is encrypted
104    /// * `is_one_to_one` - `Yes` if the room is a direct chat involving two
105    ///   people
106    pub(crate) fn get_default_room_notification_mode(
107        &self,
108        is_encrypted: IsEncrypted,
109        is_one_to_one: IsOneToOne,
110    ) -> RoomNotificationMode {
111        // get the correct default rule ID based on `is_encrypted` and `is_one_to_one`
112        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 there is an `Underride` rule that should trigger a notification, the mode
116        // is `AllMessages`
117        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            // Otherwise, the mode is `MentionsAndKeywordsOnly`
125            RoomNotificationMode::MentionsAndKeywordsOnly
126        }
127    }
128
129    /// Get all room IDs for which a user-defined rule exists.
130    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    /// Get whether the `IsUserMention` rule is enabled.
163    fn is_user_mention_enabled(&self) -> bool {
164        // Search for an `Override` rule `IsUserMention` (MSC3952).
165        // This is a new push rule that may not yet be present.
166        if let Some(rule) =
167            self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention)
168        {
169            return rule.enabled();
170        }
171
172        // Fallback to deprecated rules for compatibility.
173        #[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    /// Get whether the `IsRoomMention` rule is enabled.
195    fn is_room_mention_enabled(&self) -> bool {
196        // Search for an `Override` rule `IsRoomMention` (MSC3952).
197        // This is a new push rule that may not yet be present.
198        if let Some(rule) =
199            self.ruleset.get(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention)
200        {
201            return rule.enabled();
202        }
203
204        // Fallback to deprecated rule for compatibility
205        #[allow(deprecated)]
206        self.ruleset
207            .get(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif)
208            .is_some_and(|r| r.enabled() && r.triggers_notification())
209    }
210
211    /// Get whether the given ruleset contains some enabled keywords rules.
212    pub(crate) fn contains_keyword_rules(&self) -> bool {
213        // Search for a user defined `Content` rule.
214        self.ruleset.content.iter().any(|r| !r.default && r.enabled)
215    }
216
217    /// The keywords which have enabled rules.
218    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    /// The rules for a keyword, if any.
228    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    /// Get whether a rule is enabled.
233    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    /// Apply a group of commands to the managed ruleset.
250    ///
251    /// The command may silently fail because the ruleset may have changed
252    /// between the time the command was created and the time it is applied.
253    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
280/// Gets the `PredefinedUnderrideRuleId` for rooms corresponding to the given
281/// criteria.
282///
283/// # Arguments
284///
285/// * `is_encrypted` - `Yes` if the room is encrypted
286/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
287pub(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
299/// Gets the `PredefinedUnderrideRuleId` for poll start events corresponding to
300/// the given criteria.
301///
302/// # Arguments
303///
304/// * `is_one_to_one` - `Yes` if the room is a direct chat involving two people
305pub(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        // Initialize with one rule.
350        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        // Insert a Room rule
355        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        // Insert an Override rule where the rule ID doesn't match the room id,
369        // but with a condition that matches
370        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        // Initialize with an `Override` rule that doesn't notify
388        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        // Initialize with a `Room` rule that doesn't notify
396        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        // Initialize with a `Room` rule that doesn't notify
404        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            // A mute rule for room_id_a
415            (RuleKind::Override, &room_id_a, false),
416            // A notifying rule for room_id_b
417            (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        // The mode should be Mute as there is an Override rule that doesn't notify,
423        // with a condition matching the room_id_a
424        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        // If the corresponding underride rule is disabled
463        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        // Then the mode should be `MentionsAndKeywordsOnly`
470        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        // If the corresponding underride rule is enabled
477        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        // Then the mode should be `AllMessages`
484        assert_eq!(mode, RoomNotificationMode::AllMessages);
485    }
486
487    #[async_test]
488    async fn test_is_user_mention_enabled() {
489        // If `IsUserMention` is enable, then is_user_mention_enabled() should return
490        // `true` even if the deprecated rules are disabled
491        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        // is_enabled() should also return `true` for
507        // PredefinedOverrideRuleId::IsUserMention
508        assert!(rules
509            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
510            .unwrap());
511
512        // If `IsUserMention` is disabled, then is_user_mention_enabled() should return
513        // `false` even if the deprecated rules are enabled
514        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        // is_enabled() should also return `false` for
538        // PredefinedOverrideRuleId::IsUserMention
539        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        // If `IsRoomMention` is present and enabled then is_room_mention_enabled()
547        // should return `true` even if the deprecated rule is disabled
548        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        // is_enabled() should also return `true` for
560        // PredefinedOverrideRuleId::IsRoomMention
561        assert!(rules
562            .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
563            .unwrap());
564
565        // If `IsRoomMention` is present and disabled then is_room_mention_enabled()
566        // should return `false` even if the deprecated rule is enabled
567        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        // is_enabled() should also return `false` for
577        // PredefinedOverrideRuleId::IsRoomMention
578        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        // Initialize with a custom rule
597        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
598        let mut rules = Rules::new(ruleset);
599
600        // Build a `RuleCommands` deleting this rule
601        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        // The rule must have been removed from the updated rules
607        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        // Build a `RuleCommands` inserting a rule
616        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        // The rule must have been removed from the updated rules
622        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        // Build a `RuleCommands` disabling the rule
635        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        // The rule must have been disabled in the updated rules
647        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        // Without user-defined rules
655        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        // With one rule.
660        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        // With duplicates
668        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        // With multiple rules
680        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        // Only disabled rules
703        let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
704        assert_eq!(room_ids.len(), 0);
705
706        // Only enabled rules
707        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        // Only room_a and room_d rules are enabled
720        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}