Skip to main content

matrix_sdk/notification_settings/
rules.rs

1//! Ruleset utility struct
2
3use imbl::HashSet;
4use indexmap::IndexSet;
5use ruma::{
6    RoomId,
7    push::{
8        AnyPushRuleRef, PatternedPushRule, PredefinedContentRuleId, PredefinedOverrideRuleId,
9        PredefinedUnderrideRuleId, PushCondition, RuleKind, Ruleset,
10    },
11};
12
13use super::{RoomNotificationMode, command::Command, rule_commands::RuleCommands};
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(data) if data.key == "room_id" && data.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(data) if data.key == "room_id" && data.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(data) if data.key == "room_id" && data.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(data) = condition
146                            && data.key == "room_id"
147                        {
148                            room_ids.insert(data.pattern.clone());
149                            break;
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            && rule.enabled()
177            && rule.triggers_notification()
178        {
179            return true;
180        }
181
182        #[allow(deprecated)]
183        if let Some(rule) =
184            self.ruleset.get(RuleKind::Content, PredefinedContentRuleId::ContainsUserName)
185            && rule.enabled()
186            && rule.triggers_notification()
187        {
188            return true;
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::{
320            build_ruleset, get_server_default_ruleset, server_default_ruleset_with_legacy_mentions,
321        },
322    };
323    use ruma::{
324        OwnedRoomId, RoomId,
325        push::{
326            Action, EventMatchConditionData, NewConditionalPushRule, NewPushRule,
327            PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedUnderrideRuleId,
328            PushCondition, RuleKind,
329        },
330    };
331
332    use super::RuleCommands;
333    use crate::{
334        error::NotificationSettingsError,
335        notification_settings::{
336            IsEncrypted, IsOneToOne, RoomNotificationMode,
337            rules::{self, Rules},
338        },
339    };
340
341    fn get_test_room_id() -> OwnedRoomId {
342        RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap()
343    }
344
345    #[async_test]
346    async fn test_get_custom_rules_for_room() {
347        let room_id = get_test_room_id();
348
349        let rules = Rules::new(get_server_default_ruleset());
350        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 0);
351
352        // Initialize with one rule.
353        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
354        let rules = Rules::new(ruleset);
355        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
356
357        // Insert a Room rule
358        let ruleset = build_ruleset(vec![
359            (RuleKind::Override, &room_id, false),
360            (RuleKind::Room, &room_id, false),
361        ]);
362        let rules = Rules::new(ruleset);
363        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 2);
364    }
365
366    #[async_test]
367    async fn test_get_custom_rules_for_room_special_override_rule() {
368        let room_id = get_test_room_id();
369        let mut ruleset = get_server_default_ruleset();
370
371        // Insert an Override rule where the rule ID doesn't match the room id,
372        // but with a condition that matches
373        let new_rule = NewConditionalPushRule::new(
374            "custom_rule_id".to_owned(),
375            vec![PushCondition::EventMatch(EventMatchConditionData::new(
376                "room_id".into(),
377                room_id.to_string(),
378            ))],
379            vec![Action::Notify],
380        );
381        ruleset.insert(NewPushRule::Override(new_rule), None, None).unwrap();
382
383        let rules = Rules::new(ruleset);
384        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
385    }
386
387    #[async_test]
388    async fn test_get_user_defined_room_notification_mode() {
389        let room_id = get_test_room_id();
390        let rules = Rules::new(get_server_default_ruleset());
391        assert_eq!(rules.get_user_defined_room_notification_mode(&room_id), None);
392
393        // Initialize with an `Override` rule that doesn't notify
394        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
395        let rules = Rules::new(ruleset);
396        assert_eq!(
397            rules.get_user_defined_room_notification_mode(&room_id),
398            Some(RoomNotificationMode::Mute)
399        );
400
401        // Initialize with a `Room` rule that doesn't notify
402        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, false)]);
403        let rules = Rules::new(ruleset);
404        assert_eq!(
405            rules.get_user_defined_room_notification_mode(&room_id),
406            Some(RoomNotificationMode::MentionsAndKeywordsOnly)
407        );
408
409        // Initialize with a `Room` rule that doesn't notify
410        let ruleset = build_ruleset(vec![(RuleKind::Room, &room_id, true)]);
411        let rules = Rules::new(ruleset);
412        assert_eq!(
413            rules.get_user_defined_room_notification_mode(&room_id),
414            Some(RoomNotificationMode::AllMessages)
415        );
416
417        let room_id_a = RoomId::parse("!AAAaAAAAAaaAAaaaaa:matrix.org").unwrap();
418        let room_id_b = RoomId::parse("!BBBbBBBBBbbBBbbbbb:matrix.org").unwrap();
419        let ruleset = build_ruleset(vec![
420            // A mute rule for room_id_a
421            (RuleKind::Override, &room_id_a, false),
422            // A notifying rule for room_id_b
423            (RuleKind::Override, &room_id_b, true),
424        ]);
425        let rules = Rules::new(ruleset);
426        let mode = rules.get_user_defined_room_notification_mode(&room_id_a);
427
428        // The mode should be Mute as there is an Override rule that doesn't notify,
429        // with a condition matching the room_id_a
430        assert_eq!(mode, Some(RoomNotificationMode::Mute));
431    }
432
433    #[async_test]
434    async fn test_get_predefined_underride_room_rule_id() {
435        assert_eq!(
436            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::No),
437            PredefinedUnderrideRuleId::Message
438        );
439        assert_eq!(
440            rules::get_predefined_underride_room_rule_id(IsEncrypted::No, IsOneToOne::Yes),
441            PredefinedUnderrideRuleId::RoomOneToOne
442        );
443        assert_eq!(
444            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::No),
445            PredefinedUnderrideRuleId::Encrypted
446        );
447        assert_eq!(
448            rules::get_predefined_underride_room_rule_id(IsEncrypted::Yes, IsOneToOne::Yes),
449            PredefinedUnderrideRuleId::EncryptedRoomOneToOne
450        );
451    }
452
453    #[async_test]
454    async fn test_get_predefined_underride_poll_start_rule_id() {
455        assert_eq!(
456            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::No),
457            PredefinedUnderrideRuleId::PollStart
458        );
459        assert_eq!(
460            rules::get_predefined_underride_poll_start_rule_id(IsOneToOne::Yes),
461            PredefinedUnderrideRuleId::PollStartOneToOne
462        );
463    }
464
465    #[async_test]
466    async fn test_get_default_room_notification_mode_mentions_and_keywords() {
467        let mut ruleset = get_server_default_ruleset();
468        // If the corresponding underride rule is disabled
469        ruleset
470            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, false)
471            .unwrap();
472
473        let rules = Rules::new(ruleset);
474        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
475        // Then the mode should be `MentionsAndKeywordsOnly`
476        assert_eq!(mode, RoomNotificationMode::MentionsAndKeywordsOnly);
477    }
478
479    #[async_test]
480    async fn test_get_default_room_notification_mode_all_messages() {
481        let mut ruleset = get_server_default_ruleset();
482        // If the corresponding underride rule is enabled
483        ruleset
484            .set_enabled(RuleKind::Underride, PredefinedUnderrideRuleId::RoomOneToOne, true)
485            .unwrap();
486
487        let rules = Rules::new(ruleset);
488        let mode = rules.get_default_room_notification_mode(IsEncrypted::No, IsOneToOne::Yes);
489        // Then the mode should be `AllMessages`
490        assert_eq!(mode, RoomNotificationMode::AllMessages);
491    }
492
493    #[async_test]
494    async fn test_is_user_mention_enabled() {
495        // If `IsUserMention` is enable, then is_user_mention_enabled() should return
496        // `true` even if the deprecated rules are disabled
497        let mut ruleset = server_default_ruleset_with_legacy_mentions();
498        ruleset
499            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, true)
500            .unwrap();
501        #[allow(deprecated)]
502        ruleset
503            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, false)
504            .unwrap();
505        #[allow(deprecated)]
506        ruleset
507            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, false)
508            .unwrap();
509
510        let rules = Rules::new(ruleset);
511        assert!(rules.is_user_mention_enabled());
512        // is_enabled() should also return `true` for
513        // PredefinedOverrideRuleId::IsUserMention
514        assert!(
515            rules
516                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
517                .unwrap()
518        );
519
520        // If `IsUserMention` is disabled, then is_user_mention_enabled() should return
521        // `false` even if the deprecated rules are enabled
522        let mut ruleset = server_default_ruleset_with_legacy_mentions();
523        ruleset
524            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention, false)
525            .unwrap();
526        #[allow(deprecated)]
527        ruleset
528            .set_actions(
529                RuleKind::Override,
530                PredefinedOverrideRuleId::ContainsDisplayName,
531                vec![Action::Notify],
532            )
533            .unwrap();
534        #[allow(deprecated)]
535        ruleset
536            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::ContainsDisplayName, true)
537            .unwrap();
538        #[allow(deprecated)]
539        ruleset
540            .set_enabled(RuleKind::Content, PredefinedContentRuleId::ContainsUserName, true)
541            .unwrap();
542
543        let rules = Rules::new(ruleset);
544        assert!(!rules.is_user_mention_enabled());
545        // is_enabled() should also return `false` for
546        // PredefinedOverrideRuleId::IsUserMention
547        assert!(
548            !rules
549                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsUserMention.as_str())
550                .unwrap()
551        );
552    }
553
554    #[async_test]
555    async fn test_is_room_mention_enabled() {
556        // If `IsRoomMention` is present and enabled then is_room_mention_enabled()
557        // should return `true` even if the deprecated rule is disabled
558        let mut ruleset = server_default_ruleset_with_legacy_mentions();
559        ruleset
560            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, true)
561            .unwrap();
562        #[allow(deprecated)]
563        ruleset
564            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, false)
565            .unwrap();
566
567        let rules = Rules::new(ruleset);
568        assert!(rules.is_room_mention_enabled());
569        // is_enabled() should also return `true` for
570        // PredefinedOverrideRuleId::IsRoomMention
571        assert!(
572            rules
573                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
574                .unwrap()
575        );
576
577        // If `IsRoomMention` is present and disabled then is_room_mention_enabled()
578        // should return `false` even if the deprecated rule is enabled
579        let mut ruleset = server_default_ruleset_with_legacy_mentions();
580        ruleset
581            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention, false)
582            .unwrap();
583        #[allow(deprecated)]
584        ruleset.set_enabled(RuleKind::Override, PredefinedOverrideRuleId::RoomNotif, true).unwrap();
585
586        let rules = Rules::new(ruleset);
587        assert!(!rules.is_room_mention_enabled());
588        // is_enabled() should also return `false` for
589        // PredefinedOverrideRuleId::IsRoomMention
590        assert!(
591            !rules
592                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::IsRoomMention.as_str())
593                .unwrap()
594        );
595    }
596
597    #[async_test]
598    async fn test_is_enabled_rule_not_found() {
599        let rules = Rules::new(get_server_default_ruleset());
600
601        assert_eq!(
602            rules.is_enabled(RuleKind::Override, "unknown_rule_id"),
603            Err(NotificationSettingsError::RuleNotFound("unknown_rule_id".to_owned()))
604        );
605    }
606
607    #[async_test]
608    async fn test_apply_delete_command() {
609        let room_id = get_test_room_id();
610        // Initialize with a custom rule
611        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
612        let mut rules = Rules::new(ruleset);
613
614        // Build a `RuleCommands` deleting this rule
615        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
616        rules_commands.delete_rule(RuleKind::Override, room_id.to_string()).unwrap();
617
618        rules.apply(rules_commands);
619
620        // The rule must have been removed from the updated rules
621        assert!(rules.get_custom_rules_for_room(&room_id).is_empty());
622    }
623
624    #[async_test]
625    async fn test_apply_set_command() {
626        let room_id = get_test_room_id();
627        let mut rules = Rules::new(get_server_default_ruleset());
628
629        // Build a `RuleCommands` inserting a rule
630        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
631        rules_commands.insert_rule(RuleKind::Override, &room_id, false).unwrap();
632
633        rules.apply(rules_commands);
634
635        // The rule must have been removed from the updated rules
636        assert_eq!(rules.get_custom_rules_for_room(&room_id).len(), 1);
637    }
638
639    #[async_test]
640    async fn test_apply_set_enabled_command() {
641        let mut rules = Rules::new(get_server_default_ruleset());
642
643        rules
644            .ruleset
645            .set_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction, true)
646            .unwrap();
647
648        // Build a `RuleCommands` disabling the rule
649        let mut rules_commands = RuleCommands::new(rules.ruleset.clone());
650        rules_commands
651            .set_rule_enabled(
652                RuleKind::Override,
653                PredefinedOverrideRuleId::Reaction.as_str(),
654                false,
655            )
656            .unwrap();
657
658        rules.apply(rules_commands);
659
660        // The rule must have been disabled in the updated rules
661        assert!(
662            !rules
663                .is_enabled(RuleKind::Override, PredefinedOverrideRuleId::Reaction.as_str())
664                .unwrap()
665        );
666    }
667
668    #[async_test]
669    async fn test_get_rooms_with_user_defined_rules() {
670        // Without user-defined rules
671        let rules = Rules::new(get_server_default_ruleset());
672        let room_ids = rules.get_rooms_with_user_defined_rules(None);
673        assert!(room_ids.is_empty());
674
675        // With one rule.
676        let room_id = RoomId::parse("!room_a:matrix.org").unwrap();
677        let ruleset = build_ruleset(vec![(RuleKind::Override, &room_id, false)]);
678        let rules = Rules::new(ruleset);
679
680        let room_ids = rules.get_rooms_with_user_defined_rules(None);
681        assert_eq!(room_ids.len(), 1);
682
683        // With duplicates
684        let ruleset = build_ruleset(vec![
685            (RuleKind::Override, &room_id, false),
686            (RuleKind::Underride, &room_id, false),
687            (RuleKind::Room, &room_id, false),
688        ]);
689        let rules = Rules::new(ruleset);
690
691        let room_ids = rules.get_rooms_with_user_defined_rules(None);
692        assert_eq!(room_ids.len(), 1);
693        assert_eq!(room_ids[0], room_id.to_string());
694
695        // With multiple rules
696        let ruleset = build_ruleset(vec![
697            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
698            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
699            (RuleKind::Room, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
700            (RuleKind::Override, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
701            (RuleKind::Underride, &RoomId::parse("!room_e:matrix.org").unwrap(), false),
702        ]);
703        let rules = Rules::new(ruleset);
704
705        let room_ids = rules.get_rooms_with_user_defined_rules(None);
706        assert_eq!(room_ids.len(), 5);
707        let expected_set: HashSet<String> = vec![
708            "!room_a:matrix.org",
709            "!room_b:matrix.org",
710            "!room_c:matrix.org",
711            "!room_d:matrix.org",
712            "!room_e:matrix.org",
713        ]
714        .into_iter()
715        .collect();
716        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
717
718        // Only disabled rules
719        let room_ids = rules.get_rooms_with_user_defined_rules(Some(false));
720        assert_eq!(room_ids.len(), 0);
721
722        // Only enabled rules
723        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
724        assert_eq!(room_ids.len(), 5);
725
726        let mut ruleset = build_ruleset(vec![
727            (RuleKind::Room, &RoomId::parse("!room_a:matrix.org").unwrap(), false),
728            (RuleKind::Room, &RoomId::parse("!room_b:matrix.org").unwrap(), false),
729            (RuleKind::Override, &RoomId::parse("!room_c:matrix.org").unwrap(), false),
730            (RuleKind::Underride, &RoomId::parse("!room_d:matrix.org").unwrap(), false),
731        ]);
732        ruleset.set_enabled(RuleKind::Room, "!room_b:matrix.org", false).unwrap();
733        ruleset.set_enabled(RuleKind::Override, "!room_c:matrix.org", false).unwrap();
734        let rules = Rules::new(ruleset);
735        // Only room_a and room_d rules are enabled
736        let room_ids = rules.get_rooms_with_user_defined_rules(Some(true));
737        assert_eq!(room_ids.len(), 2);
738        let expected_set: HashSet<String> =
739            vec!["!room_a:matrix.org", "!room_d:matrix.org"].into_iter().collect();
740        assert!(expected_set.symmetric_difference(HashSet::from(room_ids)).is_empty());
741    }
742}