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.19/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.19/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_matches::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    use strass::assert_let;
1047
1048    use super::{
1049        AnyPushRule, ConditionalPushRule, PatternedPushRule, Ruleset, SimplePushRule,
1050        action::{Action, Tweak},
1051        condition::{
1052            EventMatchConditionData, PushCondition, PushConditionPowerLevelsCtx,
1053            PushConditionRoomCtx, RoomMemberCountConditionData, RoomMemberCountIs,
1054            SenderNotificationPermissionConditionData,
1055        },
1056    };
1057    use crate::{
1058        assert_to_canonical_json_eq, owned_room_id, owned_user_id,
1059        power_levels::NotificationPowerLevels,
1060        push::{
1061            HighlightTweakValue, PredefinedContentRuleId, PredefinedOverrideRuleId, SoundTweakValue,
1062        },
1063        room_version_rules::{AuthorizationRules, RoomPowerLevelsRules},
1064        serde::Raw,
1065        user_id,
1066    };
1067
1068    fn example_ruleset() -> Ruleset {
1069        let mut set = Ruleset::new();
1070
1071        set.override_.insert(ConditionalPushRule {
1072            conditions: vec![PushCondition::EventMatch(EventMatchConditionData::new(
1073                "type".into(),
1074                "m.call.invite".into(),
1075            ))],
1076            actions: vec![
1077                Action::Notify,
1078                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1079            ],
1080            rule_id: ".m.rule.call".into(),
1081            enabled: true,
1082            default: true,
1083        });
1084
1085        set
1086    }
1087
1088    fn power_levels() -> PushConditionPowerLevelsCtx {
1089        PushConditionPowerLevelsCtx {
1090            users: BTreeMap::new(),
1091            users_default: int!(50),
1092            notifications: NotificationPowerLevels { room: int!(50) },
1093            rules: RoomPowerLevelsRules::new(&AuthorizationRules::V1, None),
1094        }
1095    }
1096
1097    static CONTEXT_ONE_TO_ONE: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1098        let mut ctx = PushConditionRoomCtx::new(
1099            owned_room_id!("!dm:server.name"),
1100            uint!(2),
1101            owned_user_id!("@jj:server.name"),
1102            "Jolly Jumper".into(),
1103        );
1104        ctx.power_levels = Some(power_levels());
1105        ctx
1106    });
1107
1108    static CONTEXT_PUBLIC_ROOM: LazyLock<PushConditionRoomCtx> = LazyLock::new(|| {
1109        let mut ctx = PushConditionRoomCtx::new(
1110            owned_room_id!("!far_west:server.name"),
1111            uint!(100),
1112            owned_user_id!("@jj:server.name"),
1113            "Jolly Jumper".into(),
1114        );
1115        ctx.power_levels = Some(power_levels());
1116        ctx
1117    });
1118
1119    #[test]
1120    fn iter() {
1121        let mut set = example_ruleset();
1122
1123        let added = set.override_.insert(ConditionalPushRule {
1124            conditions: vec![PushCondition::EventMatch(EventMatchConditionData::new(
1125                "room_id".into(),
1126                "!roomid:matrix.org".into(),
1127            ))],
1128            actions: vec![],
1129            rule_id: "!roomid:matrix.org".into(),
1130            enabled: true,
1131            default: false,
1132        });
1133        assert!(added);
1134
1135        let added = set.override_.insert(ConditionalPushRule {
1136            conditions: vec![],
1137            actions: vec![],
1138            rule_id: ".m.rule.suppress_notices".into(),
1139            enabled: false,
1140            default: true,
1141        });
1142        assert!(added);
1143
1144        let mut iter = set.into_iter();
1145
1146        let rule_opt = iter.next();
1147        assert!(rule_opt.is_some());
1148        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1149        assert_eq!(rule_id, ".m.rule.call");
1150
1151        let rule_opt = iter.next();
1152        assert!(rule_opt.is_some());
1153        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1154        assert_eq!(rule_id, "!roomid:matrix.org");
1155
1156        let rule_opt = iter.next();
1157        assert!(rule_opt.is_some());
1158        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1159        assert_eq!(rule_id, ".m.rule.suppress_notices");
1160
1161        assert_matches!(iter.next(), None);
1162    }
1163
1164    #[test]
1165    fn serialize_conditional_push_rule() {
1166        let rule = ConditionalPushRule {
1167            actions: vec![
1168                Action::Notify,
1169                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1170            ],
1171            default: true,
1172            enabled: true,
1173            rule_id: ".m.rule.call".into(),
1174            conditions: vec![
1175                PushCondition::EventMatch(EventMatchConditionData::new(
1176                    "type".into(),
1177                    "m.call.invite".into(),
1178                )),
1179                #[allow(deprecated)]
1180                PushCondition::ContainsDisplayName,
1181                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1182                    RoomMemberCountIs::gt(uint!(2)),
1183                )),
1184                PushCondition::SenderNotificationPermission(
1185                    SenderNotificationPermissionConditionData::new("room".into()),
1186                ),
1187            ],
1188        };
1189
1190        assert_to_canonical_json_eq!(
1191            rule,
1192            json!({
1193                "conditions": [
1194                    {
1195                        "kind": "event_match",
1196                        "key": "type",
1197                        "pattern": "m.call.invite"
1198                    },
1199                    {
1200                        "kind": "contains_display_name"
1201                    },
1202                    {
1203                        "kind": "room_member_count",
1204                        "is": ">2"
1205                    },
1206                    {
1207                        "kind": "sender_notification_permission",
1208                        "key": "room"
1209                    }
1210                ],
1211                "actions": [
1212                    "notify",
1213                    {
1214                        "set_tweak": "highlight"
1215                    }
1216                ],
1217                "rule_id": ".m.rule.call",
1218                "default": true,
1219                "enabled": true
1220            })
1221        );
1222    }
1223
1224    #[test]
1225    fn serialize_simple_push_rule() {
1226        let rule = SimplePushRule {
1227            actions: vec![Action::Notify],
1228            default: false,
1229            enabled: false,
1230            rule_id: owned_room_id!("!roomid:server.name"),
1231        };
1232
1233        assert_to_canonical_json_eq!(
1234            rule,
1235            json!({
1236                "actions": [
1237                    "notify"
1238                ],
1239                "rule_id": "!roomid:server.name",
1240                "default": false,
1241                "enabled": false
1242            })
1243        );
1244    }
1245
1246    #[test]
1247    fn serialize_patterned_push_rule() {
1248        let rule = PatternedPushRule {
1249            actions: vec![
1250                Action::Notify,
1251                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1252                Action::SetTweak(
1253                    Tweak::new(
1254                        "dance".into(),
1255                        Some(RawJsonValue::from_string("true".into()).unwrap()),
1256                    )
1257                    .unwrap(),
1258                ),
1259            ],
1260            default: true,
1261            enabled: true,
1262            pattern: "user_id".into(),
1263            rule_id: ".m.rule.contains_user_name".into(),
1264        };
1265
1266        assert_to_canonical_json_eq!(
1267            rule,
1268            json!({
1269                "actions": [
1270                    "notify",
1271                    {
1272                        "set_tweak": "sound",
1273                        "value": "default"
1274                    },
1275                    {
1276                        "set_tweak": "dance",
1277                        "value": true
1278                    }
1279                ],
1280                "pattern": "user_id",
1281                "rule_id": ".m.rule.contains_user_name",
1282                "default": true,
1283                "enabled": true
1284            })
1285        );
1286    }
1287
1288    #[test]
1289    fn serialize_ruleset() {
1290        let mut set = example_ruleset();
1291
1292        set.override_.insert(ConditionalPushRule {
1293            conditions: vec![
1294                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1295                    RoomMemberCountIs::from(uint!(2)),
1296                )),
1297                PushCondition::EventMatch(EventMatchConditionData::new(
1298                    "type".into(),
1299                    "m.room.message".into(),
1300                )),
1301            ],
1302            actions: vec![
1303                Action::Notify,
1304                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1305                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::No)),
1306            ],
1307            rule_id: ".m.rule.room_one_to_one".into(),
1308            enabled: true,
1309            default: true,
1310        });
1311        set.content.insert(PatternedPushRule {
1312            actions: vec![
1313                Action::Notify,
1314                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1315                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1316            ],
1317            rule_id: ".m.rule.contains_user_name".into(),
1318            pattern: "user_id".into(),
1319            enabled: true,
1320            default: true,
1321        });
1322
1323        assert_to_canonical_json_eq!(
1324            set,
1325            json!({
1326                "override": [
1327                    {
1328                        "actions": [
1329                            "notify",
1330                            {
1331                                "set_tweak": "highlight",
1332                            },
1333                        ],
1334                        "conditions": [
1335                            {
1336                                "kind": "event_match",
1337                                "key": "type",
1338                                "pattern": "m.call.invite"
1339                            },
1340                        ],
1341                        "rule_id": ".m.rule.call",
1342                        "default": true,
1343                        "enabled": true,
1344                    },
1345                    {
1346                        "conditions": [
1347                            {
1348                                "kind": "room_member_count",
1349                                "is": "2"
1350                            },
1351                            {
1352                                "kind": "event_match",
1353                                "key": "type",
1354                                "pattern": "m.room.message"
1355                            }
1356                        ],
1357                        "actions": [
1358                            "notify",
1359                            {
1360                                "set_tweak": "sound",
1361                                "value": "default"
1362                            },
1363                            {
1364                                "set_tweak": "highlight",
1365                                "value": false
1366                            }
1367                        ],
1368                        "rule_id": ".m.rule.room_one_to_one",
1369                        "default": true,
1370                        "enabled": true
1371                    },
1372                ],
1373                "content": [
1374                    {
1375                        "actions": [
1376                            "notify",
1377                            {
1378                                "set_tweak": "sound",
1379                                "value": "default"
1380                            },
1381                            {
1382                                "set_tweak": "highlight"
1383                            }
1384                        ],
1385                        "pattern": "user_id",
1386                        "rule_id": ".m.rule.contains_user_name",
1387                        "default": true,
1388                        "enabled": true
1389                    }
1390                ],
1391            })
1392        );
1393    }
1394
1395    #[test]
1396    fn deserialize_patterned_push_rule() {
1397        let rule = from_json_value::<PatternedPushRule>(json!({
1398            "actions": [
1399                "notify",
1400                {
1401                    "set_tweak": "sound",
1402                    "value": "default"
1403                },
1404                {
1405                    "set_tweak": "highlight",
1406                    "value": true
1407                }
1408            ],
1409            "pattern": "user_id",
1410            "rule_id": ".m.rule.contains_user_name",
1411            "default": true,
1412            "enabled": true
1413        }))
1414        .unwrap();
1415        assert!(rule.default);
1416        assert!(rule.enabled);
1417        assert_eq!(rule.pattern, "user_id");
1418        assert_eq!(rule.rule_id, ".m.rule.contains_user_name");
1419
1420        let mut iter = rule.actions.iter();
1421        assert_matches!(iter.next(), Some(Action::Notify));
1422        assert_matches!(
1423            iter.next(),
1424            Some(Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)))
1425        );
1426        assert_matches!(
1427            iter.next(),
1428            Some(Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)))
1429        );
1430        assert_matches!(iter.next(), None);
1431    }
1432
1433    #[test]
1434    fn deserialize_ruleset() {
1435        let set: Ruleset = from_json_value(json!({
1436            "override": [
1437                {
1438                    "actions": [],
1439                    "conditions": [],
1440                    "rule_id": "!roomid:server.name",
1441                    "default": false,
1442                    "enabled": true
1443                },
1444                {
1445                    "actions": [],
1446                    "conditions": [],
1447                    "rule_id": ".m.rule.call",
1448                    "default": true,
1449                    "enabled": true
1450                },
1451            ],
1452            "underride": [
1453                {
1454                    "actions": [],
1455                    "conditions": [],
1456                    "rule_id": ".m.rule.room_one_to_one",
1457                    "default": true,
1458                    "enabled": true
1459                },
1460            ],
1461            "room": [
1462                {
1463                    "actions": [],
1464                    "rule_id": "!roomid:server.name",
1465                    "default": false,
1466                    "enabled": false
1467                }
1468            ],
1469            "sender": [],
1470            "content": [
1471                {
1472                    "actions": [],
1473                    "pattern": "user_id",
1474                    "rule_id": ".m.rule.contains_user_name",
1475                    "default": true,
1476                    "enabled": true
1477                },
1478                {
1479                    "actions": [],
1480                    "pattern": "ruma",
1481                    "rule_id": "ruma",
1482                    "default": false,
1483                    "enabled": true
1484                }
1485            ]
1486        }))
1487        .unwrap();
1488
1489        let mut iter = set.into_iter();
1490
1491        let rule_opt = iter.next();
1492        assert!(rule_opt.is_some());
1493        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1494        assert_eq!(rule_id, "!roomid:server.name");
1495
1496        let rule_opt = iter.next();
1497        assert!(rule_opt.is_some());
1498        assert_let!(AnyPushRule::Override(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap());
1499        assert_eq!(rule_id, ".m.rule.call");
1500
1501        let rule_opt = iter.next();
1502        assert!(rule_opt.is_some());
1503        assert_let!(AnyPushRule::Content(PatternedPushRule { rule_id, .. }) = rule_opt.unwrap());
1504        assert_eq!(rule_id, ".m.rule.contains_user_name");
1505
1506        let rule_opt = iter.next();
1507        assert!(rule_opt.is_some());
1508        assert_let!(AnyPushRule::Content(PatternedPushRule { rule_id, .. }) = rule_opt.unwrap());
1509        assert_eq!(rule_id, "ruma");
1510
1511        let rule_opt = iter.next();
1512        assert!(rule_opt.is_some());
1513        assert_let!(AnyPushRule::Room(SimplePushRule { rule_id, .. }) = rule_opt.unwrap());
1514        assert_eq!(rule_id, "!roomid:server.name");
1515
1516        let rule_opt = iter.next();
1517        assert!(rule_opt.is_some());
1518        assert_let!(
1519            AnyPushRule::Underride(ConditionalPushRule { rule_id, .. }) = rule_opt.unwrap()
1520        );
1521        assert_eq!(rule_id, ".m.rule.room_one_to_one");
1522
1523        assert_matches!(iter.next(), None);
1524    }
1525
1526    #[apply(test!)]
1527    async fn default_ruleset_applies() {
1528        let set = Ruleset::server_default(user_id!("@jj:server.name"));
1529
1530        let message = serde_json::from_str::<Raw<JsonValue>>(
1531            r#"{
1532                "type": "m.room.message"
1533            }"#,
1534        )
1535        .unwrap();
1536
1537        assert_matches!(
1538            set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1539            [Action::Notify, Action::SetTweak(Tweak::Sound(_)),]
1540        );
1541        assert_matches!(set.get_actions(&message, &CONTEXT_PUBLIC_ROOM).await, [Action::Notify]);
1542
1543        let user_mention = serde_json::from_str::<Raw<JsonValue>>(
1544            r#"{
1545                "type": "m.room.message",
1546                "content": {
1547                    "body": "Hi jolly_jumper!",
1548                    "m.mentions": {
1549                        "user_ids": ["@jj:server.name"]
1550                    }
1551                }
1552            }"#,
1553        )
1554        .unwrap();
1555
1556        assert_matches!(
1557            set.get_actions(&user_mention, &CONTEXT_ONE_TO_ONE).await,
1558            [
1559                Action::Notify,
1560                Action::SetTweak(Tweak::Sound(_)),
1561                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1562            ]
1563        );
1564        assert_matches!(
1565            set.get_actions(&user_mention, &CONTEXT_PUBLIC_ROOM).await,
1566            [
1567                Action::Notify,
1568                Action::SetTweak(Tweak::Sound(_)),
1569                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1570            ]
1571        );
1572
1573        let notice = serde_json::from_str::<Raw<JsonValue>>(
1574            r#"{
1575                "type": "m.room.message",
1576                "content": {
1577                    "msgtype": "m.notice"
1578                }
1579            }"#,
1580        )
1581        .unwrap();
1582        assert_matches!(set.get_actions(&notice, &CONTEXT_ONE_TO_ONE).await, []);
1583
1584        let room_mention = serde_json::from_str::<Raw<JsonValue>>(
1585            r#"{
1586                "type": "m.room.message",
1587                "sender": "@rantanplan:server.name",
1588                "content": {
1589                    "body": "@room Attention please!",
1590                    "msgtype": "m.text",
1591                    "m.mentions": {
1592                        "room": true
1593                    }
1594                }
1595            }"#,
1596        )
1597        .unwrap();
1598
1599        assert_matches!(
1600            set.get_actions(&room_mention, &CONTEXT_PUBLIC_ROOM).await,
1601            [Action::Notify, Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1602        );
1603
1604        let empty = serde_json::from_str::<Raw<JsonValue>>(r#"{}"#).unwrap();
1605        assert_matches!(set.get_actions(&empty, &CONTEXT_ONE_TO_ONE).await, []);
1606    }
1607
1608    #[apply(test!)]
1609    async fn custom_ruleset_applies() {
1610        let message = serde_json::from_str::<Raw<JsonValue>>(
1611            r#"{
1612                "sender": "@rantanplan:server.name",
1613                "type": "m.room.message",
1614                "content": {
1615                    "msgtype": "m.text",
1616                    "body": "Great joke!"
1617                }
1618            }"#,
1619        )
1620        .unwrap();
1621
1622        let mut set = Ruleset::new();
1623        let disabled = ConditionalPushRule {
1624            actions: vec![Action::Notify],
1625            default: false,
1626            enabled: false,
1627            rule_id: "disabled".into(),
1628            conditions: vec![PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1629                RoomMemberCountIs::from(uint!(2)),
1630            ))],
1631        };
1632        set.underride.insert(disabled);
1633
1634        let test_set = set.clone();
1635        assert_matches!(test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await, []);
1636
1637        let no_conditions = ConditionalPushRule {
1638            actions: vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
1639            default: false,
1640            enabled: true,
1641            rule_id: "no.conditions".into(),
1642            conditions: vec![],
1643        };
1644        set.underride.insert(no_conditions);
1645
1646        let test_set = set.clone();
1647        assert_matches!(
1648            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1649            [Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1650        );
1651
1652        let sender = SimplePushRule {
1653            actions: vec![Action::Notify],
1654            default: false,
1655            enabled: true,
1656            rule_id: owned_user_id!("@rantanplan:server.name"),
1657        };
1658        set.sender.insert(sender);
1659
1660        let test_set = set.clone();
1661        assert_matches!(
1662            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1663            [Action::Notify]
1664        );
1665
1666        let room = SimplePushRule {
1667            actions: vec![Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))],
1668            default: false,
1669            enabled: true,
1670            rule_id: owned_room_id!("!dm:server.name"),
1671        };
1672        set.room.insert(room);
1673
1674        let test_set = set.clone();
1675        assert_matches!(
1676            test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await,
1677            [Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes))]
1678        );
1679
1680        let content = PatternedPushRule {
1681            actions: vec![Action::SetTweak(Tweak::Sound("content".into()))],
1682            default: false,
1683            enabled: true,
1684            rule_id: "content".into(),
1685            pattern: "joke".into(),
1686        };
1687        set.content.insert(content);
1688
1689        let test_set = set.clone();
1690        assert_let!(
1691            [Action::SetTweak(Tweak::Sound(sound))] =
1692                test_set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await
1693        );
1694        assert_eq!(sound.as_str(), "content");
1695
1696        let three_conditions = ConditionalPushRule {
1697            actions: vec![Action::SetTweak(Tweak::Sound("three".into()))],
1698            default: false,
1699            enabled: true,
1700            rule_id: "three.conditions".into(),
1701            conditions: vec![
1702                PushCondition::RoomMemberCount(RoomMemberCountConditionData::new(
1703                    RoomMemberCountIs::from(uint!(2)),
1704                )),
1705                #[allow(deprecated)]
1706                PushCondition::ContainsDisplayName,
1707                PushCondition::EventMatch(EventMatchConditionData::new(
1708                    "room_id".into(),
1709                    "!dm:server.name".into(),
1710                )),
1711            ],
1712        };
1713        set.override_.insert(three_conditions);
1714
1715        assert_let!(
1716            [Action::SetTweak(Tweak::Sound(sound))] =
1717                set.get_actions(&message, &CONTEXT_ONE_TO_ONE).await
1718        );
1719        assert_eq!(sound.as_str(), "content");
1720
1721        let new_message = serde_json::from_str::<Raw<JsonValue>>(
1722            r#"{
1723                "sender": "@rantanplan:server.name",
1724                "type": "m.room.message",
1725                "content": {
1726                    "msgtype": "m.text",
1727                    "body": "Tell me another one, Jolly Jumper!"
1728                }
1729            }"#,
1730        )
1731        .unwrap();
1732
1733        assert_let!(
1734            [Action::SetTweak(Tweak::Sound(sound))] =
1735                set.get_actions(&new_message, &CONTEXT_ONE_TO_ONE).await
1736        );
1737        assert_eq!(sound.as_str(), "three");
1738    }
1739
1740    #[apply(test!)]
1741    #[allow(deprecated)]
1742    async fn old_mentions_apply() {
1743        let mut set = Ruleset::new();
1744        set.content.insert(PatternedPushRule {
1745            rule_id: PredefinedContentRuleId::ContainsUserName.to_string(),
1746            enabled: true,
1747            default: true,
1748            pattern: "jolly_jumper".to_owned(),
1749            actions: vec![
1750                Action::Notify,
1751                Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1752                Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1753            ],
1754        });
1755        set.override_.extend([
1756            ConditionalPushRule {
1757                actions: vec![
1758                    Action::Notify,
1759                    Action::SetTweak(Tweak::Sound(SoundTweakValue::Default)),
1760                    Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1761                ],
1762                default: true,
1763                enabled: true,
1764                rule_id: PredefinedOverrideRuleId::ContainsDisplayName.to_string(),
1765                conditions: vec![PushCondition::ContainsDisplayName],
1766            },
1767            ConditionalPushRule {
1768                actions: vec![
1769                    Action::Notify,
1770                    Action::SetTweak(Tweak::Highlight(HighlightTweakValue::Yes)),
1771                ],
1772                default: true,
1773                enabled: true,
1774                rule_id: PredefinedOverrideRuleId::RoomNotif.to_string(),
1775                conditions: vec![
1776                    PushCondition::EventMatch(EventMatchConditionData::new(
1777                        "content.body".into(),
1778                        "@room".into(),
1779                    )),
1780                    PushCondition::SenderNotificationPermission(
1781                        SenderNotificationPermissionConditionData::new("room".into()),
1782                    ),
1783                ],
1784            },
1785        ]);
1786
1787        let message = serde_json::from_str::<Raw<JsonValue>>(
1788            r#"{
1789                "content": {
1790                    "body": "jolly_jumper"
1791                },
1792                "type": "m.room.message"
1793            }"#,
1794        )
1795        .unwrap();
1796
1797        assert_eq!(
1798            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1799            PredefinedContentRuleId::ContainsUserName.as_ref()
1800        );
1801
1802        let message = serde_json::from_str::<Raw<JsonValue>>(
1803            r#"{
1804                "content": {
1805                    "body": "jolly_jumper",
1806                    "m.mentions": {}
1807                },
1808                "type": "m.room.message"
1809            }"#,
1810        )
1811        .unwrap();
1812
1813        assert_eq!(
1814            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1815            None
1816        );
1817
1818        let message = serde_json::from_str::<Raw<JsonValue>>(
1819            r#"{
1820                "content": {
1821                    "body": "Jolly Jumper"
1822                },
1823                "type": "m.room.message"
1824            }"#,
1825        )
1826        .unwrap();
1827
1828        assert_eq!(
1829            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1830            PredefinedOverrideRuleId::ContainsDisplayName.as_ref()
1831        );
1832
1833        let message = serde_json::from_str::<Raw<JsonValue>>(
1834            r#"{
1835                "content": {
1836                    "body": "Jolly Jumper",
1837                    "m.mentions": {}
1838                },
1839                "type": "m.room.message"
1840            }"#,
1841        )
1842        .unwrap();
1843
1844        assert_eq!(
1845            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1846            None
1847        );
1848
1849        let message = serde_json::from_str::<Raw<JsonValue>>(
1850            r#"{
1851                "content": {
1852                    "body": "@room"
1853                },
1854                "sender": "@admin:server.name",
1855                "type": "m.room.message"
1856            }"#,
1857        )
1858        .unwrap();
1859
1860        assert_eq!(
1861            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1862            PredefinedOverrideRuleId::RoomNotif.as_ref()
1863        );
1864
1865        let message = serde_json::from_str::<Raw<JsonValue>>(
1866            r#"{
1867                "content": {
1868                    "body": "@room",
1869                    "m.mentions": {}
1870                },
1871                "sender": "@admin:server.name",
1872                "type": "m.room.message"
1873            }"#,
1874        )
1875        .unwrap();
1876
1877        assert_eq!(
1878            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.map(|rule| rule.rule_id()),
1879            None
1880        );
1881    }
1882
1883    #[apply(test!)]
1884    async fn intentional_mentions_apply() {
1885        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1886
1887        let message = serde_json::from_str::<Raw<JsonValue>>(
1888            r#"{
1889                "content": {
1890                    "body": "Hey jolly_jumper!",
1891                    "m.mentions": {
1892                        "user_ids": ["@jolly_jumper:server.name"]
1893                    }
1894                },
1895                "sender": "@admin:server.name",
1896                "type": "m.room.message"
1897            }"#,
1898        )
1899        .unwrap();
1900
1901        assert_eq!(
1902            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1903            PredefinedOverrideRuleId::IsUserMention.as_ref()
1904        );
1905
1906        let message = serde_json::from_str::<Raw<JsonValue>>(
1907            r#"{
1908                "content": {
1909                    "body": "Listen room!",
1910                    "m.mentions": {
1911                        "room": true
1912                    }
1913                },
1914                "sender": "@admin:server.name",
1915                "type": "m.room.message"
1916            }"#,
1917        )
1918        .unwrap();
1919
1920        assert_eq!(
1921            set.get_match(&message, &CONTEXT_PUBLIC_ROOM).await.unwrap().rule_id(),
1922            PredefinedOverrideRuleId::IsRoomMention.as_ref()
1923        );
1924    }
1925
1926    #[apply(test!)]
1927    async fn invite_for_me_applies() {
1928        let set = Ruleset::server_default(user_id!("@jolly_jumper:server.name"));
1929
1930        // `invite_state` usually doesn't include the power levels.
1931        let context = PushConditionRoomCtx::new(
1932            owned_room_id!("!far_west:server.name"),
1933            uint!(100),
1934            owned_user_id!("@jj:server.name"),
1935            "Jolly Jumper".into(),
1936        );
1937
1938        let message = serde_json::from_str::<Raw<JsonValue>>(
1939            r#"{
1940                "content": {
1941                    "membership": "invite"
1942                },
1943                "state_key": "@jolly_jumper:server.name",
1944                "sender": "@admin:server.name",
1945                "type": "m.room.member"
1946            }"#,
1947        )
1948        .unwrap();
1949
1950        assert_eq!(
1951            set.get_match(&message, &context).await.unwrap().rule_id(),
1952            PredefinedOverrideRuleId::InviteForMe.as_ref()
1953        );
1954    }
1955}