Skip to main content

ruma_common/
push.rs

1//! Common types for the [push notifications module][push].
2//!
3//! [push]: https://spec.matrix.org/v1.18/client-server-api/#push-notifications
4//!
5//! ## Understanding the types of this module
6//!
7//! Push rules are grouped in `RuleSet`s, and are grouped in five kinds (for
8//! more details about the different kind of rules, see the `Ruleset` documentation,
9//! or the specification). These five kinds are, by order of priority:
10//!
11//! - override rules
12//! - content rules
13//! - room rules
14//! - sender rules
15//! - underride rules
16
17use std::hash::{Hash, Hasher};
18
19use indexmap::{Equivalent, IndexSet};
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22use tracing::instrument;
23
24use crate::{
25    OwnedRoomId, OwnedUserId, PrivOwnedStr,
26    serde::{JsonObject, Raw, StringEnum},
27};
28
29mod action;
30mod condition;
31mod iter;
32mod predefined;
33
34#[cfg(feature = "unstable-msc4306")]
35pub use self::condition::ThreadSubscriptionConditionData;
36#[cfg(feature = "unstable-msc3932")]
37pub use self::condition::{RoomVersionFeature, RoomVersionSupportsConditionData};
38pub use self::{
39    action::{Action, HighlightTweakValue, SoundTweakValue, Tweak},
40    condition::{
41        _CustomPushCondition, ComparisonOperator, EventMatchConditionData,
42        EventPropertyContainsConditionData, EventPropertyIsConditionData, FlattenedJson,
43        FlattenedJsonValue, PushCondition, PushConditionPowerLevelsCtx, PushConditionRoomCtx,
44        RoomMemberCountConditionData, RoomMemberCountIs, ScalarJsonValue,
45        SenderNotificationPermissionConditionData,
46    },
47    iter::{AnyPushRule, AnyPushRuleRef, RulesetIntoIter, RulesetIter},
48    predefined::{
49        PredefinedContentRuleId, PredefinedOverrideRuleId, PredefinedRuleId,
50        PredefinedUnderrideRuleId,
51    },
52};
53
54/// A push ruleset scopes a set of rules according to some criteria.
55///
56/// For example, some rules may only be applied for messages from a particular sender, a particular
57/// room, or by default. The push ruleset contains the entire set of scopes and rules.
58#[derive(Clone, Debug, Default, Deserialize, Serialize)]
59#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
60pub struct Ruleset {
61    /// These rules configure behavior for (unencrypted) messages that match certain patterns.
62    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
63    pub content: IndexSet<PatternedPushRule>,
64
65    /// These rules are identical to override rules, but have a lower priority than `room` and
66    /// `sender` rules.
67    #[cfg(feature = "unstable-msc4306")]
68    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
69    pub postcontent: IndexSet<ConditionalPushRule>,
70
71    /// These user-configured rules are given the highest priority.
72    ///
73    /// This field is named `override_` instead of `override` because the latter is a reserved
74    /// keyword in Rust.
75    #[serde(rename = "override", default, skip_serializing_if = "IndexSet::is_empty")]
76    pub override_: IndexSet<ConditionalPushRule>,
77
78    /// These rules change the behavior of all messages for a given room.
79    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
80    pub room: IndexSet<SimplePushRule<OwnedRoomId>>,
81
82    /// These rules configure notification behavior for messages from a specific Matrix user ID.
83    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
84    pub sender: IndexSet<SimplePushRule<OwnedUserId>>,
85
86    /// These rules are identical to override rules, but have a lower priority than `content`,
87    /// `room` and `sender` rules.
88    #[serde(default, skip_serializing_if = "IndexSet::is_empty")]
89    pub underride: IndexSet<ConditionalPushRule>,
90}
91
92impl Ruleset {
93    /// Creates an empty `Ruleset`.
94    pub fn new() -> Self {
95        Default::default()
96    }
97
98    /// Creates a borrowing iterator over all push rules in this `Ruleset`.
99    ///
100    /// For an owning iterator, use `.into_iter()`.
101    pub fn iter(&self) -> RulesetIter<'_> {
102        self.into_iter()
103    }
104
105    /// Inserts a user-defined rule in the rule set.
106    ///
107    /// If a rule with the same kind and `rule_id` exists, it will be replaced.
108    ///
109    /// If `after` or `before` is set, the rule will be moved relative to the rule with the given
110    /// ID. If both are set, the rule will become the next-most important rule with respect to
111    /// `before`. If neither are set, and the rule is newly inserted, it will become the rule with
112    /// the highest priority of its kind.
113    ///
114    /// Returns an error if the parameters are invalid.
115    pub fn insert(
116        &mut self,
117        rule: NewPushRule,
118        after: Option<&str>,
119        before: Option<&str>,
120    ) -> Result<(), InsertPushRuleError> {
121        let rule_id = rule.rule_id();
122        if rule_id.starts_with('.') {
123            return Err(InsertPushRuleError::ServerDefaultRuleId);
124        }
125        if rule_id.contains('/') {
126            return Err(InsertPushRuleError::InvalidRuleId);
127        }
128        if rule_id.contains('\\') {
129            return Err(InsertPushRuleError::InvalidRuleId);
130        }
131        if after.is_some_and(|s| s.starts_with('.')) {
132            return Err(InsertPushRuleError::RelativeToServerDefaultRule);
133        }
134        if before.is_some_and(|s| s.starts_with('.')) {
135            return Err(InsertPushRuleError::RelativeToServerDefaultRule);
136        }
137
138        match rule {
139            NewPushRule::Override(r) => {
140                let mut rule = ConditionalPushRule::from(r);
141
142                if let Some(prev_rule) = self.override_.get(rule.rule_id.as_str()) {
143                    rule.enabled = prev_rule.enabled;
144                }
145
146                // `m.rule.master` should always be the rule with the highest priority, so we insert
147                // this one at most at the second place.
148                let default_position = 1;
149
150                insert_and_move_rule(&mut self.override_, rule, default_position, after, before)
151            }
152            #[cfg(feature = "unstable-msc4306")]
153            NewPushRule::PostContent(r) => {
154                let mut rule = ConditionalPushRule::from(r);
155
156                if let Some(prev_rule) = self.postcontent.get(rule.rule_id.as_str()) {
157                    rule.enabled = prev_rule.enabled;
158                }
159
160                insert_and_move_rule(&mut self.postcontent, rule, 0, after, before)
161            }
162            NewPushRule::Underride(r) => {
163                let mut rule = ConditionalPushRule::from(r);
164
165                if let Some(prev_rule) = self.underride.get(rule.rule_id.as_str()) {
166                    rule.enabled = prev_rule.enabled;
167                }
168
169                insert_and_move_rule(&mut self.underride, rule, 0, after, before)
170            }
171            NewPushRule::Content(r) => {
172                let mut rule = PatternedPushRule::from(r);
173
174                if let Some(prev_rule) = self.content.get(rule.rule_id.as_str()) {
175                    rule.enabled = prev_rule.enabled;
176                }
177
178                insert_and_move_rule(&mut self.content, rule, 0, after, before)
179            }
180            NewPushRule::Room(r) => {
181                let mut rule = SimplePushRule::from(r);
182
183                if let Some(prev_rule) = self.room.get(rule.rule_id.as_str()) {
184                    rule.enabled = prev_rule.enabled;
185                }
186
187                insert_and_move_rule(&mut self.room, rule, 0, after, before)
188            }
189            NewPushRule::Sender(r) => {
190                let mut rule = SimplePushRule::from(r);
191
192                if let Some(prev_rule) = self.sender.get(rule.rule_id.as_str()) {
193                    rule.enabled = prev_rule.enabled;
194                }
195
196                insert_and_move_rule(&mut self.sender, rule, 0, after, before)
197            }
198        }
199    }
200
201    /// Get the rule from the given kind and with the given `rule_id` in the rule set.
202    pub fn get(&self, kind: RuleKind, rule_id: impl AsRef<str>) -> Option<AnyPushRuleRef<'_>> {
203        let rule_id = rule_id.as_ref();
204
205        match kind {
206            RuleKind::Override => self.override_.get(rule_id).map(AnyPushRuleRef::Override),
207            RuleKind::Underride => self.underride.get(rule_id).map(AnyPushRuleRef::Underride),
208            RuleKind::Sender => self.sender.get(rule_id).map(AnyPushRuleRef::Sender),
209            RuleKind::Room => self.room.get(rule_id).map(AnyPushRuleRef::Room),
210            RuleKind::Content => self.content.get(rule_id).map(AnyPushRuleRef::Content),
211            #[cfg(feature = "unstable-msc4306")]
212            RuleKind::PostContent => self.postcontent.get(rule_id).map(AnyPushRuleRef::PostContent),
213            RuleKind::_Custom(_) => None,
214        }
215    }
216
217    /// Set whether the rule from the given kind and with the given `rule_id` in the rule set is
218    /// enabled.
219    ///
220    /// Returns an error if the rule can't be found.
221    pub fn set_enabled(
222        &mut self,
223        kind: RuleKind,
224        rule_id: impl AsRef<str>,
225        enabled: bool,
226    ) -> Result<(), RuleNotFoundError> {
227        let rule_id = rule_id.as_ref();
228
229        match kind {
230            RuleKind::Override => {
231                let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
232                rule.enabled = enabled;
233                self.override_.replace(rule);
234            }
235            RuleKind::Underride => {
236                let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
237                rule.enabled = enabled;
238                self.underride.replace(rule);
239            }
240            RuleKind::Sender => {
241                let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
242                rule.enabled = enabled;
243                self.sender.replace(rule);
244            }
245            RuleKind::Room => {
246                let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
247                rule.enabled = enabled;
248                self.room.replace(rule);
249            }
250            RuleKind::Content => {
251                let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
252                rule.enabled = enabled;
253                self.content.replace(rule);
254            }
255            #[cfg(feature = "unstable-msc4306")]
256            RuleKind::PostContent => {
257                let mut rule = self.postcontent.get(rule_id).ok_or(RuleNotFoundError)?.clone();
258                rule.enabled = enabled;
259                self.postcontent.replace(rule);
260            }
261            RuleKind::_Custom(_) => return Err(RuleNotFoundError),
262        }
263
264        Ok(())
265    }
266
267    /// Set the actions of the rule from the given kind and with the given `rule_id` in the rule
268    /// set.
269    ///
270    /// Returns an error if the rule can't be found.
271    pub fn set_actions(
272        &mut self,
273        kind: RuleKind,
274        rule_id: impl AsRef<str>,
275        actions: Vec<Action>,
276    ) -> Result<(), RuleNotFoundError> {
277        let rule_id = rule_id.as_ref();
278
279        match kind {
280            RuleKind::Override => {
281                let mut rule = self.override_.get(rule_id).ok_or(RuleNotFoundError)?.clone();
282                rule.actions = actions;
283                self.override_.replace(rule);
284            }
285            RuleKind::Underride => {
286                let mut rule = self.underride.get(rule_id).ok_or(RuleNotFoundError)?.clone();
287                rule.actions = actions;
288                self.underride.replace(rule);
289            }
290            RuleKind::Sender => {
291                let mut rule = self.sender.get(rule_id).ok_or(RuleNotFoundError)?.clone();
292                rule.actions = actions;
293                self.sender.replace(rule);
294            }
295            RuleKind::Room => {
296                let mut rule = self.room.get(rule_id).ok_or(RuleNotFoundError)?.clone();
297                rule.actions = actions;
298                self.room.replace(rule);
299            }
300            RuleKind::Content => {
301                let mut rule = self.content.get(rule_id).ok_or(RuleNotFoundError)?.clone();
302                rule.actions = actions;
303                self.content.replace(rule);
304            }
305            #[cfg(feature = "unstable-msc4306")]
306            RuleKind::PostContent => {
307                let mut rule = self.postcontent.get(rule_id).ok_or(RuleNotFoundError)?.clone();
308                rule.actions = actions;
309                self.postcontent.replace(rule);
310            }
311            RuleKind::_Custom(_) => return Err(RuleNotFoundError),
312        }
313
314        Ok(())
315    }
316
317    /// Get the first push rule that applies to this event, if any.
318    ///
319    /// # Arguments
320    ///
321    /// * `event` - The raw JSON of a room message event.
322    /// * `context` - The context of the message and room at the time of the event.
323    #[instrument(skip_all, fields(context.room_id = %context.room_id))]
324    pub async fn get_match<T>(
325        &self,
326        event: &Raw<T>,
327        context: &PushConditionRoomCtx,
328    ) -> Option<AnyPushRuleRef<'_>> {
329        let event = FlattenedJson::from_raw(event);
330
331        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
332            // no need to look at the rules if the event was by the user themselves
333            return None;
334        }
335
336        for rule in self {
337            if rule.applies(&event, context).await {
338                return Some(rule);
339            }
340        }
341
342        None
343    }
344
345    /// Get the push actions that apply to this event.
346    ///
347    /// Returns an empty slice if no push rule applies.
348    ///
349    /// # Arguments
350    ///
351    /// * `event` - The raw JSON of a room message event.
352    /// * `context` - The context of the message and room at the time of the event.
353    #[instrument(skip_all, fields(context.room_id = %context.room_id))]
354    pub async fn get_actions<T>(
355        &self,
356        event: &Raw<T>,
357        context: &PushConditionRoomCtx,
358    ) -> &[Action] {
359        self.get_match(event, context).await.map(|rule| rule.actions()).unwrap_or(&[])
360    }
361
362    /// Removes a user-defined rule in the rule set.
363    ///
364    /// Returns an error if the parameters are invalid.
365    pub fn remove(
366        &mut self,
367        kind: RuleKind,
368        rule_id: impl AsRef<str>,
369    ) -> Result<(), RemovePushRuleError> {
370        let rule_id = rule_id.as_ref();
371
372        if let Some(rule) = self.get(kind.clone(), rule_id) {
373            if rule.is_server_default() {
374                return Err(RemovePushRuleError::ServerDefault);
375            }
376        } else {
377            return Err(RemovePushRuleError::NotFound);
378        }
379
380        match kind {
381            RuleKind::Override => {
382                self.override_.shift_remove(rule_id);
383            }
384            RuleKind::Underride => {
385                self.underride.shift_remove(rule_id);
386            }
387            RuleKind::Sender => {
388                self.sender.shift_remove(rule_id);
389            }
390            RuleKind::Room => {
391                self.room.shift_remove(rule_id);
392            }
393            RuleKind::Content => {
394                self.content.shift_remove(rule_id);
395            }
396            #[cfg(feature = "unstable-msc4306")]
397            RuleKind::PostContent => {
398                self.postcontent.shift_remove(rule_id);
399            }
400            // This has been handled in the `self.get` call earlier.
401            RuleKind::_Custom(_) => unreachable!(),
402        }
403
404        Ok(())
405    }
406}
407
408/// A push rule is a single rule that states under what conditions an event should be passed onto a
409/// push gateway and how the notification should be presented.
410///
411/// These rules are stored on the user's homeserver. They are manually configured by the user, who
412/// can create and view them via the Client/Server API.
413///
414/// To create an instance of this type, first create a `SimplePushRuleInit` and convert it via
415/// `SimplePushRule::from` / `.into()`.
416#[derive(Clone, Debug, Deserialize, Serialize)]
417#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
418pub struct SimplePushRule<T> {
419    /// Actions to determine if and how a notification is delivered for events matching this rule.
420    pub actions: Vec<Action>,
421
422    /// Whether this is a default rule, or has been set explicitly.
423    pub default: bool,
424
425    /// Whether the push rule is enabled or not.
426    pub enabled: bool,
427
428    /// The ID of this rule.
429    ///
430    /// This is generally the Matrix ID of the entity that it applies to.
431    pub rule_id: T,
432}
433
434/// Initial set of fields of `SimplePushRule`.
435///
436/// This struct will not be updated even if additional fields are added to `SimplePushRule` in a new
437/// (non-breaking) release of the Matrix specification.
438#[derive(Debug)]
439#[allow(clippy::exhaustive_structs)]
440pub struct SimplePushRuleInit<T> {
441    /// Actions to determine if and how a notification is delivered for events matching this rule.
442    pub actions: Vec<Action>,
443
444    /// Whether this is a default rule, or has been set explicitly.
445    pub default: bool,
446
447    /// Whether the push rule is enabled or not.
448    pub enabled: bool,
449
450    /// The ID of this rule.
451    ///
452    /// This is generally the Matrix ID of the entity that it applies to.
453    pub rule_id: T,
454}
455
456impl<T> From<SimplePushRuleInit<T>> for SimplePushRule<T> {
457    fn from(init: SimplePushRuleInit<T>) -> Self {
458        let SimplePushRuleInit { actions, default, enabled, rule_id } = init;
459        Self { actions, default, enabled, rule_id }
460    }
461}
462
463// The following trait are needed to be able to make
464// an IndexSet of the type
465
466impl<T> Hash for SimplePushRule<T>
467where
468    T: Hash,
469{
470    fn hash<H: Hasher>(&self, state: &mut H) {
471        self.rule_id.hash(state);
472    }
473}
474
475impl<T> PartialEq for SimplePushRule<T>
476where
477    T: PartialEq<T>,
478{
479    fn eq(&self, other: &Self) -> bool {
480        self.rule_id == other.rule_id
481    }
482}
483
484impl<T> Eq for SimplePushRule<T> where T: Eq {}
485
486impl<T> Equivalent<SimplePushRule<T>> for str
487where
488    T: AsRef<str>,
489{
490    fn equivalent(&self, key: &SimplePushRule<T>) -> bool {
491        self == key.rule_id.as_ref()
492    }
493}
494
495/// Like `SimplePushRule`, but with an additional `conditions` field.
496///
497/// Only applicable to underride and override rules.
498///
499/// To create an instance of this type, first create a `ConditionalPushRuleInit` and convert it via
500/// `ConditionalPushRule::from` / `.into()`.
501#[derive(Clone, Debug, Deserialize, Serialize)]
502#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
503pub struct ConditionalPushRule {
504    /// Actions to determine if and how a notification is delivered for events matching this rule.
505    pub actions: Vec<Action>,
506
507    /// Whether this is a default rule, or has been set explicitly.
508    pub default: bool,
509
510    /// Whether the push rule is enabled or not.
511    pub enabled: bool,
512
513    /// The ID of this rule.
514    pub rule_id: String,
515
516    /// The conditions that must hold true for an event in order for a rule to be applied to an
517    /// event.
518    ///
519    /// A rule with no conditions always matches.
520    #[serde(default)]
521    pub conditions: Vec<PushCondition>,
522}
523
524impl ConditionalPushRule {
525    /// Check if the push rule applies to the event.
526    ///
527    /// # Arguments
528    ///
529    /// * `event` - The flattened JSON representation of a room message event.
530    /// * `context` - The context of the room at the time of the event.
531    pub async fn applies(&self, event: &FlattenedJson, context: &PushConditionRoomCtx) -> bool {
532        if !self.enabled {
533            return false;
534        }
535
536        #[cfg(feature = "unstable-msc3932")]
537        {
538            // These 3 rules always apply.
539            #[allow(deprecated)]
540            if self.rule_id != PredefinedOverrideRuleId::Master.as_ref()
541                && self.rule_id != PredefinedOverrideRuleId::RoomNotif.as_ref()
542                && self.rule_id != PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
543            {
544                // Push rules which don't specify a `room_version_supports` condition are assumed
545                // to not support extensible events and are therefore expected to be treated as
546                // disabled when a room version does support extensible events.
547                let room_supports_ext_ev =
548                    context.supported_features.contains(&RoomVersionFeature::ExtensibleEvents);
549                let rule_has_room_version_supports = self.conditions.iter().any(|condition| {
550                    matches!(condition, PushCondition::RoomVersionSupports { .. })
551                });
552
553                if room_supports_ext_ev && !rule_has_room_version_supports {
554                    return false;
555                }
556            }
557        }
558
559        // The old mention rules are disabled when an m.mentions field is present.
560        #[allow(deprecated)]
561        if (self.rule_id == PredefinedOverrideRuleId::RoomNotif.as_ref()
562            || self.rule_id == PredefinedOverrideRuleId::ContainsDisplayName.as_ref())
563            && event.contains_mentions()
564        {
565            return false;
566        }
567
568        for cond in &self.conditions {
569            if !cond.applies(event, context).await {
570                return false;
571            }
572        }
573        true
574    }
575}
576
577/// Initial set of fields of `ConditionalPushRule`.
578///
579/// This struct will not be updated even if additional fields are added to `ConditionalPushRule` in
580/// a new (non-breaking) release of the Matrix specification.
581#[derive(Debug)]
582#[allow(clippy::exhaustive_structs)]
583pub struct ConditionalPushRuleInit {
584    /// Actions to determine if and how a notification is delivered for events matching this rule.
585    pub actions: Vec<Action>,
586
587    /// Whether this is a default rule, or has been set explicitly.
588    pub default: bool,
589
590    /// Whether the push rule is enabled or not.
591    pub enabled: bool,
592
593    /// The ID of this rule.
594    pub rule_id: String,
595
596    /// The conditions that must hold true for an event in order for a rule to be applied to an
597    /// event.
598    ///
599    /// A rule with no conditions always matches.
600    pub conditions: Vec<PushCondition>,
601}
602
603impl From<ConditionalPushRuleInit> for ConditionalPushRule {
604    fn from(init: ConditionalPushRuleInit) -> Self {
605        let ConditionalPushRuleInit { actions, default, enabled, rule_id, conditions } = init;
606        Self { actions, default, enabled, rule_id, conditions }
607    }
608}
609
610// The following trait are needed to be able to make
611// an IndexSet of the type
612
613impl Hash for ConditionalPushRule {
614    fn hash<H: Hasher>(&self, state: &mut H) {
615        self.rule_id.hash(state);
616    }
617}
618
619impl PartialEq for ConditionalPushRule {
620    fn eq(&self, other: &Self) -> bool {
621        self.rule_id == other.rule_id
622    }
623}
624
625impl Eq for ConditionalPushRule {}
626
627impl Equivalent<ConditionalPushRule> for str {
628    fn equivalent(&self, key: &ConditionalPushRule) -> bool {
629        self == key.rule_id
630    }
631}
632
633/// Like `SimplePushRule`, but with an additional `pattern` field.
634///
635/// Only applicable to content rules.
636///
637/// To create an instance of this type, first create a `PatternedPushRuleInit` and convert it via
638/// `PatternedPushRule::from` / `.into()`.
639#[derive(Clone, Debug, Deserialize, Serialize)]
640#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
641pub struct PatternedPushRule {
642    /// Actions to determine if and how a notification is delivered for events matching this rule.
643    pub actions: Vec<Action>,
644
645    /// Whether this is a default rule, or has been set explicitly.
646    pub default: bool,
647
648    /// Whether the push rule is enabled or not.
649    pub enabled: bool,
650
651    /// The ID of this rule.
652    pub rule_id: String,
653
654    /// The glob-style pattern to match against.
655    pub pattern: String,
656}
657
658impl PatternedPushRule {
659    /// Check if the push rule applies to the event.
660    ///
661    /// # Arguments
662    ///
663    /// * `event` - The flattened JSON representation of a room message event.
664    /// * `context` - The context of the room at the time of the event.
665    pub fn applies_to(
666        &self,
667        key: &str,
668        event: &FlattenedJson,
669        context: &PushConditionRoomCtx,
670    ) -> bool {
671        // The old mention rules are disabled when an m.mentions field is present.
672        #[allow(deprecated)]
673        if self.rule_id == PredefinedContentRuleId::ContainsUserName.as_ref()
674            && event.contains_mentions()
675        {
676            return false;
677        }
678
679        if event.get_str("sender").is_some_and(|sender| sender == context.user_id) {
680            return false;
681        }
682
683        self.enabled && condition::check_event_match(event, key, &self.pattern, context)
684    }
685}
686
687/// Initial set of fields of `PatternedPushRule`.
688///
689/// This struct will not be updated even if additional fields are added to `PatternedPushRule` in a
690/// new (non-breaking) release of the Matrix specification.
691#[derive(Debug)]
692#[allow(clippy::exhaustive_structs)]
693pub struct PatternedPushRuleInit {
694    /// Actions to determine if and how a notification is delivered for events matching this rule.
695    pub actions: Vec<Action>,
696
697    /// Whether this is a default rule, or has been set explicitly.
698    pub default: bool,
699
700    /// Whether the push rule is enabled or not.
701    pub enabled: bool,
702
703    /// The ID of this rule.
704    pub rule_id: String,
705
706    /// The glob-style pattern to match against.
707    pub pattern: String,
708}
709
710impl From<PatternedPushRuleInit> for PatternedPushRule {
711    fn from(init: PatternedPushRuleInit) -> Self {
712        let PatternedPushRuleInit { actions, default, enabled, rule_id, pattern } = init;
713        Self { actions, default, enabled, rule_id, pattern }
714    }
715}
716
717// The following trait are needed to be able to make
718// an IndexSet of the type
719
720impl Hash for PatternedPushRule {
721    fn hash<H: Hasher>(&self, state: &mut H) {
722        self.rule_id.hash(state);
723    }
724}
725
726impl PartialEq for PatternedPushRule {
727    fn eq(&self, other: &Self) -> bool {
728        self.rule_id == other.rule_id
729    }
730}
731
732impl Eq for PatternedPushRule {}
733
734impl Equivalent<PatternedPushRule> for str {
735    fn equivalent(&self, key: &PatternedPushRule) -> bool {
736        self == key.rule_id
737    }
738}
739
740/// Information for a pusher using the Push Gateway API.
741#[derive(Clone, Debug, Serialize, Deserialize)]
742#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
743pub struct HttpPusherData {
744    /// The URL to use to send notifications to.
745    ///
746    /// Required if the pusher's kind is http.
747    pub url: String,
748
749    /// The format to use when sending notifications to the Push Gateway.
750    #[serde(skip_serializing_if = "Option::is_none")]
751    pub format: Option<PushFormat>,
752
753    /// Custom data for the pusher.
754    #[serde(flatten, default, skip_serializing_if = "JsonObject::is_empty")]
755    pub data: JsonObject,
756}
757
758impl HttpPusherData {
759    /// Creates a new `HttpPusherData` with the given URL.
760    pub fn new(url: String) -> Self {
761        Self { url, format: None, data: JsonObject::default() }
762    }
763}
764
765/// A special format that the homeserver should use when sending notifications to a Push Gateway.
766/// Currently, only `event_id_only` is supported, see the [Push Gateway API][spec].
767///
768/// [spec]: https://spec.matrix.org/v1.18/push-gateway-api/#homeserver-behaviour
769#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
770#[derive(Clone, StringEnum)]
771#[ruma_enum(rename_all = "snake_case")]
772#[non_exhaustive]
773pub enum PushFormat {
774    /// Require the homeserver to only send a reduced set of fields in the push.
775    EventIdOnly,
776
777    #[doc(hidden)]
778    _Custom(PrivOwnedStr),
779}
780
781/// The kinds of push rules that are available.
782#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
783#[derive(Clone, StringEnum)]
784#[ruma_enum(rename_all = "snake_case")]
785#[non_exhaustive]
786pub enum RuleKind {
787    /// User-configured rules that override all other kinds.
788    Override,
789
790    /// Lowest priority user-defined rules.
791    Underride,
792
793    /// Sender-specific rules.
794    Sender,
795
796    /// Room-specific rules.
797    Room,
798
799    /// Content-specific rules.
800    Content,
801
802    /// Post-content specific rules.
803    #[cfg(feature = "unstable-msc4306")]
804    #[ruma_enum(rename = "io.element.msc4306.postcontent")]
805    PostContent,
806
807    #[doc(hidden)]
808    _Custom(PrivOwnedStr),
809}
810
811/// A push rule to update or create.
812#[derive(Clone, Debug)]
813#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
814pub enum NewPushRule {
815    /// Rules that override all other kinds.
816    Override(NewConditionalPushRule),
817
818    /// Content-specific rules.
819    Content(NewPatternedPushRule),
820
821    /// Post-content specific rules.
822    #[cfg(feature = "unstable-msc4306")]
823    PostContent(NewConditionalPushRule),
824
825    /// Room-specific rules.
826    Room(NewSimplePushRule<OwnedRoomId>),
827
828    /// Sender-specific rules.
829    Sender(NewSimplePushRule<OwnedUserId>),
830
831    /// Lowest priority rules.
832    Underride(NewConditionalPushRule),
833}
834
835impl NewPushRule {
836    /// The kind of this `NewPushRule`.
837    pub fn kind(&self) -> RuleKind {
838        match self {
839            NewPushRule::Override(_) => RuleKind::Override,
840            NewPushRule::Content(_) => RuleKind::Content,
841            #[cfg(feature = "unstable-msc4306")]
842            NewPushRule::PostContent(_) => RuleKind::PostContent,
843            NewPushRule::Room(_) => RuleKind::Room,
844            NewPushRule::Sender(_) => RuleKind::Sender,
845            NewPushRule::Underride(_) => RuleKind::Underride,
846        }
847    }
848
849    /// The ID of this `NewPushRule`.
850    pub fn rule_id(&self) -> &str {
851        match self {
852            NewPushRule::Override(r) => &r.rule_id,
853            NewPushRule::Content(r) => &r.rule_id,
854            #[cfg(feature = "unstable-msc4306")]
855            NewPushRule::PostContent(r) => &r.rule_id,
856            NewPushRule::Room(r) => r.rule_id.as_ref(),
857            NewPushRule::Sender(r) => r.rule_id.as_ref(),
858            NewPushRule::Underride(r) => &r.rule_id,
859        }
860    }
861}
862
863/// A simple push rule to update or create.
864#[derive(Clone, Debug, Deserialize, Serialize)]
865#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
866pub struct NewSimplePushRule<T> {
867    /// The ID of this rule.
868    ///
869    /// This is generally the Matrix ID of the entity that it applies to.
870    pub rule_id: T,
871
872    /// Actions to determine if and how a notification is delivered for events matching this
873    /// rule.
874    pub actions: Vec<Action>,
875}
876
877impl<T> NewSimplePushRule<T> {
878    /// Creates a `NewSimplePushRule` with the given ID and actions.
879    pub fn new(rule_id: T, actions: Vec<Action>) -> Self {
880        Self { rule_id, actions }
881    }
882}
883
884impl<T> From<NewSimplePushRule<T>> for SimplePushRule<T> {
885    fn from(new_rule: NewSimplePushRule<T>) -> Self {
886        let NewSimplePushRule { rule_id, actions } = new_rule;
887        Self { actions, default: false, enabled: true, rule_id }
888    }
889}
890
891/// A patterned push rule to update or create.
892#[derive(Clone, Debug, Deserialize, Serialize)]
893#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
894pub struct NewPatternedPushRule {
895    /// The ID of this rule.
896    pub rule_id: String,
897
898    /// The glob-style pattern to match against.
899    pub pattern: String,
900
901    /// Actions to determine if and how a notification is delivered for events matching this
902    /// rule.
903    pub actions: Vec<Action>,
904}
905
906impl NewPatternedPushRule {
907    /// Creates a `NewPatternedPushRule` with the given ID, pattern and actions.
908    pub fn new(rule_id: String, pattern: String, actions: Vec<Action>) -> Self {
909        Self { rule_id, pattern, actions }
910    }
911}
912
913impl From<NewPatternedPushRule> for PatternedPushRule {
914    fn from(new_rule: NewPatternedPushRule) -> Self {
915        let NewPatternedPushRule { rule_id, pattern, actions } = new_rule;
916        Self { actions, default: false, enabled: true, rule_id, pattern }
917    }
918}
919
920/// A conditional push rule to update or create.
921#[derive(Clone, Debug, Deserialize, Serialize)]
922#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
923pub struct NewConditionalPushRule {
924    /// The ID of this rule.
925    pub rule_id: String,
926
927    /// The conditions that must hold true for an event in order for a rule to be applied to an
928    /// event.
929    ///
930    /// A rule with no conditions always matches.
931    #[serde(default)]
932    pub conditions: Vec<PushCondition>,
933
934    /// Actions to determine if and how a notification is delivered for events matching this
935    /// rule.
936    pub actions: Vec<Action>,
937}
938
939impl NewConditionalPushRule {
940    /// Creates a `NewConditionalPushRule` with the given ID, conditions and actions.
941    pub fn new(rule_id: String, conditions: Vec<PushCondition>, actions: Vec<Action>) -> Self {
942        Self { rule_id, conditions, actions }
943    }
944}
945
946impl From<NewConditionalPushRule> for ConditionalPushRule {
947    fn from(new_rule: NewConditionalPushRule) -> Self {
948        let NewConditionalPushRule { rule_id, conditions, actions } = new_rule;
949        Self { actions, default: false, enabled: true, rule_id, conditions }
950    }
951}
952
953/// The error type returned when trying to insert a user-defined push rule into a `Ruleset`.
954#[derive(Debug, Error)]
955#[non_exhaustive]
956pub enum InsertPushRuleError {
957    /// The rule ID starts with a dot (`.`), which is reserved for server-default rules.
958    #[error("rule IDs starting with a dot are reserved for server-default rules")]
959    ServerDefaultRuleId,
960
961    /// The rule ID contains an invalid character.
962    #[error("invalid rule ID")]
963    InvalidRuleId,
964
965    /// The rule is being placed relative to a server-default rule, which is forbidden.
966    #[error("can't place rule relative to server-default rule")]
967    RelativeToServerDefaultRule,
968
969    /// The `before` or `after` rule could not be found.
970    #[error("The before or after rule could not be found")]
971    UnknownRuleId,
972
973    /// `before` has a higher priority than `after`.
974    #[error("before has a higher priority than after")]
975    BeforeHigherThanAfter,
976}
977
978/// The error type returned when trying modify a push rule that could not be found in a `Ruleset`.
979#[derive(Debug, Error)]
980#[non_exhaustive]
981#[error("The rule could not be found")]
982pub struct RuleNotFoundError;
983
984/// Insert the rule in the given indexset and move it to the given position.
985pub fn insert_and_move_rule<T>(
986    set: &mut IndexSet<T>,
987    rule: T,
988    default_position: usize,
989    after: Option<&str>,
990    before: Option<&str>,
991) -> Result<(), InsertPushRuleError>
992where
993    T: Hash + Eq,
994    str: Equivalent<T>,
995{
996    let (from, replaced) = set.replace_full(rule);
997
998    let mut to = default_position;
999
1000    if let Some(rule_id) = after {
1001        let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
1002        to = idx + 1;
1003    }
1004    if let Some(rule_id) = before {
1005        let idx = set.get_index_of(rule_id).ok_or(InsertPushRuleError::UnknownRuleId)?;
1006
1007        if idx < to {
1008            return Err(InsertPushRuleError::BeforeHigherThanAfter);
1009        }
1010
1011        to = idx;
1012    }
1013
1014    // Only move the item if it's new or if it was positioned.
1015    if replaced.is_none() || after.is_some() || before.is_some() {
1016        set.move_index(from, to);
1017    }
1018
1019    Ok(())
1020}
1021
1022/// The error type returned when trying to remove a user-defined push rule from a `Ruleset`.
1023#[derive(Debug, Error)]
1024#[non_exhaustive]
1025pub enum RemovePushRuleError {
1026    /// The rule is a server-default rules and they can't be removed.
1027    #[error("server-default rules cannot be removed")]
1028    ServerDefault,
1029
1030    /// The rule was not found.
1031    #[error("rule not found")]
1032    NotFound,
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037    use std::{collections::BTreeMap, sync::LazyLock};
1038
1039    use assert_matches2::{assert_let, assert_matches};
1040    use js_int::{int, uint};
1041    use macro_rules_attribute::apply;
1042    use serde_json::{
1043        Value as JsonValue, from_value as from_json_value, json, value::RawValue as RawJsonValue,
1044    };
1045    use smol_macros::test;
1046
1047    use super::{
1048        AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
1049        action::{Action, Tweak},
1050        condition::{
1051            EventMatchConditionData, PushCondition, PushConditionPowerLevelsCtx,
1052            PushConditionRoomCtx, RoomMemberCountConditionData, RoomMemberCountIs,
1053            SenderNotificationPermissionConditionData,
1054        },
1055    };
1056    use crate::{
1057        assert_to_canonical_json_eq, owned_room_id, owned_user_id,
1058        power_levels::NotificationPowerLevels,
1059        push::{
1060            HighlightTweakValue, PredefinedContentRuleId, PredefinedOverrideRuleId, SoundTweakValue,
1061        },
1062        room_version_rules::{AuthorizationRules, RoomPowerLevelsRules},
1063        serde::Raw,
1064        user_id,
1065    };
1066
1067    fn example_ruleset() -> Ruleset {
1068        let mut set = Ruleset::new();
1069
1070        set.override_.insert(ConditionalPushRule {
1071            conditions: vec![PushCondition::EventMatch(EventMatchConditionData::new(
1072                "type".into(),
1073                "m.call.invite".into(),
1074            ))],
1075            actions: vec![
1076                Action::Notify,
1077                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1078            ],
1079            rule_id: ".m.rule.call".into(),
1080            enabled: true,
1081            default: true,
1082        });
1083
1084        set
1085    }
1086
1087    fn power_levels() -> PushConditionPowerLevelsCtx {
1088        PushConditionPowerLevelsCtx {
1089            users: BTreeMap::new(),
1090            users_default: int!(50),
1091            notifications: NotificationPowerLevels { room: int!(50) },
1092            rules: RoomPowerLevelsRules::new(&AuthorizationRules::V1, None),
1093        }
1094    }
1095
1096    static CONTEXT_ONE_TO_ONE: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1097        let mut ctx = PushConditionRoomCtx::new(
1098            owned_room_id!("!dm:server.name"),
1099            uint!(2),
1100            owned_user_id!("@jj:server.name"),
1101            "Jolly Jumper".into(),
1102        );
1103        ctx.power_levels = Some(power_levels());
1104        ctx
1105    });
1106
1107    static CONTEXT_PUBLIC_ROOM: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1108        let mut ctx = PushConditionRoomCtx::new(
1109            owned_room_id!("!far_west:server.name"),
1110            uint!(100),
1111            owned_user_id!("@jj:server.name"),
1112            "Jolly Jumper".into(),
1113        );
1114        ctx.power_levels = Some(power_levels());
1115        ctx
1116    });
1117
1118    #[test]
1119    fn iter() {
1120        let mut set = example_ruleset();
1121
1122        let added = set.override_.insert(ConditionalPushRule {
1123            conditions: vec![PushCondition::EventMatch(EventMatchConditionData::new(
1124                "room_id".into(),
1125                "!roomid:matrix.org".into(),
1126            ))],
1127            actions: vec![],
1128            rule_id: "!roomid:matrix.org".into(),
1129            enabled: true,
1130            default: false,
1131        });
1132        assert!(added);
1133
1134        let added = set.override_.insert(ConditionalPushRule {
1135            conditions: vec![],
1136            actions: vec![],
1137            rule_id: ".m.rule.suppress_notices".into(),
1138            enabled: false,
1139            default: true,
1140        });
1141        assert!(added);
1142
1143        let mut iter = set.into_iter();
1144
1145        let rule_opt = iter.next();
1146        assert!(rule_opt.is_some());
1147        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1148        assert_eq!(rule_id, ".m.rule.call");
1149
1150        let rule_opt = iter.next();
1151        assert!(rule_opt.is_some());
1152        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1153        assert_eq!(rule_id, "!roomid:matrix.org");
1154
1155        let rule_opt = iter.next();
1156        assert!(rule_opt.is_some());
1157        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1158        assert_eq!(rule_id, ".m.rule.suppress_notices");
1159
1160        assert_matches!(iter.next(), None);
1161    }
1162
1163    #[test]
1164    fn serialize_conditional_push_rule() {
1165        let rule = ConditionalPushRule {
1166            actions: vec![
1167                Action::Notify,
1168                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1169            ],
1170            default: true,
1171            enabled: true,
1172            rule_id: ".m.rule.call".into(),
1173            conditions: vec![
1174                PushCondition::EventMatch(EventMatchConditionData::new(
1175                    "type".into(),
1176                    "m.call.invite".into(),
1177                )),
1178                #[allow(deprecated)]
1179                PushCondition::ContainsDisplayName,
1180                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1181                    RoomMemberCountIs::gt(uint!(2)),
1182                )),
1183                PushCondition::SenderNotificationPermission(
1184                    SenderNotificationPermissionConditionData::new("room".into()),
1185                ),
1186            ],
1187        };
1188
1189        assert_to_canonical_json_eq!(
1190            rule,
1191            json!({
1192                "conditions": [
1193                    {
1194                        "kind": "event_match",
1195                        "key": "type",
1196                        "pattern": "m.call.invite"
1197                    },
1198                    {
1199                        "kind": "contains_display_name"
1200                    },
1201                    {
1202                        "kind": "room_member_count",
1203                        "is": ">2"
1204                    },
1205                    {
1206                        "kind": "sender_notification_permission",
1207                        "key": "room"
1208                    }
1209                ],
1210                "actions": [
1211                    "notify",
1212                    {
1213                        "set_tweak": "highlight"
1214                    }
1215                ],
1216                "rule_id": ".m.rule.call",
1217                "default": true,
1218                "enabled": true
1219            })
1220        );
1221    }
1222
1223    #[test]
1224    fn serialize_simple_push_rule() {
1225        let rule = SimplePushRule {
1226            actions: vec![Action::Notify],
1227            default: false,
1228            enabled: false,
1229            rule_id: owned_room_id!("!roomid:server.name"),
1230        };
1231
1232        assert_to_canonical_json_eq!(
1233            rule,
1234            json!({
1235                "actions": [
1236                    "notify"
1237                ],
1238                "rule_id": "!roomid:server.name",
1239                "default": false,
1240                "enabled": false
1241            })
1242        );
1243    }
1244
1245    #[test]
1246    fn serialize_patterned_push_rule() {
1247        let rule = PatternedPushRule {
1248            actions: vec![
1249                Action::Notify,
1250                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1251                Action::SetTweak(
1252                    Tweak::new(
1253                        "dance".into(),
1254                        Some(RawJsonValue::from_string("true".into()).unwrap()),
1255                    )
1256                    .unwrap(),
1257                ),
1258            ],
1259            default: true,
1260            enabled: true,
1261            pattern: "user_id".into(),
1262            rule_id: ".m.rule.contains_user_name".into(),
1263        };
1264
1265        assert_to_canonical_json_eq!(
1266            rule,
1267            json!({
1268                "actions": [
1269                    "notify",
1270                    {
1271                        "set_tweak": "sound",
1272                        "value": "default"
1273                    },
1274                    {
1275                        "set_tweak": "dance",
1276                        "value": true
1277                    }
1278                ],
1279                "pattern": "user_id",
1280                "rule_id": ".m.rule.contains_user_name",
1281                "default": true,
1282                "enabled": true
1283            })
1284        );
1285    }
1286
1287    #[test]
1288    fn serialize_ruleset() {
1289        let mut set = example_ruleset();
1290
1291        set.override_.insert(ConditionalPushRule {
1292            conditions: vec![
1293                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1294                    RoomMemberCountIs::from(uint!(2)),
1295                )),
1296                PushCondition::EventMatch(EventMatchConditionData::new(
1297                    "type".into(),
1298                    "m.room.message".into(),
1299                )),
1300            ],
1301            actions: vec![
1302                Action::Notify,
1303                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1304                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::No)),
1305            ],
1306            rule_id: ".m.rule.room_one_to_one".into(),
1307            enabled: true,
1308            default: true,
1309        });
1310        set.content.insert(PatternedPushRule {
1311            actions: vec![
1312                Action::Notify,
1313                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1314                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1315            ],
1316            rule_id: ".m.rule.contains_user_name".into(),
1317            pattern: "user_id".into(),
1318            enabled: true,
1319            default: true,
1320        });
1321
1322        assert_to_canonical_json_eq!(
1323            set,
1324            json!({
1325                "override": [
1326                    {
1327                        "actions": [
1328                            "notify",
1329                            {
1330                                "set_tweak": "highlight",
1331                            },
1332                        ],
1333                        "conditions": [
1334                            {
1335                                "kind": "event_match",
1336                                "key": "type",
1337                                "pattern": "m.call.invite"
1338                            },
1339                        ],
1340                        "rule_id": ".m.rule.call",
1341                        "default": true,
1342                        "enabled": true,
1343                    },
1344                    {
1345                        "conditions": [
1346                            {
1347                                "kind": "room_member_count",
1348                                "is": "2"
1349                            },
1350                            {
1351                                "kind": "event_match",
1352                                "key": "type",
1353                                "pattern": "m.room.message"
1354                            }
1355                        ],
1356                        "actions": [
1357                            "notify",
1358                            {
1359                                "set_tweak": "sound",
1360                                "value": "default"
1361                            },
1362                            {
1363                                "set_tweak": "highlight",
1364                                "value": false
1365                            }
1366                        ],
1367                        "rule_id": ".m.rule.room_one_to_one",
1368                        "default": true,
1369                        "enabled": true
1370                    },
1371                ],
1372                "content": [
1373                    {
1374                        "actions": [
1375                            "notify",
1376                            {
1377                                "set_tweak": "sound",
1378                                "value": "default"
1379                            },
1380                            {
1381                                "set_tweak": "highlight"
1382                            }
1383                        ],
1384                        "pattern": "user_id",
1385                        "rule_id": ".m.rule.contains_user_name",
1386                        "default": true,
1387                        "enabled": true
1388                    }
1389                ],
1390            })
1391        );
1392    }
1393
1394    #[test]
1395    fn deserialize_patterned_push_rule() {
1396        let rule = from_json_value::<PatternedPushRule>(json!({
1397            "actions": [
1398                "notify",
1399                {
1400                    "set_tweak": "sound",
1401                    "value": "default"
1402                },
1403                {
1404                    "set_tweak": "highlight",
1405                    "value": true
1406                }
1407            ],
1408            "pattern": "user_id",
1409            "rule_id": ".m.rule.contains_user_name",
1410            "default": true,
1411            "enabled": true
1412        }))
1413        .unwrap();
1414        assert!(rule.default);
1415        assert!(rule.enabled);
1416        assert_eq!(rule.pattern, "user_id");
1417        assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1418
1419        let mut iter = rule.actions.iter();
1420        assert_matches!(iter.next(), Some(Action::Notify));
1421        assert_matches!(
1422            iter.next(),
1423            Some(Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)))
1424        );
1425        assert_matches!(
1426            iter.next(),
1427            Some(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
1428        );
1429        assert_matches!(iter.next(), None);
1430    }
1431
1432    #[test]
1433    fn deserialize_ruleset() {
1434        let set: Ruleset = from_json_value(json!({
1435            "override": [
1436                {
1437                    "actions": [],
1438                    "conditions": [],
1439                    "rule_id": "!roomid:server.name",
1440                    "default": false,
1441                    "enabled": true
1442                },
1443                {
1444                    "actions": [],
1445                    "conditions": [],
1446                    "rule_id": ".m.rule.call",
1447                    "default": true,
1448                    "enabled": true
1449                },
1450            ],
1451            "underride": [
1452                {
1453                    "actions": [],
1454                    "conditions": [],
1455                    "rule_id": ".m.rule.room_one_to_one",
1456                    "default": true,
1457                    "enabled": true
1458                },
1459            ],
1460            "room": [
1461                {
1462                    "actions": [],
1463                    "rule_id": "!roomid:server.name",
1464                    "default": false,
1465                    "enabled": false
1466                }
1467            ],
1468            "sender": [],
1469            "content": [
1470                {
1471                    "actions": [],
1472                    "pattern": "user_id",
1473                    "rule_id": ".m.rule.contains_user_name",
1474                    "default": true,
1475                    "enabled": true
1476                },
1477                {
1478                    "actions": [],
1479                    "pattern": "ruma",
1480                    "rule_id": "ruma",
1481                    "default": false,
1482                    "enabled": true
1483                }
1484            ]
1485        }))
1486        .unwrap();
1487
1488        let mut iter = set.into_iter();
1489
1490        let rule_opt = iter.next();
1491        assert!(rule_opt.is_some());
1492        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1493        assert_eq!(rule_id, "!roomid:server.name");
1494
1495        let rule_opt = iter.next();
1496        assert!(rule_opt.is_some());
1497        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1498        assert_eq!(rule_id, ".m.rule.call");
1499
1500        let rule_opt = iter.next();
1501        assert!(rule_opt.is_some());
1502        assert_let!(AnyPushRule::Content(PatternedPushRule { rule_id, .. }) = rule_opt.unwrap());
1503        assert_eq!(rule_id, ".m.rule.contains_user_name");
1504
1505        let rule_opt = iter.next();
1506        assert!(rule_opt.is_some());
1507        assert_let!(AnyPushRule::Content(PatternedPushRule { rule_id, .. }) = rule_opt.unwrap());
1508        assert_eq!(rule_id, "ruma");
1509
1510        let rule_opt = iter.next();
1511        assert!(rule_opt.is_some());
1512        assert_let!(AnyPushRule::Room(SimplePushRule { rule_id, .. }) = rule_opt.unwrap());
1513        assert_eq!(rule_id, "!roomid:server.name");
1514
1515        let rule_opt = iter.next();
1516        assert!(rule_opt.is_some());
1517        assert_let!(
1518            AnyPushRule::Underride(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap()
1519        );
1520        assert_eq!(rule_id, ".m.rule.room_one_to_one");
1521
1522        assert_matches!(iter.next(), None);
1523    }
1524
1525    #[apply(test!)]
1526    async fn default_ruleset_applies() {
1527        let set = Ruleset::server_default(user_id!("@jj:server.name"));
1528
1529        let message = serde_json::from_str::<Raw<JsonValue>>(
1530            r#"{
1531                "type": "m.room.message"
1532            }"#,
1533        )
1534        .unwrap();
1535
1536        assert_matches!(
1537            set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1538            [Action::Notify, Action::SetTweak(Tweak::Sound(_)),]
1539        );
1540        assert_matches!(set.get_actions(&message, &CONTEXT_PUBLIC_ROOM).await, [Action::Notify]);
1541
1542        let user_mention = serde_json::from_str::<Raw<JsonValue>>(
1543            r#"{
1544                "type": "m.room.message",
1545                "content": {
1546                    "body": "Hi jolly_jumper!",
1547                    "m.mentions": {
1548                        "user_ids": ["@jj:server.name"]
1549                    }
1550                }
1551            }"#,
1552        )
1553        .unwrap();
1554
1555        assert_matches!(
1556            set.get_actions(&user_mention, &CONTEXT_ONE_TO_ONE).await,
1557            [
1558                Action::Notify,
1559                Action::SetTweak(Tweak::Sound(_)),
1560                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1561            ]
1562        );
1563        assert_matches!(
1564            set.get_actions(&user_mention, &CONTEXT_PUBLIC_ROOM).await,
1565            [
1566                Action::Notify,
1567                Action::SetTweak(Tweak::Sound(_)),
1568                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1569            ]
1570        );
1571
1572        let notice = serde_json::from_str::<Raw<JsonValue>>(
1573            r#"{
1574                "type": "m.room.message",
1575                "content": {
1576                    "msgtype": "m.notice"
1577                }
1578            }"#,
1579        )
1580        .unwrap();
1581        assert_matches!(set.get_actions(&notice, &CONTEXT_ONE_TO_ONE).await, []);
1582
1583        let room_mention = serde_json::from_str::<Raw<JsonValue>>(
1584            r#"{
1585                "type": "m.room.message",
1586                "sender": "@rantanplan:server.name",
1587                "content": {
1588                    "body": "@room Attention please!",
1589                    "msgtype": "m.text",
1590                    "m.mentions": {
1591                        "room": true
1592                    }
1593                }
1594            }"#,
1595        )
1596        .unwrap();
1597
1598        assert_matches!(
1599            set.get_actions(&room_mention, &CONTEXT_PUBLIC_ROOM).await,
1600            [Action::Notify, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1601        );
1602
1603        let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1604        assert_matches!(set.get_actions(&empty, &CONTEXT_ONE_TO_ONE).await, []);
1605    }
1606
1607    #[apply(test!)]
1608    async fn custom_ruleset_applies() {
1609        let message = serde_json::from_str::<Raw<JsonValue>>(
1610            r#"{
1611                "sender": "@rantanplan:server.name",
1612                "type": "m.room.message",
1613                "content": {
1614                    "msgtype": "m.text",
1615                    "body": "Great joke!"
1616                }
1617            }"#,
1618        )
1619        .unwrap();
1620
1621        let mut set = Ruleset::new();
1622        let disabled = ConditionalPushRule {
1623            actions: vec![Action::Notify],
1624            default: false,
1625            enabled: false,
1626            rule_id: "disabled".into(),
1627            conditions: vec![PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1628                RoomMemberCountIs::from(uint!(2)),
1629            ))],
1630        };
1631        set.underride.insert(disabled);
1632
1633        let test_set = set.clone();
1634        assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []);
1635
1636        let no_conditions = ConditionalPushRule {
1637            actions: vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
1638            default: false,
1639            enabled: true,
1640            rule_id: "no.conditions".into(),
1641            conditions: vec![],
1642        };
1643        set.underride.insert(no_conditions);
1644
1645        let test_set = set.clone();
1646        assert_matches!(
1647            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1648            [Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1649        );
1650
1651        let sender = SimplePushRule {
1652            actions: vec![Action::Notify],
1653            default: false,
1654            enabled: true,
1655            rule_id: owned_user_id!("@rantanplan:server.name"),
1656        };
1657        set.sender.insert(sender);
1658
1659        let test_set = set.clone();
1660        assert_matches!(
1661            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1662            [Action::Notify]
1663        );
1664
1665        let room = SimplePushRule {
1666            actions: vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
1667            default: false,
1668            enabled: true,
1669            rule_id: owned_room_id!("!dm:server.name"),
1670        };
1671        set.room.insert(room);
1672
1673        let test_set = set.clone();
1674        assert_matches!(
1675            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1676            [Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1677        );
1678
1679        let content = PatternedPushRule {
1680            actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1681            default: false,
1682            enabled: true,
1683            rule_id: "content".into(),
1684            pattern: "joke".into(),
1685        };
1686        set.content.insert(content);
1687
1688        let test_set = set.clone();
1689        assert_matches!(
1690            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1691            [Action::SetTweak(Tweak::Sound(sound))]
1692        );
1693        assert_eq!(sound.as_str(), "content");
1694
1695        let three_conditions = ConditionalPushRule {
1696            actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1697            default: false,
1698            enabled: true,
1699            rule_id: "three.conditions".into(),
1700            conditions: vec![
1701                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1702                    RoomMemberCountIs::from(uint!(2)),
1703                )),
1704                #[allow(deprecated)]
1705                PushCondition::ContainsDisplayName,
1706                PushCondition::EventMatch(EventMatchConditionData::new(
1707                    "room_id".into(),
1708                    "!dm:server.name".into(),
1709                )),
1710            ],
1711        };
1712        set.override_.insert(three_conditions);
1713
1714        assert_matches!(
1715            set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1716            [Action::SetTweak(Tweak::Sound(sound))]
1717        );
1718        assert_eq!(sound.as_str(), "content");
1719
1720        let new_message = serde_json::from_str::<Raw<JsonValue>>(
1721            r#"{
1722                "sender": "@rantanplan:server.name",
1723                "type": "m.room.message",
1724                "content": {
1725                    "msgtype": "m.text",
1726                    "body": "Tell me another one, Jolly Jumper!"
1727                }
1728            }"#,
1729        )
1730        .unwrap();
1731
1732        assert_matches!(
1733            set.get_actions(&new_message, &CONTEXT_ONE_TO_ONE).await,
1734            [Action::SetTweak(Tweak::Sound(sound))]
1735        );
1736        assert_eq!(sound.as_str(), "three");
1737    }
1738
1739    #[apply(test!)]
1740    #[allow(deprecated)]
1741    async fn old_mentions_apply() {
1742        let mut set = Ruleset::new();
1743        set.content.insert(PatternedPushRule {
1744            rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
1745            enabled: true,
1746            default: true,
1747            pattern: "jolly_jumper".to_owned(),
1748            actions: vec![
1749                Action::Notify,
1750                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1751                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1752            ],
1753        });
1754        set.override_.extend([
1755            ConditionalPushRule {
1756                actions: vec![
1757                    Action::Notify,
1758                    Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1759                    Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1760                ],
1761                default: true,
1762                enabled: true,
1763                rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(),
1764                conditions: vec![PushCondition::ContainsDisplayName],
1765            },
1766            ConditionalPushRule {
1767                actions: vec![
1768                    Action::Notify,
1769                    Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1770                ],
1771                default: true,
1772                enabled: true,
1773                rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(),
1774                conditions: vec![
1775                    PushCondition::EventMatch(EventMatchConditionData::new(
1776                        "content.body".into(),
1777                        "@room".into(),
1778                    )),
1779                    PushCondition::SenderNotificationPermission(
1780                        SenderNotificationPermissionConditionData::new("room".into()),
1781                    ),
1782                ],
1783            },
1784        ]);
1785
1786        let message = serde_json::from_str::<Raw<JsonValue>>(
1787            r#"{
1788                "content": {
1789                    "body": "jolly_jumper"
1790                },
1791                "type": "m.room.message"
1792            }"#,
1793        )
1794        .unwrap();
1795
1796        assert_eq!(
1797            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1798            PredefinedContentRuleId::ContainsUserName.as_ref()
1799        );
1800
1801        let message = serde_json::from_str::<Raw<JsonValue>>(
1802            r#"{
1803                "content": {
1804                    "body": "jolly_jumper",
1805                    "m.mentions": {}
1806                },
1807                "type": "m.room.message"
1808            }"#,
1809        )
1810        .unwrap();
1811
1812        assert_eq!(
1813            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1814            None
1815        );
1816
1817        let message = serde_json::from_str::<Raw<JsonValue>>(
1818            r#"{
1819                "content": {
1820                    "body": "Jolly Jumper"
1821                },
1822                "type": "m.room.message"
1823            }"#,
1824        )
1825        .unwrap();
1826
1827        assert_eq!(
1828            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1829            PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1830        );
1831
1832        let message = serde_json::from_str::<Raw<JsonValue>>(
1833            r#"{
1834                "content": {
1835                    "body": "Jolly Jumper",
1836                    "m.mentions": {}
1837                },
1838                "type": "m.room.message"
1839            }"#,
1840        )
1841        .unwrap();
1842
1843        assert_eq!(
1844            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1845            None
1846        );
1847
1848        let message = serde_json::from_str::<Raw<JsonValue>>(
1849            r#"{
1850                "content": {
1851                    "body": "@room"
1852                },
1853                "sender": "@admin:server.name",
1854                "type": "m.room.message"
1855            }"#,
1856        )
1857        .unwrap();
1858
1859        assert_eq!(
1860            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1861            PredefinedOverrideRuleId::RoomNotif.as_ref()
1862        );
1863
1864        let message = serde_json::from_str::<Raw<JsonValue>>(
1865            r#"{
1866                "content": {
1867                    "body": "@room",
1868                    "m.mentions": {}
1869                },
1870                "sender": "@admin:server.name",
1871                "type": "m.room.message"
1872            }"#,
1873        )
1874        .unwrap();
1875
1876        assert_eq!(
1877            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1878            None
1879        );
1880    }
1881
1882    #[apply(test!)]
1883    async fn intentional_mentions_apply() {
1884        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1885
1886        let message = serde_json::from_str::<Raw<JsonValue>>(
1887            r#"{
1888                "content": {
1889                    "body": "Hey jolly_jumper!",
1890                    "m.mentions": {
1891                        "user_ids": ["@jolly_jumper:server.name"]
1892                    }
1893                },
1894                "sender": "@admin:server.name",
1895                "type": "m.room.message"
1896            }"#,
1897        )
1898        .unwrap();
1899
1900        assert_eq!(
1901            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1902            PredefinedOverrideRuleId::IsUserMention.as_ref()
1903        );
1904
1905        let message = serde_json::from_str::<Raw<JsonValue>>(
1906            r#"{
1907                "content": {
1908                    "body": "Listen room!",
1909                    "m.mentions": {
1910                        "room": true
1911                    }
1912                },
1913                "sender": "@admin:server.name",
1914                "type": "m.room.message"
1915            }"#,
1916        )
1917        .unwrap();
1918
1919        assert_eq!(
1920            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1921            PredefinedOverrideRuleId::IsRoomMention.as_ref()
1922        );
1923    }
1924
1925    #[apply(test!)]
1926    async fn invite_for_me_applies() {
1927        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1928
1929        // `invite_state` usually doesn't include the power levels.
1930        let context = PushConditionRoomCtx::new(
1931            owned_room_id!("!far_west:server.name"),
1932            uint!(100),
1933            owned_user_id!("@jj:server.name"),
1934            "Jolly Jumper".into(),
1935        );
1936
1937        let message = serde_json::from_str::<Raw<JsonValue>>(
1938            r#"{
1939                "content": {
1940                    "membership": "invite"
1941                },
1942                "state_key": "@jolly_jumper:server.name",
1943                "sender": "@admin:server.name",
1944                "type": "m.room.member"
1945            }"#,
1946        )
1947        .unwrap();
1948
1949        assert_eq!(
1950            set.get_match(&message, &context).await.unwrap().rule_id(),
1951            PredefinedOverrideRuleId::InviteForMe.as_ref()
1952        );
1953    }
1954}